Skip to main content

Resolving 'java.lang.NoClassDefFoundError: javax/servlet' When Migrating to Spring Boot 3

 The migration from Spring Boot 2.7 to Spring Boot 3.0+ is not a trivial version bump; it is a paradigm shift in the Java ecosystem. The most immediate and jarring obstacle developers face is the application crashing at startup with java.lang.NoClassDefFoundError: javax/servlet/http/HttpServlet or java.lang.ClassNotFoundException: javax.persistence.Entity.

This error signifies that your bytecode is referencing the legacy Java EE APIs (javax.*), but the runtime environment (Spring Boot 3 / Jakarta EE) only provides the modern Jakarta EE APIs (jakarta.*).

The Root Cause: The "Big Bang" Namespace Shift

To understand the fix, you must understand the architecture change. When Oracle transferred Java EE to the Eclipse Foundation to become Jakarta EE, they retained the trademark rights to the javax namespace. Consequently, starting with Jakarta EE 9, all specifications were legally required to move from the javax.* package namespace to jakarta.*.

Spring Boot 3.0 is built on Jakarta EE 10 APIs. It embeds Tomcat 10Jetty 11, or Undertow 2.3, all of which have completely stripped out support for the javax namespace.

When your application attempts to load a class expecting javax.servlet.Filter (from the Servlet API) or javax.persistence.Entity (from JPA), the ClassLoader fails because those classes literally do not exist in the Spring Boot 3 classpath. They have been replaced by jakarta.servlet.Filter and jakarta.persistence.Entity.

The Fix: Comprehensive Migration to Jakarta EE

Resolving this requires a coordinated update of your build configuration, source code, and third-party dependencies.

1. Upgrade Build Configuration (Maven)

Spring Boot 3 requires Java 17 as a baseline. Ensure your pom.xml reflects the new parent and Java version.

<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.3</version> <!-- Use latest stable Spring Boot 3.x -->
        <relativePath/> 
    </parent>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!-- Ensure 3rd party libs are Jakarta compatible -->
    </dependencies>
</project>

2. Refactor Source Imports

You must perform a global find-and-replace across your codebase. The logic is consistent: replace javax. with jakarta. for EE specifications.

Common Replacements:

  • javax.servlet -> jakarta.servlet
  • javax.persistence -> jakarta.persistence
  • javax.validation -> jakarta.validation
  • javax.annotation -> jakarta.annotation
  • javax.transaction -> jakarta.transaction

Example: Migrating a JPA Entity

Legacy Code (Spring Boot 2 / Java EE 8):

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.validation.constraints.NotNull;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    private String username;
    
    // Getters and Setters
}

Modern Code (Spring Boot 3 / Jakarta EE 10):

package com.example.demo.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import java.io.Serializable;

@Entity
@Table(name = "users")
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    private String username;

    // Standard constructor
    public User() {}

    public User(String username) {
        this.username = username;
    }

    // Getters
    public Long getId() { return id; }
    public String getUsername() { return username; }
}

3. Handling Third-Party Dependencies

This is the most frequent source of lingering NoClassDefFoundError exceptions. If you use libraries like Swagger/OpenAPI, Hibernate Types, or specialized JSON mappers, you must upgrade them to versions that support Jakarta EE.

If a library does not yet support Jakarta, you may see errors even after fixing your own code.

Key Dependency Upgrades:

  1. Hibernate: Spring Boot 3 pulls in Hibernate 6 automatically. Ensure you are not manually forcing an older Hibernate version in your POM.
  2. Lombok: Upgrade to 1.18.30 or higher.
  3. Jackson: Spring Boot 3 manages this, but if you manually import jackson-jaxrs-json-provider, ensure you switch to jackson-jakarta-rs-json-provider.

Example: Dependency Override in Maven If you rely on a library that hasn't migrated (e.g., an old utility jar), you might need to use the Eclipse Transformer build plugin to rewrite the bytecode at build time, though upgrading the library is always preferred.

Why This Fix Works

When you change the import from javax.persistence.Entity to jakarta.persistence.Entity, the compiler looks for the jakarta.persistence-api JAR (transitively provided by spring-boot-starter-data-jpa).

At runtime, Spring Boot initializes the Hibernate 6 EntityManagerFactory. Hibernate 6 is hard-coded to scan for classes annotated with jakarta.persistence.Entity. By aligning your imports, the annotations on your class match the annotations the framework expects.

Similarly, the embedded Tomcat 10 server uses the Servlet 6.0 specification. It expects filters and servlets to implement interfaces from the jakarta.servlet package. By updating your imports, your code implements the correct interface versions loaded by the container's ClassLoader.

Conclusion

The NoClassDefFoundError: javax/servlet is the defining barrier of the Spring Boot 3 upgrade. It is not a configuration error but a structural requirement of the Jakarta EE platform. By upgrading to Java 17, replacing the javax namespace with jakarta, and rigorously auditing third-party dependencies for Jakarta compatibility, you ensure your application runs on the modern, supported standard for Enterprise Java.