Lambda 对私有方法的可访问性

Lambda accessibility to private methods

我对以下情况感到困惑。

考虑两个包 ab 以及以下 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 根本不同:

JLS §15.27.2, Lambda Body:

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);
    }
}