2

I'm playing around with the new modular system, and am running into a problem with reflection.

Somewhere in my code, I try to call the method getValue of a Map.Entry via reflection. It doesn't let me do it.

I have read a few tutorials about the new modular system, why protections against reflection ahve been set up, what are exports and opens, how and where to declare them and so on. But for this particular one I'm not sure to understand everything.

Here's the minimal code I'm able to come with:

package pkg;
import java.util.*;
import java.lang.reflect.*;

public class Test {
public static void main (String[] args) throws Exception {

Map<String,Object> someMap = new LinkedHashMap<>();
someMap.put("someKey", "someValue");
Map.Entry<String,Object> entry = someMap.entrySet().iterator().next();

System.out.println(entry.getValue()); // normal access is fine

Method getter = entry .getClass() .getMethod("getValue");
getter.setAccessible(true); //1
System.out.println(getter.invoke(entry)); //2 
}}

When calling setAccessible, it crashes on it (1) :
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make public final java.lang.Object java.util.HashMap$Node.getValue() accessible: module java.base does not "opens java.util" to module mymodule

When commenting the setAccessible (1), it crashes when calling invoke (2) :
Exception in thread "main" java.lang.IllegalAccessException: class pkg.Test (in module mymodule) cannot access a member of class java.util.HashMap$Node (in module java.base) with modifiers "public final"

Fortunately, error messages are quite explicit: I can solve the problem by using the command-line argument --add-opens java.base/java.util=mymodule, to add the missing open. But it's probably not the correct way to go long term. I have read somewhere that --add-opens/exports are going to disappear with Java 11 anyway.

Isn't there a better solution than using --add-opens ?

Normal/direct/regular access works, Method getValue is public, Map.Entry is obviously correctly exported from java.base. So why is this reflection call blocked ?

Thank you for your answers.

QuentinC
  • 12,311
  • 4
  • 24
  • 37

1 Answers1

0

Using interface's Method to call getValue solves the problem: you don't need to make your method accessible, because it is already public:

Method getter = Map.Entry.class.getMethod("getValue");
System.out.println(getter.invoke(entry));

Demo 1.

You do not need to hard-code the interface - reflection lets you search for it where available:

private static Method tryFindInterfaceMethod(Method m) throws Exception {
    for (Class bc : m.getDeclaringClass().getInterfaces()) {
        try {
    return bc.getMethod(m.getName(), m.getParameterTypes());
        } catch (NoSuchMethodException ignore) {
        }
    }
    return m;
}

Now you can access the getter as follows:

Method getter = tryFindInterfaceMethod(entry.getClass().getMethod("getValue"));

Demo 2

As far as the explanation for the behavior goes, this is probably related to a very old bug in Java reflection.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • Your demo uses java 8, while modules have been introduced with java 9. Are you sure this answer is acceptable? – Yassin Hajaj May 12 '18 at 12:25
  • 1
    @YassinHajaj This is a basic reflection problem, it manifested itself long time ago in a different form (see [Cant access to java.util.HashMap$Entry with modifiers “public final”](https://stackoverflow.com/q/12037489/335858) question for details). Essentially, it's "accessing a public method of a private class" issue. I am pretty certain that OP will be able to get this to work once he switches to using the `Method` object on `Map.Entry`'s interface. – Sergey Kalinichenko May 12 '18 at 12:27
  • Alright, because with [tag:java-9] there is a new way make packages available at runtime and I thought this was linked to the problem. – Yassin Hajaj May 12 '18 at 12:32
  • Thank you, getting the method from the interface works. However, it means that I have to change the reflection code to add a special case. The real place where I encountered that problem is of course unaware at all that it is calling a method on a object implementing Map.Entry. And what about other potential similar cases ? I hoped a more generic answer. – QuentinC May 12 '18 at 12:53
  • @QuentinC You don't have to use a special case for this if you add code to "trace" the method to the interface method that it implements. The issue boils down to a "public method of a private class". Using a method object from an interface or a public class is a good way of addressing this problem. – Sergey Kalinichenko May 12 '18 at 12:56
  • 1
    OK, I found a solution for this, probably not ideal: before invoking the method, if the class is private, then I go through implemented interfaces to see if I find a method with the same name and parameters. Didn't tried yet with edge cases like overloads and generics, but globally it works. So I can accept this answer. Thank you again. – QuentinC May 12 '18 at 13:13
  • @QuentinC I was editing the answer to add the same workaround, along with a demo. – Sergey Kalinichenko May 12 '18 at 13:22
  • In fact you need also to go through interfaces of superclasses up to Object as long as you don't find the method. For example with an entry coming from a LinkedHashMap, your code doesn't find anything because getInterfaces() returns an empty array. Interfaces seem to actually be implemented by the superclass. – QuentinC May 12 '18 at 16:54