The upgrade to Spring Boot 3.0 represents the most significant breaking change in the Java ecosystem in the last decade. You change the version in your pom.xml, ensure your environment is running Java 17, and execute a build. The result is often a catastrophic wall of red text in your console, predominantly screaming one error:
package javax.servlet does not exist
This isn't just a deprecated method warning; it is a fundamental breakage of the API contract that underpins your web container, persistence layer, and dependency injection. Here is how to navigate the javax to jakarta transition with engineering rigor.
The Root Cause: A Legal Split, Not a Technical One
To fix the issue, you must understand that the bytecode signature of your application's core dependencies has changed.
- The Transfer: In 2017, Oracle donated Java EE (Enterprise Edition) to the Eclipse Foundation.
- The Trademark: While Oracle donated the source code, they retained the copyright to the
javax.*package namespace. - The Consequence: The Eclipse Foundation renamed the specifications to Jakarta EE. While Jakarta EE 8 maintained backward compatibility using
javaxpackages, Jakarta EE 9 broke that compatibility to completely migrate to thejakarta.*namespace.
Spring Boot 3.0 is built on Spring Framework 6, which is strictly based on Jakarta EE 10 (Servlet 6.0, JPA 3.1). Consequently, Spring Boot 3 no longer supports the javax namespace for EE APIs.
If your code tries to compile import javax.persistence.Entity; against Spring Boot 3 libraries, the compiler will fail because that class no longer exists in the classpath provided by Hibernate 6 or Tomcat 10.
The Fix: A Systematic Migration Strategy
Migrating requires three distinct phases: Environment preparation, automated refactoring, and dependency mitigation.
Phase 1: The BOM and Java Version Upgrade
Spring Boot 3 requires Java 17 as a baseline. Update your pom.xml or build.gradle to reflect the new parent and Java version.
Maven (pom.xml):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version> <!-- Use latest stable 3.x -->
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
</properties>
Phase 2: The Namespace Migration (Automated)
Manually finding and replacing imports across thousands of files is error-prone. The rigorous approach is using OpenRewrite, an automated refactoring ecosystem that modifies the Abstract Syntax Tree (AST) of your code, preserving formatting while safely swapping namespaces.
Add the OpenRewrite Maven plugin to your build configuration solely for the migration.
Execute via Command Line (Recommended):
You do not need to modify your POM for this; you can run the recipe directly.
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:5.0.0 \
-Drewrite.activeRecipes=org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0
Or Configure via pom.xml:
<plugin>
<groupId>org.openrewrite.maven</groupId>
<artifactId>rewrite-maven-plugin</artifactId>
<version>5.10.0</version>
<configuration>
<activeRecipes>
<recipe>org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0</recipe>
</activeRecipes>
</configuration>
<dependencies>
<dependency>
<groupId>org.openrewrite.recipe</groupId>
<artifactId>rewrite-spring</artifactId>
<version>5.0.0</version>
</dependency>
</dependencies>
</plugin>
Run mvn rewrite:run. This recipe performs a deep search for javax. imports related to Jakarta EE specifications (Servlet, JPA, Validation, Mail) and rewrites them to jakarta..
Phase 3: Manual Code Verification
If you cannot use OpenRewrite, or need to verify the changes, here are the specific mappings you must apply manually.
Persistence (JPA / Hibernate 6):
// BEFORE (Spring Boot 2.7)
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
// AFTER (Spring Boot 3.0)
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
}
Web Layer (Servlet API):
// BEFORE
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.Filter;
// AFTER
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.Filter;
Validation (Hibernate Validator):
// BEFORE
import javax.validation.constraints.NotNull;
// AFTER
import jakarta.validation.constraints.NotNull;
Phase 4: Third-Party Dependency Hell
This is where builds usually fail after the code fixes. Many third-party libraries rely on javax. If a library has not released a Jakarta-compatible version, you have two options.
Option A: Upgrade the Library Check Maven Central. Most major libraries have released updates.
- Hibernate: Upgrade to 6.x (Managed automatically by Spring Boot 3 starter).
- Jackson: Generally compatible, but ensure you are on 2.15+.
- Lombok: Must be upgraded to at least 1.18.26.
Option B: The Eclipse Transformer (Last Resort) If you depend on an abandoned library (e.g., an old SOAP client) that strictly imports javax.servlet, you must transform the bytecode at build time.
Add the Eclipse Transformer plugin to shade the legacy jar and rewrite the bytecode during the packaging phase.
<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>transform-jakarta</id>
<goals>
<goal>jar</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
Why This Fix Works
The core issue is Type Erasure and ClassLoader definitions. In the JVM, javax.servlet.Servlet and jakarta.servlet.Servlet are two completely distinct types. They share the same method signatures, but they are not assignment-compatible.
When you run Tomcat 10 (embedded in Spring Boot 3), the container expects objects implementing jakarta.servlet.Servlet. If your code or your dependencies provide a javax.servlet.Servlet, the ClassLoader cannot bridge them.
By updating the imports, you align your source code with the binaries provided by the Jakarta EE 10 providers (Hibernate 6, Tomcat 10, Jersey 3). By using OpenRewrite, you ensure that this transition catches deep references, such as those in web.xml (if present) or string literals in reflection logic.
Conclusion
The transition from javax to jakarta is painful but it solves a massive legal impasse in the Java community. By moving to the jakarta namespace, the ecosystem is finally free to evolve rapidly under the open governance of the Eclipse Foundation.
Treat this migration not as a patch, but as a modernization of your codebase's foundation. Once you clear the javax hurdle, you unlock the performance benefits of Spring Boot 3, the AOT compilation capabilities, and the robust features of Java 17+.