Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lambda accessibility to private methods

I'm confused with following situation.

Consider two packages a and b with following classes:

1) MethodInvoker just invokes call() on given object:

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???

            }
        });
    }
}

I explicitly made method() private to test how it can be invoked outside Test class scope. And I'm generally confused about all 3 cases, because I find them contradictory. I generally would expect that all of them should work in same way. As minimum I would expect that if TEST 3 throws IllegalAccessException, so and TEST 2 should do the same. But TEST 2 works fine!

Could somebody give strict explanation according to JLS why each of these cases work as it works?

like image 291
Andremoniy Avatar asked Feb 02 '17 20:02

Andremoniy


2 Answers

The difference between TEST1 and TEST3 boil down to the difference between how lambda and anonymous classes are implemented.

It is always interesting to look at actual bytecode of these special cases. https://javap.yawk.at/#jXcoec

TEST1 lambda:

A lambda expression is converted to a method inside the class it is defined. A method reference to that method is passed. As the lambda method is part of the class it has direct access to private methods of the class. method.invoke()works.

TEST3 anonymous class:

An anonymous class is converted to a class. method.invoke() is called in that class that should not have access to the private method. Because of reflection the workarround with synthetic methods does not work.

TEST2: To allow nested classes access to private members of their outer classes, synthetic methods are introduced. If you look at the bytecode you will see a method with the signature static java.lang.Void access$000(); that forwards the call to Void method()

like image 88
k5_ Avatar answered Sep 16 '22 12:09

k5_


Regarding the accessibility on the language level, there is a direct statement in 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.

Since all nested classes and lambda expressions reside within the same “body of the top level class”, this is already sufficient to explain the validity of the access.

But lambda expressions are fundamentally different to inner classes anyway:

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).

This makes it obvious that a lambda expression can access private members of its class, which is the class in which it is defined, not the functional interface. The lambda expression isn’t implementing the functional interface and it isn’t inheriting members from it. It will be type-compatible with the target type and there will be an instance of the functional interface that executes the lambda expression’s body when the function method in invoked at runtime.

The way this instance is produced, is intentionally unspecified. As a remark regarding the technical details, the class generated in the reference implementation can access private methods of another class, which is necessary, as the synthetic method generated for the lambda expression will be private too. This can be illustrated by adding MethodInvoker.invoke(Test::method); to your test cases. This method reference allows method to be called directly without any synthetic method within the class Test.


Reflection is a different thing, though. It doesn’t even appear in the language specification. It’s a library feature. And this library has known issues when it comes to inner class accessibility. These issues are as old as the inner class feature itself (since Java 1.1). There is JDK-8010319, JVM support for Java access rules in nested classes whose current status is targeting Java 10…

If you really need reflective access within inner classes, you can use the java.lang.invoke package:

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

Of course, since the MethodHandles.Lookup object and MethodHandle contain the ability to access private members of their creator without further checks, care must be taken not to hand them out to someone unintended. But for this, you can settle on the existing language level accessibility. If you store a lookup object or handle within a private field, only code within the same top level class can access it, if you use a local variable, only classes within the same local scope can access it.


Since only the direct caller of java.lang.reflect.Method matters, another solution is to use a trampoline:

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);
    }
}
like image 35
Holger Avatar answered Sep 19 '22 12:09

Holger