How to properly configure Jakarta EE libraries in Maven pom.xml for Tomcat?

I have been trying in vain to implement tomcat 9’s jakarta servlet as opposed to the previous javax.servlet implementation (as its my understanding that the jakarta package is the way forward). The issue is that when I point the browser at my servlet’s url I get the following error(s)…

java.lang.ClassCastException: class cti.nm.web.Index cannot be cast to class javax.servlet.Servlet (cti.nm.web.Index is in unnamed module of loader org.apache.catalina.loader.ParallelWebappClassLoader @48c76607; javax.servlet.Servlet is in unnamed module of loader java.net.URLClassLoader @621be5d1)
    org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)
    org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
    org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:690)
    org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
    org.apache.coyote.ajp.AjpProcessor.service(AjpProcessor.java:432)
    org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
    org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:888)
    org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1597)
    org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
    java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
    org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    java.base/java.lang.Thread.run(Thread.java:832)

The problem is obvious enough. Tomcat is trying to cast my jakarta.servlet.http.HttpServlet to a javax.servlet.Servlet which clearly won’t work. What I can’t figure is how to tell it what class the servlet is actually implementing.

The class itself is declared as …

package cti.nm.web;
import jakarta.servlet.http.HttpServlet;
import java.io.IOException;
import java.io.PrintWriter;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class Index extends HttpServlet {
     
    public void doGet(HttpServletRequest request,
                      HttpServletResponse response)
      throws IOException, ServletException {

        response.setContentType("text/html");
       //print a bunch of stuff
    }
}

My web.xml file is as follows …

<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                             https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">
         
    <display-name>NMWeb</display-name>
    <description>
       NMWeb Description
    </description>

    <servlet>
        <servlet-name>Index</servlet-name>
        <servlet-class>cti.nm.web.Index</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>Index</servlet-name>
        <url-pattern>/NMWeb</url-pattern>
    </servlet-mapping>

</web-app>  

I had hoped that using the proper jakartaee xml schema in the deployment descriptor would cause the correct clases to be used but apparently not. The WAR seems to contain the proper jars …

jakarta.jakartaee-api-9.0.0.jar
tomcat-el-api-10.0.0.jar
tomcat-servlet-api-10.0.0.jar
jakarta.servlet-api-5.0.0.jar
tomcat-jsp-api-10.0.0.jar

The pom is specified as …

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>cti.nm.NMWeb</groupId>
  <artifactId>NMWeb</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>

  <build>
    <sourceDirectory>src</sourceDirectory>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>
      <plugin>
        <artifactId>maven-war-plugin</artifactId>
        <version>3.2.3</version>
        <configuration>
          <warSourceDirectory>WebContent</warSourceDirectory>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <dependency>
      <groupId>jakarta.platform</groupId>
      <artifactId>jakarta.jakartaee-api</artifactId>
      <version>[9.0.0,)</version>
      
    </dependency>
    <!-- https://mvnrepository.com/artifact/jakarta.servlet/jakarta.servlet-api -->
    <dependency>
      <groupId>jakarta.servlet</groupId>
      <artifactId>jakarta.servlet-api</artifactId>
      <version>[5.0.0,)</version>
      
    </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-jsp-api -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jsp-api</artifactId>
            <version>[10.0.0,)</version>
       
        </dependency>
  </dependencies>
</project>

It’s been days of searching the web for answers to this question and I can’t seem to find any other instances of this exact scenario. I should mention that this is an eclipse generated probject that I have manually modified. Building with both maven and eclipse generates the same results when I deploy the war file.

You’re basically physically including Tomcat 10.x specific libraries in WAR and then deploying the WAR to Tomcat 9.x. This is not the correct approach at all. Moreover, Tomcat 10.x was the first version to be Jakartified, not Tomcat 9.x.

Tomcat 9.x

For your Tomcat 9.x, which is based on Servlet 4.0, JSP 2.3, EL 3.0, WS 1.1 and JASIC 1.0, you should use javax.* imports and the entire <dependencies> section should minimally look like:

