Lambda 对私有方法的可访问性
Lambda accessibility to private methods
我对以下情况感到困惑。
考虑两个包 a
和 b
以及以下 classes:
1) MethodInvoker
只是在给定对象上调用 call()
:
package b;
import java.util.concurrent.Callable;
public class MethodInvoker {
public static void invoke(Callable r) throws Exception {
r.call();
}
}
2)
package a;
import b.MethodInvoker;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
public class Test {
private static Void method() {
System.out.println("OK");
return null;
}
public static void main(String[] args) throws Exception {
Method method = Test.class.getDeclaredMethod("method");
method.invoke(null); // ok
// TEST 1
MethodInvoker.invoke(() -> {
return method.invoke(null); // ok (hmm....
});
// TEST 2
MethodInvoker.invoke(new Callable() {
@Override
public Object call() {
return method(); // ok (hm...???
}
});
// TEST 3
MethodInvoker.invoke(new Callable() {
@Override
public Object call() throws Exception {
return method.invoke(null); // throws IllegalAccessException, why???
}
});
}
}
我明确将 method()
设为私有 以测试如何在 Test
class 范围外调用它。我通常对所有 3 个案例感到困惑,因为我发现它们相互矛盾。
我通常希望所有这些都应该以相同的方式工作。至少我希望如果 TEST 3 抛出 IllegalAccessException
,那么和 TEST 2 应该做同样的事情。但是测试 2 工作正常!
有人可以根据 JLS 给出严格的解释,为什么这些案例中的每一个都能正常工作?
TEST1 和 TEST3 之间的区别归结为 lambda 和匿名 classes 的实现方式之间的区别。
查看这些特殊情况的实际字节码总是很有趣。
https://javap.yawk.at/#jXcoec
TEST1 拉姆达:
lambda 表达式被转换为它定义的 class 中的方法。传递对该方法的方法引用。由于 lambda 方法是 class 的一部分,它可以直接访问 class 的私有方法。 method.invoke()
有效。
TEST3 匿名 class:
匿名 class 转换为 class。 method.invoke() 在不应该访问私有方法的 class 中被调用。由于反射,合成方法的解决方法不起作用。
测试 2:
为了允许嵌套 classes 访问其外部 classes 的私有成员,引入了合成方法。如果您查看字节码,您会看到一个带有签名 static java.lang.Void access[=11=]0();
的方法,它将调用转发给 Void method()
关于语言层面的无障碍,JLS §6.6.1, Determining Accessibility中有直接的说法:
…
- Otherwise, the member or constructor is declared
private
, and access is permitted if and only if it occurs within the body of the top level class (§7.6) that encloses the declaration of the member or constructor.
由于所有嵌套的classes和lambda表达式都位于同一个“顶层主体class”,这已经足以说明访问的有效性
但是 lambda 表达式与内部 classes 根本不同:
Unlike code appearing in anonymous class declarations, the meaning of names and the this
and super
keywords appearing in a lambda body, along with the accessibility of referenced declarations, are the same as in the surrounding context (except that lambda parameters introduce new names).
这表明 lambda 表达式可以访问其 class 的 private
成员,这是定义它的 class,而不是函数接口。 lambda 表达式没有实现功能接口,也没有从中继承成员。它将 type-compatible 与目标类型,并且当在运行时调用函数方法时,将有一个函数接口实例执行 lambda 表达式的主体。
这个实例的生成方式是有意未指定的。关于技术细节的评论,在参考实现 中生成的 class 可以 访问另一个 class 的 private
方法,这是必要的,因为为 lambda 表达式生成的合成方法也将是 private
。这可以通过将 MethodInvoker.invoke(Test::method);
添加到您的测试用例来说明。此方法引用允许直接调用 method
而无需在 class Test
.
中使用任何合成方法
不过,反射是另一回事。它甚至没有出现在语言规范中。这是一个图书馆功能。该库在内部 class 可访问性方面存在已知问题。这些问题与内部 class 功能本身一样古老(自 Java 1.1 起)。有 JDK-8010319
, JVM support for Java access rules in nested classes 的当前状态是定位 Java 10…
如果你真的需要内部 classes 的反射访问,你可以使用 java.lang.invoke
包:
public class Test {
private static Void method() {
System.out.println("OK");
return null;
}
public static void main(String[] args) throws Exception {
// captures the context including accessibility,
// stored in a local variable, thus only available to inner classes of this method
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle method = lookup.findStatic(Test.class, "method",
MethodType.methodType(Void.class));
// TEST 2
MethodInvoker.invoke(new Callable() {
public Object call() throws Exception {
// invoking a method handle performs no access checks
try { return (Void)method.invokeExact(); }
catch(Exception|Error e) { throw e; }
catch(Throwable t) { throw new AssertionError(t); }
}
});
// TEST 3
MethodInvoker.invoke(new Callable() {
// since lookup captured the access context, we can search for Test's
// private members even from within the inner class
MethodHandle method = lookup.findStatic(Test.class, "method",
MethodType.methodType(Void.class));
public Object call() throws Exception {
// again, invoking a method handle performs no access checks
try { return (Void)method.invokeExact(); }
catch(Exception|Error e) { throw e; }
catch(Throwable t) { throw new AssertionError(t); }
}
});
}
}
当然,由于 MethodHandles.Lookup
对象和 MethodHandle
包含无需进一步检查即可访问其创建者的 private
成员的能力,因此必须注意不要将它们交给有人无意中。但是为此,您可以解决现有的语言级别可访问性。如果您在 private
字段中存储查找对象或句柄,则只有同一顶层 class 中的代码才能访问它,如果您使用局部变量,则只有 class 同一顶层中的代码可以访问它本地范围可以访问它。
因为只有 java.lang.reflect.Method
的直接调用者才重要,另一种解决方案是使用蹦床:
public class Test {
private static Void method() {
System.out.println("OK");
return null;
}
public static void main(String[] args) throws Exception {
Method method = Test.class.getDeclaredMethod("method");
// TEST 3
MethodInvoker.invoke(new Callable() {
@Override
public Object call() throws Exception {
return invoke(method, null); // works
}
});
}
private static Object invoke(Method m, Object obj, Object... arg)
throws ReflectiveOperationException {
return m.invoke(obj, arg);
}
}
我对以下情况感到困惑。
考虑两个包 a
和 b
以及以下 classes:
1) MethodInvoker
只是在给定对象上调用 call()
:
package b;
import java.util.concurrent.Callable;
public class MethodInvoker {
public static void invoke(Callable r) throws Exception {
r.call();
}
}
2)
package a;
import b.MethodInvoker;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
public class Test {
private static Void method() {
System.out.println("OK");
return null;
}
public static void main(String[] args) throws Exception {
Method method = Test.class.getDeclaredMethod("method");
method.invoke(null); // ok
// TEST 1
MethodInvoker.invoke(() -> {
return method.invoke(null); // ok (hmm....
});
// TEST 2
MethodInvoker.invoke(new Callable() {
@Override
public Object call() {
return method(); // ok (hm...???
}
});
// TEST 3
MethodInvoker.invoke(new Callable() {
@Override
public Object call() throws Exception {
return method.invoke(null); // throws IllegalAccessException, why???
}
});
}
}
我明确将 method()
设为私有 以测试如何在 Test
class 范围外调用它。我通常对所有 3 个案例感到困惑,因为我发现它们相互矛盾。
我通常希望所有这些都应该以相同的方式工作。至少我希望如果 TEST 3 抛出 IllegalAccessException
,那么和 TEST 2 应该做同样的事情。但是测试 2 工作正常!
有人可以根据 JLS 给出严格的解释,为什么这些案例中的每一个都能正常工作?
TEST1 和 TEST3 之间的区别归结为 lambda 和匿名 classes 的实现方式之间的区别。
查看这些特殊情况的实际字节码总是很有趣。 https://javap.yawk.at/#jXcoec
TEST1 拉姆达:
lambda 表达式被转换为它定义的 class 中的方法。传递对该方法的方法引用。由于 lambda 方法是 class 的一部分,它可以直接访问 class 的私有方法。 method.invoke()
有效。
TEST3 匿名 class:
匿名 class 转换为 class。 method.invoke() 在不应该访问私有方法的 class 中被调用。由于反射,合成方法的解决方法不起作用。
测试 2:
为了允许嵌套 classes 访问其外部 classes 的私有成员,引入了合成方法。如果您查看字节码,您会看到一个带有签名 static java.lang.Void access[=11=]0();
的方法,它将调用转发给 Void method()
关于语言层面的无障碍,JLS §6.6.1, Determining Accessibility中有直接的说法:
…
- Otherwise, the member or constructor is declared
private
, and access is permitted if and only if it occurs within the body of the top level class (§7.6) that encloses the declaration of the member or constructor.
由于所有嵌套的classes和lambda表达式都位于同一个“顶层主体class”,这已经足以说明访问的有效性
但是 lambda 表达式与内部 classes 根本不同:
Unlike code appearing in anonymous class declarations, the meaning of names and the
this
andsuper
keywords appearing in a lambda body, along with the accessibility of referenced declarations, are the same as in the surrounding context (except that lambda parameters introduce new names).
这表明 lambda 表达式可以访问其 class 的 private
成员,这是定义它的 class,而不是函数接口。 lambda 表达式没有实现功能接口,也没有从中继承成员。它将 type-compatible 与目标类型,并且当在运行时调用函数方法时,将有一个函数接口实例执行 lambda 表达式的主体。
这个实例的生成方式是有意未指定的。关于技术细节的评论,在参考实现 中生成的 class 可以 访问另一个 class 的 private
方法,这是必要的,因为为 lambda 表达式生成的合成方法也将是 private
。这可以通过将 MethodInvoker.invoke(Test::method);
添加到您的测试用例来说明。此方法引用允许直接调用 method
而无需在 class Test
.
不过,反射是另一回事。它甚至没有出现在语言规范中。这是一个图书馆功能。该库在内部 class 可访问性方面存在已知问题。这些问题与内部 class 功能本身一样古老(自 Java 1.1 起)。有 JDK-8010319
, JVM support for Java access rules in nested classes 的当前状态是定位 Java 10…
如果你真的需要内部 classes 的反射访问,你可以使用 java.lang.invoke
包:
public class Test {
private static Void method() {
System.out.println("OK");
return null;
}
public static void main(String[] args) throws Exception {
// captures the context including accessibility,
// stored in a local variable, thus only available to inner classes of this method
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle method = lookup.findStatic(Test.class, "method",
MethodType.methodType(Void.class));
// TEST 2
MethodInvoker.invoke(new Callable() {
public Object call() throws Exception {
// invoking a method handle performs no access checks
try { return (Void)method.invokeExact(); }
catch(Exception|Error e) { throw e; }
catch(Throwable t) { throw new AssertionError(t); }
}
});
// TEST 3
MethodInvoker.invoke(new Callable() {
// since lookup captured the access context, we can search for Test's
// private members even from within the inner class
MethodHandle method = lookup.findStatic(Test.class, "method",
MethodType.methodType(Void.class));
public Object call() throws Exception {
// again, invoking a method handle performs no access checks
try { return (Void)method.invokeExact(); }
catch(Exception|Error e) { throw e; }
catch(Throwable t) { throw new AssertionError(t); }
}
});
}
}
当然,由于 MethodHandles.Lookup
对象和 MethodHandle
包含无需进一步检查即可访问其创建者的 private
成员的能力,因此必须注意不要将它们交给有人无意中。但是为此,您可以解决现有的语言级别可访问性。如果您在 private
字段中存储查找对象或句柄,则只有同一顶层 class 中的代码才能访问它,如果您使用局部变量,则只有 class 同一顶层中的代码可以访问它本地范围可以访问它。
因为只有 java.lang.reflect.Method
的直接调用者才重要,另一种解决方案是使用蹦床:
public class Test {
private static Void method() {
System.out.println("OK");
return null;
}
public static void main(String[] args) throws Exception {
Method method = Test.class.getDeclaredMethod("method");
// TEST 3
MethodInvoker.invoke(new Callable() {
@Override
public Object call() throws Exception {
return invoke(method, null); // works
}
});
}
private static Object invoke(Method m, Object obj, Object... arg)
throws ReflectiveOperationException {
return m.invoke(obj, arg);
}
}