1

Lets say I want to ship a program that runs on java 17 as that is what is available widely, but use reflection to detect if im running on a vm with the ability to produce a thread factory via Thread.ofVirtual().name("abc").factory(). Java prohibits reflective access to its internals when they are not properly configured with modules. How do I configure my program to be able to access this method reflectively? The reason for reflective access is to continue compiling into <jdk19 bytecode, but use reflection to use jdk19 features if they are present. Is there a combination of arguments or module-info.java contents that can achieve this goal, or is this not possible?

when you try this in jshell, here is what you get:

jshell --enable-preview
|  Welcome to JShell -- Version 19.0.2
|  For an introduction type: /help intro

jshell> Thread.class.getMethod("ofVirtual")
   ...>                 .invoke(null)
   ...>                 .getClass()
   ...>                 .getMethod("name", String.class, Long.TYPE)
   ...>                 .setAccessible(true)
|  Exception java.lang.reflect.InaccessibleObjectException: Unable to make public java.lang.Thread$Builder$OfVirtual java.lang.ThreadBuilders$VirtualThreadBuilder.name(java.lang.String,long) accessible: module java.base does not "opens java.lang" to unnamed module @30dae81
|        at AccessibleObject.throwInaccessibleObjectException (AccessibleObject.java:387)
|        at AccessibleObject.checkCanSetAccessible (AccessibleObject.java:363)
|        at AccessibleObject.checkCanSetAccessible (AccessibleObject.java:311)
|        at Method.checkCanSetAccessible (Method.java:201)
|        at Method.setAccessible (Method.java:195)
|        at (#1:5)

Exception java.lang.reflect.InaccessibleObjectException: Unable to make public java.lang.Thread$Builder$OfVirtual java.lang.ThreadBuilders$VirtualThreadBuilder.name(java.lang.String,long) accessible: module java.base does not "opens java.lang" to unnamed module @30dae81

adding required java.base; into the module-info.java does not seem to change the outcome either:

// src/main/java/module-info.java
module test_20230518_ {
    requires java.base;
}
// src/main/java/a/A.java

package a;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.ThreadFactory;

public class A {
    public static void main(String[] args) {
        ThreadFactory threadFactory = tf();
        threadFactory.newThread(() ->
                System.out.println("hi from " +
                        Thread.currentThread().getName()));
    }

    private static ThreadFactory tf() {
        Method[] methods = Thread.class.getMethods();
        boolean haveVirtual = Arrays.stream(methods)
                .anyMatch(m -> m.getName().equals("ofVirtual") &&
                        m.getParameterCount() == 0);

        if (haveVirtual) {
            try {
                Object b = Thread.class.getMethod("ofVirtual")
                        .invoke(null);
                b = b.getClass().getMethod("name", String.class, Long.TYPE)
                        .invoke(b, "prefix-", (long) 1);
                b = b.getClass().getMethod("factory")
                        .invoke(b);
                return (ThreadFactory) b;
            } catch (Throwable t) {
                throw new RuntimeException(t);
            }
        } else {
            return Thread::new;
        }
    }
}

still produces:

Exception in thread "main" java.lang.RuntimeException: java.lang.IllegalAccessException: class a.A cannot access a member of class java.lang.ThreadBuilders$VirtualThreadBuilder (in module java.base) with modifiers "public volatile"
    at a.A.tf(A.java:31)
    at a.A.main(A.java:9)
Caused by: java.lang.IllegalAccessException: class a.A cannot access a member of class java.lang.ThreadBuilders$VirtualThreadBuilder (in module java.base) with modifiers "public volatile"
    at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:420)
    at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:709)
    at java.base/java.lang.reflect.Method.invoke(Method.java:569)
    at a.A.tf(A.java:26)
    ... 1 more

class a.A cannot access a member of class java.lang.ThreadBuilders$VirtualThreadBuilder (in module java.base) with modifiers "public volatile"

Dave Ankin
  • 1,060
  • 2
  • 9
  • 20
  • 1
    Consider creating a multi-release JAR instead, with specific code for Java 19, and alternative code for Java 17. Though to be honest, I'm not sure that will work, because virtual threads are a preview feature. – Mark Rotteveel Mar 18 '23 at 11:40

2 Answers2

4

You can use Expression from the java.beans package if you don’t mind creating a dependency to the java.desktop module, but it’s worth highlighting what it does different than your approach.

You issue is not specific to the module system, but a widespread wrong use of the Reflection API. When you use getClass(), you get the actual (implementation) class of an object and it’s possible that this class is not public and hence, the methods you get with getClass().getMethod(…) are inaccessible despite overriding or implementing a public, accessible API.

Instead of trying to hotfix this by calling setAccessible(true) on the method, you should try to navigate to the right API class, either via getSuperclass() or getInterfaces(). In this specific case, we want the Thread.Builder.OfVirtual interface which matches the declared return type of Thread.ofVirtual():

private static ThreadFactory tf() {
    Optional<Method> virtual = Arrays.stream(Thread.class.getMethods())
        .filter(m -> m.getParameterCount() == 0 && m.getName().equals("ofVirtual"))
        .findAny();

    if(virtual.isPresent()) {
        try {
            Method method = virtual.orElseThrow();
            Object b = method.invoke(null);
         // matches b.getClass().getInterfaces()[0]
         //   but method.getReturnType() is more reliable
            Class<?> factoryClass = method.getReturnType();
            b = factoryClass.getMethod("name", String.class, long.class)
                .invoke(b, "prefix-", 1L);
            return (ThreadFactory)factoryClass.getMethod("factory").invoke(b);
        }
        catch(ReflectiveOperationException t) {
            throw new RuntimeException(t);
        }
    }
    else {
        return Thread::new;
    }
}
Holger
  • 285,553
  • 42
  • 434
  • 765
  • 2
    It's better to use `virtual.orElseThrow().getReturnType()`. – Johannes Kuhn Mar 21 '23 at 13:55
  • 1
    @JohannesKuhn good point. Incorporated. – Holger Mar 21 '23 at 17:35
  • 1
    Generally, in the java language every expression has a type, which is used to find the method to invoke. So, if you want to do `foo.bar().baz()` reflectively, always use the declared return type of `bar` when trying to find the `baz` method. That will save you a lot of headaches. (Many expression languages ignore that and either use `getClass()` on the target or try to reconstruct a possible target type by walking the type hierarchy.) – Johannes Kuhn Mar 23 '23 at 23:49
2

You can use the Expression class from java.beans package (from java.desktop module)

 import java.beans.Expression;
 var builder = new Expression(Thread.class, "ofVirtual", null).getValue();
 builder = new Expression(builder, "name", new Object[] {"ABC"}).getValue();
 var factory = new Expression(builder, "factory", null).getValue();