Mysterious classloader issue when using old JAXB on JDK11

I was trying to use javax.xml.bind in a plugin I’m developing. Everything was working fine in my functional tests. When I tried building a project that applies the plugin, it failed due to missing classes.

It was working for the tests because I have java.toolchain.languageVersion set to 8 in my plugin build, thus running with Java 8, where javax.xml.bind is still part of the standard library. It was failing for the project applying the plugin because I actually have Java 11 installed on the machine, where javax.xml.bind was removed from the standard library and has to be added as a an external dependency. (Why compile works with the installed Java 11, but languageVersion set to 8 is not really clear to me.)

I added an implementation dependency to my plugin:

dependencies {
    implementation("com.sun.xml.bind:jaxb-ri:2.3.9") {
        because("javax.xml.bind is not part of the JDK anymore starring with JDK9")
    }
}

I have two places in my plugin where I use JAXB. One place is in a task:

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;

public class WriteCompileSpecFile extends DefaultTask {
    // ...

    @TaskAction
    protected void generate() {
        DefaultHDVLCompileSpec compileSpec = new DefaultHDVLCompileSpec(getSvSource().getFiles());
        try {
            JAXBContext jaxbContext = JAXBContext.newInstance(DefaultHDVLCompileSpec.class);
            Marshaller marshaller = jaxbContext.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

            FileAdapter fileAdapter = new FileAdapter(getProject().getProjectDir());
            marshaller.setAdapter(fileAdapter);

            marshaller.marshal(compileSpec, destination.get().getAsFile());
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
    }
}

I can execute this task without any problems.

The other place where I use JAXB is in an artifact transform:

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

public abstract class WriteXrunArgsFile implements TransformAction<TransformParameters.None> {
    // ...

    private static DefaultHDVLCompileSpec getCompileSpec(File input) {
        File compileSpec = new File(input, ".gradle-hdvl/compile-spec.xml");
        try {
            JAXBContext jaxbContext = JAXBContext.newInstance(DefaultHDVLCompileSpec.class);
            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();

            FileAdapter fileAdapter = new FileAdapter(input);
            unmarshaller.setAdapter(fileAdapter);

            DefaultHDVLCompileSpec result = (DefaultHDVLCompileSpec) unmarshaller.unmarshal(compileSpec);
            for (File svSourceFile : result.getSvSourceFiles()) {
                assert svSourceFile.isAbsolute() : "not absolute: " + svSourceFile;
                assert svSourceFile.exists() : "doesn't exist: " + svSourceFile;
            }

            return result;
        } catch (JAXBException e) {
            throw new RuntimeException(e);
        }
    }
}

When the artifact transform gets executed, I get:

      > Execution failed for WriteXrunArgsFile: /home/tudor/.gradle/caches/transforms-4/2f8f369cd18dbcfb90e34fd250b46eaa/transformed/some-published-dependency-0.1.0.zip.
         > javax.xml.bind.JAXBException: Implementation of JAXB-API has not been found on module path or classpath.
            - with linked exception:
           [java.lang.ClassNotFoundException: com.sun.xml.bind.v2.ContextFactory]

I don’t understand why it works in one case (the task), but not in the other (the artifact transform). I even had a look at the JAR file pulled in by the dependency and I can find ContextFactory in there. This is the build scan: Build Scan® | Develocity.

To get this to work I just switched to com.sun.xml.bind:jaxb-ri:3.0.2, though this also required changing the namespace, so I’m not blocked by this issue.

I would just like to figure out what is going on, to maybe learn a bit more about how Gradle works. I was also a bit surprised by the entire Java EE/Jakarta situation and the backward incompatibility and the dependency hell. I always had the impression that Java highly values backward compatibility.

(Why compile works with the installed Java 11, but languageVersion set to 8 is not really clear to me.)

Why not?

By configuring to use a JVM Toolchain for language version 8, what you have installed is irrelevant.
Well, it will be used to run Gradle, but for compiling your code and running your tests, a Java 8 JDK will be used, either a recognized or auto-provisioned one, depending on your further confguration.

Even if you would use Java 11 for compiling and used the release setting with value “8”, it would use -release 8 for compilation which tells the compiler to compile against the Java 8 API and it would also work.

Just if you only used sourceCompatibility / targetCompatibility it would become a problem.

I added an implementation dependency to my plugin:

If you would have ended up doing so, I would have recommended to build two feature variants of your plugin. One for Java 8 and one for Java 9 and newer, then only add the dependency for the Java 9+ variant, so that builds using Java 8 do not need the useless external dependency. If done right, it would work automatically without the consumer doing any explicit action. But as you upgraded to the Jakarta-version anyway, you anyway always need it.

I was also a bit surprised by the entire Java EE/Jakarta situation and the backward incompatibility and the dependency hell. I always had the impression that Java highly values backward compatibility.

Yeah, that was / is a really unfortunate f***up.
When all those things were donated to the Eclipse Foundation to be developed further under their roof,
all usages of the name Java had to be removed, I guess due to trademark rules or whatever, so also all the packages were renamed from javax... to jakarta....

I don’t understand why it works in one case (the task), but not in the other (the artifact transform)

JAXBContext.newInstance searches in the thread context classloader for the jaxb implementation. For the task execution, the task class’ classloader is set as the thread context class loader and so it is found. For the transform the thread context classloader is not set so a different one that cannot find the class is used. I don’t know why this is the case. Or whether this is intentional.

You might maybe open an issue about it to Gradle to either get clarification, or the thread context classloader set for transform actions like it is for task executions.

As a workaround you can so the same as Gradle does for task executions. Store the original TCCL, set it to the right one, then in a finally reset the original TCCL: gradle/subprojects/core/src/main/java/org/gradle/api/internal/project/taskfactory/StandardTaskAction.java at master · gradle/gradle · GitHub

But this should probably really fixed in Gradle unless there is some good reason not to.

1 Like

I think I was confusing java.toolchain.languageVersion with source/targetCompatibility.

Thanks for the detailed explanation on the class loader stuff. I opened this issue on GitHub.

1 Like