<dependencies>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>4.0.0</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>javax.servlet.jsp-api</artifactId>
        <version>2.3.0</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>javax.el</groupId>
        <artifactId>javax.el-api</artifactId>
        <version>3.0.0</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>javax.websocket</groupId>
        <artifactId>javax.websocket-api</artifactId>
        <version>1.1</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>javax.security.enterprise</groupId>
        <artifactId>javax.security.enterprise-api</artifactId>
        <version>1.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

Tomcat 10.0.x

For Tomcat 10.0.x, which is based on Servlet 5.0, JSP 3.0, EL 4.0, WS 2.0 and JASIC 2.0, you should use jakarta.* imports and the entire <dependencies> section should minimally look like:

<dependencies>
    <dependency>
        <groupId>jakarta.servlet</groupId>
        <artifactId>jakarta.servlet-api</artifactId>
        <version>5.0.0</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>jakarta.servlet.jsp</groupId>
        <artifactId>jakarta.servlet.jsp-api</artifactId>
        <version>3.0.0</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>jakarta.el</groupId>
        <artifactId>jakarta.el-api</artifactId>
        <version>4.0.0</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>jakarta.websocket</groupId>
        <artifactId>jakarta.websocket-api</artifactId>
        <version>2.0.0</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>jakarta.security.enterprise</groupId>
        <artifactId>jakarta.security.enterprise-api</artifactId>
        <version>2.0.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

Tomcat 10.1.x

For Tomcat 10.1.x, which is based on Servlet 6.0, JSP 3.1, EL 5.0, WS 2.1 and JASIC 3.0, you should use jakarta.* imports and the entire <dependencies> section should minimally look like:

<dependencies>
    <dependency>
        <groupId>jakarta.servlet</groupId>
        <artifactId>jakarta.servlet-api</artifactId>
        <version>6.0.0</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>jakarta.servlet.jsp</groupId>
        <artifactId>jakarta.servlet.jsp-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>jakarta.el</groupId>
        <artifactId>jakarta.el-api</artifactId>
        <version>5.0.0</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>jakarta.websocket</groupId>
        <artifactId>jakarta.websocket-api</artifactId>
        <version>2.1.0</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>jakarta.security.enterprise</groupId>
        <artifactId>jakarta.security.enterprise-api</artifactId>
        <version>3.0.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

In any case

Please note that the <scope> is explicitly set to provided for them, which means that Maven should not include the physical JAR files in /WEB-INF/lib of the generated WAR file (because it’s already provided by Tomcat itself!). Otherwise you only end up in runtime conflicts caused by duplicate classes in the runtime classpath.

Please also note that Tomcat is not a JEE server and thus importing either javax:javaee-api for Tomcat 9.x or jakarta.platform:jakarta.jakartaee-api for Tomcat 10.x is per definition wrong. Because it will allow you to compile your code against other JEE components such as JSF, JSTL, CDI, BV, EJB, JPA, JAX-RS, JSONB, etc etc while Tomcat actually doesn’t offer them out the box. Tomcat only offers Servlet, JSP, EL, WS and JASIC out the box, so you should only declare them in pom.xml.

For example, JSTL needs to be installed separately as instructed in How to install JSTL? The absolute uri: http://java.sun.com/jstl/core cannot be resolved and JSF needs to be installed separately as instructed in How to properly install and configure JSF libraries via Maven? and CDI needs to be installed separately as instructed in How to install and use CDI on Tomcat?

If you’re however very well aware of this limitation during developing code for Tomcat (i.e. make sure yourself that you don’t accidentally use e.g. JSTL, CDI, BV, JPA, etc without actually installing them in Tomcat first), and merely want to minimize pom.xml boilerplate, then you can also get away with this minimalist dependencies configuration for Tomcat 9.x:

<dependencies>
    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-web-api</artifactId>
        <version>8.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

Or this for Tomcat 10.0.x:

<dependencies>
    <dependency>
        <groupId>jakarta.platform</groupId>
        <artifactId>jakarta.jakartaee-web-api</artifactId>
        <version>9.0.0</version> <!-- Use 9.1.0 if you're already on Java 11 -->
        <scope>provided</scope>
    </dependency>
</dependencies>

Or this for Tomcat 10.1.x:

<dependencies>
    <dependency>
        <groupId>jakarta.platform</groupId>
        <artifactId>jakarta.jakartaee-web-api</artifactId>
        <version>10.0.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

See also:

Leave a Comment