Client testing Java workload on Java 9+

I am trying to follow the client testing guide, for the Java workloads.

My java version:

java -version
openjdk version "20.0.2" 2023-07-18
OpenJDK Runtime Environment Temurin-20.0.2+9 (build 20.0.2+9)
OpenJDK 64-Bit Server VM Temurin-20.0.2+9 (build 20.0.2+9, mixed mode)

I did obtain the shared files, and I can pass a classPath=... which shows that the dynamic shared object is loading.
However there is a JNIError

<Event Severity="10" Time="66.230689" DateTime="2023-10-30T08:58:16Z" Type="TryAddToClassPath" Machine="[abcd::3:4:3:1]:1" ID="0000000000000000" Path="/Users/nicolasha/repo/banksy/workload_test.jar" ThreadID="34855

82194721136708" LogGroup="default" Roles="TS" />


<Event Severity="40" ErrorKind="Unset" Time="66.230689" DateTime="2023-10-30T08:58:16Z" Type="JNIError" Machine="[abcd::3:4:3:1]:1" ID="0000000000000000" Location="/Users/nicolasha/repo/foundationdb/bindings/java/JavaWorkload.cpp:335" Error="java.lang.NullPointerException: Cannot read field "nameToModule" because "this" is null" ThreadID="3485582194721136708" Backtrace="atos -o fdbserver.debug -arch x86_64 -l 0x104748000 0x10701fb90 0x10701ff1c 0x10701c86c 0x106003328 0x1060034c4 0x1060033b8 0x104778738 0x106eb81b0 0x106eb804c 0x10514a2e8 0x18fd7d0e0" LogGroup="default" Roles="TS" />

I trace it back to this piece of code:

Which looks like it would work on Java 8, but not on Java 9+ java - How to load JAR files dynamically at Runtime? - Stack Overflow ?

So then I looked at this extract from the docs:

You can set a class path through the JVM argument -Djava.class.path=…

But I have not found a way to pass this as arguments through my workload.txt. It doesn’t seem like there is one

Am I doing something incorrectly, or is it not possible at the moment to use the client testing on Java 9+?

I have attempted to hack into the Java workload cpp code. Changing addToClassPath from

to

void addToClassPath(const std::string& path) {
    log->trace(info, "TryAddToClassPath", { { "Path", path } });
    if (!env) {
      throw JNIError{};
    }
    if (classPath.count(path) > 0) {
      // already added
      return;
    }

    auto p = env->NewStringUTF(path.c_str());
    checkException();
    auto fileClass = getClass("java/io/File");
    auto file = env->NewObject(fileClass, getMethod(fileClass, "<init>", "(Ljava/lang/String;)V"), p);
    checkException();
    auto uri = env->CallObjectMethod(file, env->GetMethodID(fileClass, "toURI", "()Ljava/net/URI;"));
    checkException();
    auto uriClass = getClass("java/net/URI");
    auto url = env->CallObjectMethod(uri, getMethod(uriClass, "toURL", "()Ljava/net/URL;"));
    checkException();
    auto classLoaderClass = getClass("java/lang/ClassLoader");
    auto sysLoaderMethod =
        env->GetStaticMethodID(classLoaderClass, "getSystemClassLoader", "()Ljava/lang/ClassLoader;");
    checkException();
    auto classLoader = env->CallStaticObjectMethod(classLoaderClass, sysLoaderMethod);
    checkException();

    auto urlLoaderClass = getClass("java/net/URLClassLoader");

    // previous official impl.
    // env->CallVoidMethod(classLoader, getMethod(urlLoaderClass, "addURL", "(Ljava/net/URL;)V"), url);

  //START
    // URLClassLoader is created with the new URL.
    //  - Grab the current thread via Thread.currentThread().
    //  - Set the context class loader of the current thread to the new URLClassLoader using Thread.setContextClassLoader(ClassLoader).
    //  - This way, the URLClassLoader with the updated classpath is set as the context class loader for the current thread, which should be used for class loading in subsequent JNI calls.
    auto urlArray = env->NewObjectArray(1, env->FindClass("java/net/URL"), url);
    checkException();

    auto urlClassLoaderConstructor = env->GetMethodID(urlLoaderClass, "<init>", "([Ljava/net/URL;Ljava/lang/ClassLoader;)V");
    checkException();

    auto urlClassLoader = env->NewObject(urlLoaderClass, urlClassLoaderConstructor, urlArray, classLoader);
    checkException();

    auto threadClass = getClass("java/lang/Thread");
    auto currentThreadMethod = env->GetStaticMethodID(threadClass, "currentThread", "()Ljava/lang/Thread;");
    checkException();

    auto currentThread = env->CallStaticObjectMethod(threadClass, currentThreadMethod);
    checkException();

    auto setContextClassLoaderMethod = env->GetMethodID(threadClass, "setContextClassLoader", "(Ljava/lang/ClassLoader;)V");
    checkException();

    env->CallVoidMethod(currentThread, setContextClassLoaderMethod, urlClassLoader);
  // END

    checkException();
    env->DeleteLocalRef(classLoader);

    checkException();
  }

I end up with another JNIError:

<Event Severity="40" ErrorKind="Unset" Time="129.769316" DateTime="2023-10-30T16:38:51Z" Type="JNIError" Machine="[abcd::3:4:3:3]:1" ID="0000000000000000" Location="/Users/nicolasha/repo/foundationdb/bindings/java/JavaWorkload.cpp:403" Error="java.lang.NoClassDefFoundError: com/apple/foundationdb/testing/AbstractWorkload" ThreadID="8147160290012382807" Backtrace="atos -o fdbserver.debug -arch x86_64 -l 0x102b1c000 0x1053f3b90 0x1053f3f1c 0x1053f086c 0x1043d7328 0x1043d74c4 0x1043d73b8 0x102b4c738 0x10528c1b0 0x10528c04c 0x10351e2e8 0x18fd7d0e0" LogGroup="default" Roles="TS" />

I think this indicates that the addToClassPath function succeeds, but then the classloader is not actually reused (?) resulting in NoClassDefFoundError down the line.

This seems to support the hypothesis that the Java client testing does not work on Java 9+, but that my attempt is incorrect / incomplete: the classloader needs to be reused in all the subsequent calls.