LambdaMetaFactory 和私有方法

LambdaMetaFactory and Private Methods

我想使用 LambdaMetaFactory 来高效访问私有方法。

public class Foo {
  private void bar() {  // here's what I want to invoke
    System.out.println("bar!");
  }
}

我知道这不是安全违规,因为以下代码有效:

Foo foo = new Foo();
Method m = Foo.class.getDeclaredMethod("bar");
m.setAccessible(true);
m.invoke(foo);  // output: bar!

但是,我尝试使用 LambdaMetaFactory 失败了:

MethodHandles.Lookup lookup = MethodHandles.lookup();
Method m = Foo.class.getDeclaredMethod("bar");
m.setAccessible(true);

CallSite site = LambdaMetafactory.metafactory(lookup, "accept",
        MethodType.methodType(Consumer.class),
        MethodType.methodType(void.class, Object.class),
        lookup.unreflect(m),
        MethodType.methodType(void.class, Foo.class));
Consumer<Foo> func = (Consumer<Foo>) site.getTarget().invoke();
func.accept(foo);  // IllegalAccessException: member is private

显然 m.setAccessible(true) 在这里是不够的。我尝试将 lookup 更改为 MethodHandles.privateLookupIn(Foo.class, MethodHandles.lookup()) 确实 在我的玩具示例中解决了它......但在我的实际应用程序中却没有,它生成了 IllegalAccessException 说我的 class “没有完全权限访问”。我一直无法发现为什么我的应用程序“没有完全权限访问”,或者如何修复它。

我发现几乎可以工作的唯一一件事是:

MethodHandles.Lookup original = MethodHandles.lookup();
Field internal = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
internal.setAccessible(true);
TRUSTED = (MethodHandles.Lookup) internal.get(original);

这允许我使用 TRUSTED 代替 lookup,只要我在 VM 选项中有 --illegal-access=permit,我可以这样做。这会产生一个 NoClassDefFoundError(说它找不到 Foo),这看起来很有希望......但我仍然无法弄清楚如何让它完全工作,只是产生这个错误其他的。

这里发生了什么,如何通过 LambdaMetaFactory 访问 bar

我想,您只是在玩具示例中尝试了 setAccessible(true) 方法,而不是实际应用程序。这些操作的规则之间的差异在您的情况下很小。

Method.setAccessible(boolean)

This method may be used by a caller in class C to enable access to a member of declaring class D if any of the following hold:

  • C and D are in the same module.
  • The member is public and D is public in a package that the module containing D exports to at least the module containing C.
  • The member is protected static, D is public in a package that the module containing D exports to at least the module containing C, and C is a subclass of D.
  • D is in a package that the module containing D opens to at least the module containing C. All packages in unnamed and open modules are open to all modules and so this method always succeeds when D is in an unnamed or open module.

MethodHandles.privateLookupIn(…)

A caller, specified as a Lookup object, in module M1 is allowed to do deep reflection on module M2 and package of the target class if and only if all of the following conditions are true:

[...]

  • If the caller module M1 differs from the target module M2 then both of the following must be true:
    • M1 reads M2.
    • M2 opens the package containing the target class to at least M1.

privateLookupIn(…)setAccessible(true) 有更严格的规则,这并不奇怪,因为它比允许访问特定的单个成员具有更大的影响。由于该成员是private,因此有效应用于操作的点数没有区别。如果访问者在同一个模块中,或者成员的包已 open 到调用者的模块,则授予访问权限。 (因为你在调用者代码中直接访问 class Foo,读取边显然已经存在。)

在您的玩具示例中,classes 可能在同一个模块中或根本不使用模块(在运行时被放置在未命名的模块中)。相反,应用程序代码试图访问另一个模块的成员。由于您访问 IMPL_LOOKUP 的方法确实通过使用选项 --illegal-access=permit 起作用,调用者代码必须在未命名模块中,而目标必须在不同模块中,而不是将成员的包打开到未命名模块。

这个特定于实现的查找对象是特殊的。它具有允许它访问所有内容的可信标志,但它的查找 class 是 java.lang.Object,因此它只能看到 class 对 bootstrap 加载程序可见。您必须使用 in 更改查找 class;与普通查找对象不同,这不会清除权限。

Field internal = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
internal.setAccessible(true);
TRUSTED = (MethodHandles.Lookup) internal.get(null);

MethodHandles.Lookup lookup = TRUSTED.in(Foo.class);
MethodHandle mh = lookup.findSpecial(
    Foo.class, "bar", MethodType.methodType(void.class), Foo.class);

// mh.invokeExact(foo); // could simply invoke it here

CallSite site = LambdaMetafactory.metafactory(lookup, "accept",
        MethodType.methodType(Consumer.class),
        mh.type().erase(), mh, mh.type());
Consumer<Foo> func = (Consumer<Foo>) site.getTarget().invoke();

func.accept(foo);

但需要强调的是,此 hack 仅适用于此特定实现,甚至预计将在未来版本中停止工作。实现访问的唯一简洁方法是正确设置模块,即添加适当的 opens 指令。