Search code examples
javamavenjerseyjettyjersey-3.0

Jersey Bean Validation: Unable to initialize 'jakarta.el.ExpressionFactory'


I have implemented a REST service using Jersey and wanted to use Bean Validation. When I add the bean validation maven dependency for Jersey (jersey-bean-validation), the webapp breaks on startup due to an error:

HV000183: Unable to initialize 'jakarta.el.ExpressionFactory'. Check that you have the EL dependencies on the classpath, or use ParameterMessageInterpolator instead

I searched around and found older solutions mentioning including missing dependencies, such as this question:

javax.validation.ValidationException: HV000183: Unable to load 'javax.el.ExpressionFactory'

but the dependencies are outdated in my case. Also, when looking at the dependency tree:

[INFO] org.example:valtest:war:1.0-SNAPSHOT
[INFO] +- org.glassfish.jersey.containers:jersey-container-servlet:jar:3.0.2:compile
[INFO] |  +- org.glassfish.jersey.containers:jersey-container-servlet-core:jar:3.0.2:compile
[INFO] |  +- org.glassfish.jersey.core:jersey-common:jar:3.0.2:compile
[INFO] |  |  +- jakarta.annotation:jakarta.annotation-api:jar:2.0.0:compile
[INFO] |  |  \- org.glassfish.hk2:osgi-resource-locator:jar:1.0.3:compile
[INFO] |  +- org.glassfish.jersey.core:jersey-server:jar:3.0.2:compile
[INFO] |  |  \- org.glassfish.jersey.core:jersey-client:jar:3.0.2:compile
[INFO] |  \- jakarta.ws.rs:jakarta.ws.rs-api:jar:3.0.0:compile
[INFO] +- org.glassfish.jersey.inject:jersey-hk2:jar:3.0.2:compile
[INFO] |  +- org.glassfish.hk2:hk2-locator:jar:3.0.1:compile
[INFO] |  |  +- org.glassfish.hk2.external:aopalliance-repackaged:jar:3.0.1:compile
[INFO] |  |  +- org.glassfish.hk2:hk2-api:jar:3.0.1:compile
[INFO] |  |  \- org.glassfish.hk2:hk2-utils:jar:3.0.1:compile
[INFO] |  \- org.javassist:javassist:jar:3.25.0-GA:compile
[INFO] \- org.glassfish.jersey.ext:jersey-bean-validation:jar:3.0.2:compile
[INFO]    +- jakarta.inject:jakarta.inject-api:jar:2.0.0:compile
[INFO]    +- jakarta.validation:jakarta.validation-api:jar:3.0.0:compile
[INFO]    +- org.hibernate.validator:hibernate-validator:jar:7.0.0.Final:compile
[INFO]    |  +- org.jboss.logging:jboss-logging:jar:3.4.1.Final:compile
[INFO]    |  \- com.fasterxml:classmate:jar:1.5.1:compile
[INFO]    +- jakarta.el:jakarta.el-api:jar:4.0.0:compile
[INFO]    \- org.glassfish:jakarta.el:jar:4.0.0:compile

I can see at the very bottom that the el-api and el libraries are included as transitive dependencies of the jersey-bean-validation library already.

I put a breakpoint into ResourceBundleMessageInterpolator to find out the cause of this, and when the ExpressionFactory is getting instanciated, an exception is thrown:

java.util.ServiceConfigurationError: jakarta.el.ExpressionFactory: org.apache.el.ExpressionFactoryImpl not a subtype

I don't know where that comes from. I can at least create an ExpressionFactory as expected using:

ExpressionFactory.newInstance()

I have created a very simple example project that uses the same Jersey+Jetty setup with maven that I use in my project: https://www.dropbox.com/s/dcvief9dlqk648m/valtest.zip?dl=0 It can be run with mvn jetty:run and it should display a message on http://localhost:8080/test.

What am I missing?


Solution

  • tl;dr;

    The Jetty Maven plugin pulls in a conflicting dependency. The dependency deals with JSP. If you don't have any need for JSP in your application, I think the following solution should be safe. You just need to exclude the bad dependency

    <plugin>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-maven-plugin</artifactId>
        <version>11.0.2</version>
        <dependencies>
            <dependency>
                <groupId>org.eclipse.jetty</groupId>
                <artifactId>apache-jsp</artifactId>
                <version>11.0.2</version>
                <exclusions>
                    <exclusion>
                        <groupId>org.mortbay.jasper</groupId>
                        <artifactId>apache-el</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
        </dependencies>
    </plugin>
    

    So how the ExpressionFactory is found, is through a ServiceLoader. The org.glassfish:jakarta.el jar doesn't have the required META-INF/services file to be found through the ServiceLoader. But after the ServiceLoader fails, other options will be tried. So ultimately, the jar's ExpressionFactoryImpl will be found. But the problem is that the Jetty Maven plugin also pulls in an implementation, which does have the META-INF/services file. So that is the implementation that will be used.

    After some debugging, and stepping into the ServiceLoader code, I came across this error when the class was found:

    "jakarta.el.ExpressionFactory: org.apache.el.ExpressionFactoryImpl not a subtype"
    

    So I created a new Maven project and added the jetty-maven-plugin as a dependency. After searching through all the jars, I found the o.a.e.ExpressionFactoryImpl class. It was in the apache-el jar that Jetty pulled in. The weird part is that the class actually does implement the jakarta.el.ExpressionFactory. I thought that maybe the problem was that it implemented the old javax class, but that wasn't the case. I'm not completely sure why this causes an error. But after getting that out of the way, it began to work.

    Another option if removing apache-el is not desired

    One thing I tried before I got the above solution was to try to manually add the META-INF/services file to the the org.glassfish:jakarta.el jar. What I did was the following

    cd ~/.m2/repository/org/glassfish/jakarta.el/4.0.0
    mkdir -p META-INF/services
    echo com.sun.el.ExpressionFactoryImpl > META-INF/services/jakarta.el.ExpressionFactory
    jar uf jakarta.el-4.0.0.jar META-INF/services/jakarta.el.ExpressionFactory
    
    # You may want to make a copy of the original dependency before you do this
    # so you can easily revert back to the original if needed.
    

    This may not be a desirable solution either, but it worked.

    If you need the apache-el, you can also try to replace it with the org.glassfish:jakarta.el and see if that works. I haven't tried it yet, but I'm not sure if Jetty will run into similar problems with a different implementation or not.