Can Lambda expressions access private methods of c

2019-06-17 18:32发布

问题:

I want to gain reflective access to java.lang.String's package private constructor.

Namely, this one:

/*
* Package private constructor which shares value array for speed.
* this constructor is always expected to be called with share==true.
* a separate constructor is needed because we already have a public
* String(char[]) constructor that makes a copy of the given char[].
*/
String(char[] value, boolean share) {
    // assert share : "unshared not supported";
    this.value = value;
}

Creating a MethodHandle for it is simple enough, and so is invoking it. The same is true for using Reflection directly.

But I'm curious whether it's possible to directly call the constructor via functional interfaces.

27602758 touches on a somewhat similar issue, but the solutions provided do not appear to work in this case.

The test case below compiles without issues. Everything works, except for the actual interface invocation.

package test;

import java.lang.invoke.CallSite;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;

public class Test {

    // Creates a new String that shares the supplied char[]
    private static interface StringCreator {

        public String create(char[] value, boolean shared);
    }

    // Creates a new conventional String
    private static String create(char[] value, boolean shared) {
        return String.valueOf(value);
    }

    public static void main(String[] args) throws Throwable {
        // Reflectively generate a TRUSTED Lookup for the calling class
        Lookup caller = MethodHandles.lookup();
        Field modes = Lookup.class.getDeclaredField("allowedModes");
        modes.setAccessible(true);
        modes.setInt(caller, -1);   // -1 == Lookup.TRUSTED

        // create handle for #create()
        MethodHandle conventional = caller.findStatic(
            Test.class, "create", MethodType.methodType(String.class, char[].class, boolean.class)
        );
        StringCreator normal = getStringCreator(caller, conventional);
        System.out.println(
            normal.create("foo".toCharArray(), true)
        // prints "foo"
        );

        // create handle for shared String constructor
        MethodHandle constructor = caller.findConstructor(
            String.class, MethodType.methodType(void.class, char[].class, boolean.class)
        );
        // test directly if the construcor is correctly accessed
        char[] chars = "foo".toCharArray();
        String s = (String) constructor.invokeExact(chars, true);
        chars[0] = 'b'; // modify array contents
        chars[1] = 'a';
        chars[2] = 'r';
        System.out.println(
            s
        // prints "bar"
        );

        // generate interface for constructor
        StringCreator shared = getStringCreator(caller, constructor);
        System.out.println(
            shared.create("foo".toCharArray(), true)
        // throws error
        );
    }

    // returns a StringCreator instance
    private static StringCreator getStringCreator(Lookup caller, MethodHandle handle) throws Throwable {
        CallSite callSite = LambdaMetafactory.metafactory(
            caller,
            "create",
            MethodType.methodType(StringCreator.class),
            handle.type(),
            handle,
            handle.type()
        );
        return (StringCreator) callSite.getTarget().invokeExact();
    }
}

Specficially the instruction

shared.create("foo".toCharArray(), true)

throws the following error:

Exception in thread "main" java.lang.IllegalAccessError: tried to access method java.lang.String.<init>([CZ)V from class test.Test$$Lambda$2/989110044 at test.Test.main(Test.java:59)

Why is this error still being thrown, despite access ostensibly being granted?

Can anyone come up with an explanation for why the generated interface has no access to a method that all of its components have access to?

Is there a solution or a viable alternative that actually works for this particular use case, without reverting to pure Reflection or MethodHandles?

Because I'm stumped.

回答1:

The problem is that you override the lookup object to be trusted, so its access to a private method of String will pass the lookup procedure and lambda meta factory, but its still bound to your Test class as that’s the class which created the lookup object via MethodHandles.lookup() and the generated class will live in the same context. The JVM is quite generous regarding accessibility when it comes to these generated classes but apparently, accessing a private member of the bootstrap class java.lang.String from a class living in the context of your application class is not accepted.

You can get a lookup object living in an appropriate context via, e.g. MethodHandles.lookup() .in(String.class) (and then patching it to have private or “trusted” access), but then, you will get another problem: a class living in the context of java.lang.String (or just in the bootstrap loader’s context) will not have access to your custom interface StringCreator and can’t implement it.

The only solution is to use a lookup object living in the context of String and implementing one of the existing generic interfaces, accessible from the bootstrap class loader:

import java.lang.invoke.*;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Field;
import java.util.function.BiFunction;

public class Test {
    public static void main(String[] args) throws Throwable {
        // Reflectively generate a TRUSTED Lookup for the String class
        Lookup caller = MethodHandles.lookup().in(String.class);
        Field modes = Lookup.class.getDeclaredField("allowedModes");
        modes.setAccessible(true);
        modes.setInt(caller, -1);   // -1 == Lookup.TRUSTED
        // create handle for shared String constructor
        MethodHandle constructor = caller.findConstructor(
            String.class, MethodType.methodType(void.class, char[].class, boolean.class)
        );
        // generate interface implementation for constructor
        BiFunction<char[],Boolean,String> shared=getStringCreator(caller, constructor);

        // test if the construcor is correctly accessed
        char[] chars = "foo".toCharArray();
        String s = shared.apply(chars, true);
        chars[0] = 'b'; chars[1] = 'a'; chars[2] = 'r';// modify array contents
        System.out.println(s); // prints "bar"
        chars[0] = '1'; chars[1] = '2'; chars[2] = '3';
        System.out.println(s); // prints "123"
    }
    private static BiFunction<char[],Boolean,String> getStringCreator(
            Lookup caller, MethodHandle handle) throws Throwable {
        CallSite callSite = LambdaMetafactory.metafactory(
            caller,
            "apply",
            MethodType.methodType(BiFunction.class),
            handle.type().generic(),
            handle,
            handle.type()
        );
        return (BiFunction) callSite.getTarget().invokeExact();
    }
}