Franz Deschler
Franz Deschler

Reputation: 2574

ClassNotFoundException in embedded Jetty when using Module system

I use an embedded Jetty (11.0.13) server with Jersey (3.1.0) that provides a simple REST interface which returns JSON objects. The JSON objects are serialized using Jackson.

The setup works fine as long as I don´t use Java´s module system. But when I add the module-info.java file (see below), I get the following error as soon as I call the service.

WARNING: The following warnings have been detected: WARNING: Unknown HK2 failure detected:
MultiException stack 1 of 2
java.lang.NoClassDefFoundError: jakarta/xml/bind/annotation/XmlElement
    at com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector.<init>(JakartaXmlBindAnnotationIntrospector.java:137)
    ...
Caused by: java.lang.ClassNotFoundException: jakarta.xml.bind.annotation.XmlElement
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
    ... 83 more
MultiException stack 2 of 2
java.lang.IllegalStateException: Unable to perform operation: post construct on org.glassfish.jersey.jackson.internal.DefaultJacksonJaxbJsonProvider
    at org.jvnet.hk2.internal.ClazzCreator.create(ClazzCreator.java:429)
    at org.jvnet.hk2.internal.SystemDescriptor.create(SystemDescriptor.java:466)
    ...

To make it work, I have to add the JAX-B-API to the pom.xml and to the module-info.java. The error only occurs when using Java modules. When I simply delete the module-info.java file, everythink works fine even without the JAX-B dependency.

This is the point where I am really confused. Why do I need the JAX-B dependency when I use the module system, but not when I don´t use it? And why does the ClassNotFoundException even occur? Shouldn´t warn the module system about missing dependencies on startup?

I hope someone can explain that. It took me days to make it work.


This is the setup that produces the issue:

pom.xml

<project>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.target>17</maven.compiler.target>
        <maven.compiler.source>17</maven.compiler.source>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>11.0.13</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-servlet</artifactId>
            <version>11.0.13</version>
        </dependency>

        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-servlet</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.inject</groupId>
            <artifactId>jersey-hk2</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-jackson</artifactId>
            <version>3.1.0</version>
        </dependency>
    </dependencies>
</project>

Main.java

public class Main {
    public static void main(String[] args) throws Exception {
        Server server = new Server(8080);
        server.setStopAtShutdown(true);
    
        ServletContextHandler context = new ServletContextHandler(server, "/");
        ServletHolder servletHolder = context.addServlet(ServletContainer.class, "/*");
        servletHolder.setInitParameter("jersey.config.server.provider.packages", "com.example.demo");
        servletHolder.setInitParameter("jersey.config.server.wadl.disableWadl", "true");
        
        server.start();
    }
}

DemoResource.java

@Path("/hello")
public class DemoResource {
    @GET
    @Produces("application/json")
    public HelloDto hello() {
        return new HelloDto("Hello, World!");
    }
    
    public record HelloDto(String value) {
        @JsonGetter("value")
        public String value() {
            return this.value;
        }
    }
}

module-info.java

module demo {
    requires org.eclipse.jetty.server;
    requires org.eclipse.jetty.servlet;
    requires jersey.container.servlet.core;
    requires jakarta.ws.rs;
    requires com.fasterxml.jackson.annotation;
}

Upvotes: 0

Views: 768

Answers (1)

Joakim Erdfelt
Joakim Erdfelt

Reputation: 49472

This is the standard JVM behavior of classpath (old school Java) and modulepath (new school Java Platform Module System, aka JPMS).

Once you have a module-info.class you have a modulepath active, and all of the access rules it has.

Your runtime can have both at the same time, and this is quite normal.

Don't rely on old school classpath to get around bad code and bad behavior, use JPMS and module-info.class and you'll know what the developers of those projects jars intend for you to use (you won't be allowed to use internal classes for example, as those are highly volatile and can change at a moments notice).

jakarta.xml.bind is required by HK2 to operate, so you have to declare it in your build dependencies to just compile, and then your module-info.java to be able to access it.

Check the other answers here on Stackoverflow for advice on how to use module-info.java properly (there's far more to it than just requires <module>).

Upvotes: 1

Related Questions