How to detect virtual threads on java 19

820 Views Asked by At

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"

2

There are 2 best solutions below

0
Thiago Henrique Hupner On BEST ANSWER

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();
3
Holger On

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