【问题标题】:Lambda accessibility to private methods私有方法的 Lambda 可访问性
【发布时间】:2017-02-02 20:26:24
【问题描述】:

我对以下情况感到困惑。

考虑两个包 ab 具有以下类:

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() private 用于测试如何在Test 类范围之外调用它。而且我通常对所有 3 个案例都感到困惑,因为我发现它们相互矛盾。 我通常希望它们都应该以相同的方式工作。至少我希望如果 TEST 3 抛出 IllegalAccessException,那么 TEST 2 也应该这样做。但是 TEST 2 工作正常!

有人可以根据 JLS 给出严格的解释,为什么每个案例都能正常工作?

【问题讨论】:

  • “有争议”你的意思是矛盾的吗?
  • @BT 或许“矛盾”更合适,是的

标签: java reflection lambda java-8 jls


【解决方案1】:

关于语言层面的可访问性,JLS §6.6.1, Determining Accessibility中有直接说法:

  • 否则,成员或构造函数被声明为private,并且当且仅当它出现在包含成员或构造函数声明的顶级类 (§7.6) 的主体中时才允许访问。李>

由于所有嵌套类和 lambda 表达式都位于同一个“顶级类的主体”中,这已经足以解释访问的有效性。

但无论如何,lambda 表达式与内部类有着根本的不同:​​

JLS §15.27.2, Lambda Body:

与出现在匿名类声明中的代码不同,名称的含义和出现在 lambda 主体中的 thissuper 关键字,以及引用声明的可访问性,与周围上下文中的相同(除了lambda 参数引入了新名称)。

这表明 lambda 表达式可以访问其类的 private 成员,这是定义它的类,而不是函数接口。 lambda 表达式没有实现函数式接口,也没有从它继承成员。它将与目标类型兼容,并且在运行时调用函数方法时,将有一个函数接口实例执行 lambda 表达式的主体。

故意未指定此实例的生成方式。作为关于技术细节的说明,参考实现中生成的类可以访问另一个类的private 方法,这是必要的,因为为 lambda 表达式生成的合成方法将是 private也。这可以通过在您的测试用例中添加MethodInvoker.invoke(Test::method); 来说明。此方法引用允许直接调用 method,而无需在类 Test 中使用任何合成方法。


不过,反思是另一回事。它甚至没有出现在语言规范中。这是一个图书馆功能。这个库在内部类可访问性方面存在已知问题。这些问题与内部类特性本身一样古老(从 Java 1.1 开始)。 JDK-8010319, JVM support for Java access rules in nested classes 的当前状态是针对 Java 10...

如果您确实需要内部类中的反射访问,可以使用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 字段中,则只有同一顶级类中的代码可以访问它,如果您使用局部变量,则只有同一本地范围内的类可以访问它。


由于只有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);
    }
}

【讨论】:

  • 感谢您的详细解释
【解决方案2】:

TEST1 和 TEST3 之间的区别归结为 lambda 和匿名类的实现方式之间的区别。

查看这些特殊情况的实际字节码总是很有趣。 https://javap.yawk.at/#jXcoec

TEST1 λ:

一个 lambda 表达式被转换为它定义的类中的一个方法。传递对该方法的方法引用。由于 lambda 方法是类的一部分,它可以直接访问类的私有方法。 method.invoke()works.

TEST3 匿名类:

匿名类被转换为类。在不应访问私有方法的类中调用 method.invoke()。由于反射,合成方法的变通方法不起作用。

测试2: 为了允许嵌套类访问其外部类的私有成员,引入了合成方法。如果您查看字节码,您将看到带有签名 static java.lang.Void access$000(); 的方法,该方法将调用转发到 Void method()

【讨论】:

  • 好的,但是,我强调我期待基于 JLS 的字符串解释
  • 抱歉弄乱了数字。现在应该修好了。抱歉,我不会提供所有内容的 jls 链接,因为这涉及太多话题。
  • 合成方法是package private,因为匿名类在同一个包中,所以足够可见
  • 是的,但是它通过反射调用了private方法,如果你解析了access方法,它将能够调用它。
  • 嗯,帮助方法的名称是编译器特定的,并且该方法仅在已经存在调用该方法的内部类时才存在。最坏的情况不会是该方法不存在。最坏的情况是有另一个内部类调用不同的方法,所以access$000 会做一些超出预期的事情。如果存在调用预期方法的内部类,甚至可能发生这种情况,因为目标方法与access$000access$001access$002 等之间的映射是不可预测的。重新编译未更改的源代码时甚至可能会发生变化。
猜你喜欢
  • 1970-01-01
  • 2010-11-23
  • 1970-01-01
  • 1970-01-01
  • 2018-05-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多