Skip to main content

Fixing 'package javax.servlet does not exist' during Spring Boot 3 Migration

 One of the most jarring experiences in modern Java development is upgrading a legacy Spring Boot 2.x application to Spring Boot 3.0+ and immediately facing a compilation failure affecting nearly every file in your controller and persistence layers.

The error is explicit yet baffling to developers who haven't tracked the Jakarta EE governance changes:

[ERROR] /src/main/java/com/enterprise/app/config/SecurityFilter.java:[5,20] package javax.servlet does not exist
[ERROR] /src/main/java/com/enterprise/app/model/User.java:[3,24] package javax.persistence does not exist

This isn't a simple deprecated method warning. It is a hard break in binary compatibility. This post dissects why the javax namespace was effectively "deleted" from the modern ecosystem and details the precise steps required to migrate your codebase to the jakarta namespace.

The Root Cause: The Oracle vs. Eclipse Trademark Split

The migration from javax.* to jakarta.* is not a technical evolution; it is a legal one.

When Oracle donated Java EE to the Eclipse Foundation in 2017, the rights to the javax package namespace were not part of the transfer. Oracle retained the trademark on the name "Java." Consequently, the Eclipse Foundation was legally required to rename the Enterprise specifications (Servlet, JPA, JMS, etc.) to a new brand: Jakarta EE.

  • Java EE 8 (Spring Boot 2.x): Uses javax.servlet.*javax.persistence.*.
  • Jakarta EE 9/10 (Spring Boot 3.x): Uses jakarta.servlet.*jakarta.persistence.*.

Spring Boot 3 is based on Spring Framework 6, which sets the baseline at Jakarta EE 9 API compliance (specifically Servlet 6.0 and JPA 3.1). Any library relying on the old javax bytecode signatures will fail to load, and any source code importing javax will fail to compile.

The Solution: The Great Namespace Migration

To fix this, you must upgrade your dependencies and perform a global namespace replacement.

1. Upgrade the Build Artifacts

First, ensure your pom.xml (or build.gradle) targets the Spring Boot 3 baseline. This pulls in the Jakarta EE BOMs (Bill of Materials) and excludes the old Java EE dependencies.

Maven (pom.xml):

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.4</version> <!-- Use latest stable 3.x -->
    <relativePath/> 
</parent>

<properties>
    <java.version>17</java.version> <!-- Spring Boot 3 requires Java 17+ -->
</properties>

<dependencies>
    <!-- 
      Remove explicit dependencies on javax.servlet-api or javax.persistence-api.
      The starters below will pull in the jakarta.* equivalents automatically.
    -->
    <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>
</dependencies>

2. Refactoring Servlet Filters and Controllers

The most common source of the "does not exist" error is the Servlet API. You must replace all javax.servlet imports with jakarta.servlet.

Before (Spring Boot 2.x):

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
// ...

After (Spring Boot 3.x):

package com.enterprise.app.security;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class RequestLoggingFilter implements Filter {

    @Override
    public void doFilter(
            ServletRequest request, 
            ServletResponse response, 
            FilterChain chain
    ) throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // Logic remains exactly the same, only imports changed
        chain.doFilter(httpRequest, httpResponse);
    }
}

3. Refactoring Persistence (JPA/Hibernate)

Hibernate 6 is the default JPA provider in Spring Boot 3. It natively supports Jakarta Persistence.

Before:

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;

After:

package com.enterprise.app.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Table;

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

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

    // ... getters and setters
}

4. Handling Validation (Bean Validation)

If you use javax.validation (JSR-303/380), this also moves to the Jakarta namespace.

Dependency: Ensure you have the validation starter:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Code:

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public class UserDto {
    @NotNull
    @Size(min = 2, max = 50)
    private String username;
}

Bulk Refactoring Strategy

If you are working on a large monolith, doing this file-by-file is inefficient. You can use a regex-based find-and-replace across your entire source directory.

Unix/Linux/macOS (using sed):

# Recursively find java files and replace javax. with jakarta.
# WARNING: Verify changes afterwards, as not ALL javax packages moved (e.g., javax.sql, javax.crypto remain).
# The packages that moved are servlet, persistence, validation, annotation, transaction, xml.bind.

find src/main/java -name "*.java" -exec sed -i '' 's/javax\.servlet/jakarta.servlet/g' {} +
find src/main/java -name "*.java" -exec sed -i '' 's/javax\.persistence/jakarta.persistence/g' {} +
find src/main/java -name "*.java" -exec sed -i '' 's/javax\.validation/jakarta.validation/g' {} +
find src/main/java -name "*.java" -exec sed -i '' 's/javax\.annotation/jakarta.annotation/g' {} +
find src/main/java -name "*.java" -exec sed -i '' 's/javax\.transaction/jakarta.transaction/g' {} +

5. Third-Party Dependency Conflicts

This is the most dangerous part of the migration. If you use a third-party library (e.g., an older PDF generator, a specific payment SDK) that depends on javax.servlet internally, your application will compile but fail at runtime with java.lang.NoClassDefFoundError: javax/servlet/http/HttpServletRequest.

The Fix:

  1. Check for Updates: Check Maven Central for a version of the library compatible with Jakarta EE 9/10 (often denoted with a -jakarta suffix or a major version bump).
  2. Eclipse Transformer: If the vendor has not updated the library, you can use the Eclipse Transformer Maven Plugin to rewrite the library's bytecode at build time, converting javax references to jakarta.
<!-- Example of transforming a legacy dependency -->
<plugin>
    <groupId>org.eclipse.transformer</groupId>
    <artifactId>transformer-maven-plugin</artifactId>
    <version>0.5.0</version>
    <extensions>true</extensions>
    <configuration>
        <rules>
            <jakartaDefaults>true</jakartaDefaults>
        </rules>
    </configuration>
    <executions>
        <execution>
            <id>default-jar</id>
            <goals>
                <goal>jar</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Why this works

By explicitly importing jakarta.*, you are aligning your source code with the JAR files provided by Tomcat 10 (embedded in Spring Boot 3) and Hibernate 6.

Under the hood, the classloader for Tomcat 10 is looking for jakarta.servlet.Servlet. If your code compiles against javax.servlet.Servlet, the bytecode refers to a class path that no longer exists in the container's runtime environment. The rename ensures the contract between your code and the container is restored.

Conclusion

The javax to jakarta migration is a one-time tax required to unlock the features of Spring Boot 3, Java 17+, and the modern cloud-native ecosystem. While the compilation errors look intimidating initially, the fix is deterministic: upgrade the BOM, find-and-replace the imports, and ensure your third-party dependencies are Jakarta-compatible.