Skip to main content

Spring Boot 3 Migration: Resolving javax to jakarta ClassNotFoundExceptions

The Crash

You have just bumped your version numbers in pom.xml or build.gradle to upgrade from Spring Boot 2.7 to Spring Boot 3.0+. You compile successfully, but upon runtime startup or the first HTTP request, the application crashes with a stack trace resembling this:

java.lang.NoClassDefFoundError: javax/servlet/http/HttpServletRequest
    at com.example.config.SecurityConfig.configure(SecurityConfig.java:24)
    at ...
Caused by: java.lang.ClassNotFoundException: javax.servlet.http.HttpServletRequest
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
    ...

Or perhaps an error related to JPA:

java.lang.NoClassDefFoundError: javax/persistence/Entity

This is the single most common blocking issue in Spring Boot 3 migrations. It is not a bug in your code logic; it is a binary incompatibility caused by a massive ecosystem shift.

The Root Cause: The Great Renaming

Spring Boot 3.0 is built on Spring Framework 6, which mandates Jakarta EE 9 (specifically Jakarta EE 10 APIs).

When Oracle donated Java EE to the Eclipse Foundation, they retained the trademark on the javax.* namespace. Consequently, the Eclipse Foundation was legally required to rename all Jakarta EE specifications to use the jakarta.* namespace.

  • Spring Boot 2.x relies on Java EE 8 (Servlet 4.0, JPA 2.2), which uses javax.*.
  • Spring Boot 3.x relies on Jakarta EE 10 (Servlet 6.0, JPA 3.1), which uses jakarta.*.

The bytecode in your compiled application (and your dependencies) looks for classes like javax.servlet.http.HttpServletRequest. However, the Tomcat 10 (or Jetty 11) embedded container provided by Spring Boot 3 only provides jakarta.servlet.http.HttpServletRequest. The classloaders cannot find the legacy javax packages, resulting in NoClassDefFoundError.

The Fix: Comprehensive Migration Strategy

Resolving this requires a three-step approach: Environment upgrade, Dependency resolution, and Code refactoring.

1. Enforce Java 17

Spring Boot 3 requires Java 17 as a baseline. Before touching namespaces, ensure your build environment matches.

Maven (pom.xml):

<properties>
    <java.version>17</java.version>
    <spring-boot.version>3.2.0</spring-boot.version>
</properties>

2. The Namespace Refactor

You must replace all imports in your codebase. A simple "Find and Replace" is usually sufficient for application code, but it must be exhaustive.

Servlet Imports

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;

JPA / Persistence Imports

Before:

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

After:

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

Validation Imports

Before:

import javax.validation.constraints.NotNull;
import javax.validation.Valid;

After:

import jakarta.validation.constraints.NotNull;
import jakarta.validation.Valid;

3. Dependency Management (The Hidden Trap)

Changing your code is the easy part. The hard part is third-party libraries. If your pom.xml includes a library (e.g., an older version of Swagger, a custom internal library, or an outdated JSON mapper) that depends on javax.*, your application will still crash.

You must upgrade key dependencies to their Jakarta-compatible versions. Common upgrades include:

  • Hibernate: Upgrade to 6.x (Spring Boot 3 manages this automatically via the starter).
  • Jackson: Generally compatible, but ensure you are using standard starters.
  • Lombok: Upgrade to 1.18.24+.
  • Swagger / OpenAPI: Migrate from springfox (dead project) to springdoc-openapi-starter-webmvc-ui (v2.0+).

Maven Dependency Example:

<dependencies>
    <!-- Use Spring Boot Starters to guarantee Jakarta-compatible versions -->
    <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>
    
    <!-- DO NOT manually enforce old hibernate versions -->
    
    <!-- Replace Springfox with SpringDoc v2 for Jakarta support -->
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>2.2.0</version>
    </dependency>
</dependencies>

Automated Migration with OpenRewrite

For large codebases, manual replacement is error-prone. Use OpenRewrite to automate the namespace migration and dependency bumps.

Run the following Maven command to apply the Spring Boot 3 migration recipe:

mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
  -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:RELEASE \
  -Drewrite.activeRecipes=org.openrewrite.java.spring.boot3.SpringBoot3BestPractices

Why This Fix Works

  1. Bytecode Compatibility: By changing the source imports to jakarta.* and recompiling with JDK 17, the resulting .class files now contain constant pool references to jakarta/servlet/... instead of javax/servlet/....
  2. Container Alignment: Spring Boot 3 embeds Tomcat 10 (or Jetty 11 / Undertow 2.3). These containers implement the Jakarta EE specifications. When the container loads your servlet or filter, the signatures now match.
  3. Dependency Alignment: By upgrading dependencies (like Hibernate 6), you ensure that the ORM provider is also looking for jakarta.persistence.Entity annotations rather than ignoring your entities or throwing loader errors.

Conclusion

The shift from javax to jakarta is a one-time, painful cost of modernizing the Java ecosystem. It is not backward compatible. To survive the Spring Boot 3 upgrade, you must rigorously update your build target to Java 17, perform a global namespace replacement, and—most critically—audit your third-party dependencies to ensure they have also migrated to Jakarta EE artifacts. Failure to upgrade a transitive dependency is the most common reason the ClassNotFoundException persists after code refactoring.

Popular posts from this blog

Restricting Jetpack Compose TextField to Numeric Input Only

Jetpack Compose has revolutionized Android development with its declarative approach, enabling developers to build modern, responsive UIs more efficiently. Among the many components provided by Compose, TextField is a critical building block for user input. However, ensuring that a TextField accepts only numeric input can pose challenges, especially when considering edge cases like empty fields, invalid characters, or localization nuances. In this blog post, we'll explore how to restrict a Jetpack Compose TextField to numeric input only, discussing both basic and advanced implementations. Why Restricting Input Matters Restricting user input to numeric values is a common requirement in apps dealing with forms, payment entries, age verifications, or any data where only numbers are valid. Properly validating input at the UI level enhances user experience, reduces backend validation overhead, and minimizes errors during data processing. Compose provides the flexibility to implement ...

jetpack compose - TextField remove underline

Compose TextField Remove Underline The TextField is the text input widget of android jetpack compose library. TextField is an equivalent widget of the android view system’s EditText widget. TextField is used to enter and modify text. The following jetpack compose tutorial will demonstrate to us how we can remove (actually hide) the underline from a TextField widget in an android application. We have to apply a simple trick to remove (hide) the underline from the TextField. The TextField constructor’s ‘colors’ argument allows us to set or change colors for TextField’s various components such as text color, cursor color, label color, error color, background color, focused and unfocused indicator color, etc. Jetpack developers can pass a TextFieldDefaults.textFieldColors() function with arguments value for the TextField ‘colors’ argument. There are many arguments for this ‘TextFieldDefaults.textFieldColors()’function such as textColor, disabledTextColor, backgroundColor, cursorC...

jetpack compose - Image clickable

Compose Image Clickable The Image widget allows android developers to display an image object to the app user interface using the jetpack compose library. Android app developers can show image objects to the Image widget from various sources such as painter resources, vector resources, bitmap, etc. Image is a very essential component of the jetpack compose library. Android app developers can change many properties of an Image widget by its modifiers such as size, shape, etc. We also can specify the Image object scaling algorithm, content description, etc. But how can we set a click event to an Image widget in a jetpack compose application? There is no built-in property/parameter/argument to set up an onClick event directly to the Image widget. This android application development tutorial will demonstrate to us how we can add a click event to the Image widget and make it clickable. Click event of a widget allow app users to execute a task such as showing a toast message by cli...