Skip to content
Ashish.
All posts
Diagram illustrating the migration from Java EE/Javax to Jakarta EE and Spring Boot 3 architecture.
6 min readDevelopmentJava DevelopersFeatured#spring boot#jakarta ee#java 17#migration#backend development#java ecosystem

Upgrading to Spring Boot 3: Jakarta, Java 17, and What Breaks

A practical guide to migrating to Spring Boot 3, covering the Jakarta EE namespace shift, Java 17 requirements, and common breaking changes.

By Ashish Kumar

Migrating to Spring Boot 3.0 is often mischaracterized as a simple dependency update. In reality, it is a fundamental restructuring of the Java backend ecosystem. The release introduces two non-negotiable prerequisites: Java 17 as the minimum runtime and the adoption of Jakarta EE 9+ namespaces. This shift severs the historical link between Java EE and the javax.* package structure, forcing developers to refactor code, dependencies, and configuration files. This guide details the mechanical steps required to navigate these changes without breaking production systems.

The Java 17 Baseline

Spring Boot 3.0 drops support for Java 8 and Java 11, setting Java 17 as the minimum version. This is not arbitrary; Spring Framework 6 leverages Java 17 language features to reduce boilerplate and improve type safety. A successful dependency upgrade to Java 17 is the first step in this process.

Mechanism: Bytecode Compatibility and Toolchain Configuration

When you compile Java code, the compiler produces bytecode compatible with specific JVM versions. By targeting Java 17, Spring Boot 3 ensures that all internal components utilize modern language features. For your project, this means the build tool must be explicitly configured to target Java 17. A dependency upgrade to Java 17 involves configuring the build toolchain to ensure consistent compilation across environments.

In Maven, this is handled via the maven-compiler-plugin or the spring-boot-maven-plugin:

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

In Gradle:

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

If your CI/CD pipeline runs on Java 8 or 11, the build will fail immediately. You must ensure the JDK used for compilation and execution is Java 17 or higher. This enables the use of Java 17 features in your own codebase, such as record classes for immutable data carriers and sealed classes for controlled inheritance hierarchies.

Technical diagram showing Java 17 baseline requirements. Visualize a server stack with Java 17 highlighted as the foundation layer, supporting Spring Boot 3 applications. Use a clean, architectural style with blue and green color palette.

The Jakarta EE Namespace Shift

The most significant breaking change is the migration from javax.* to jakarta.*. The governance of Java EE specifications transferred from Oracle to the Eclipse Foundation with Jakarta EE 8, which still utilized the javax.* namespace. The distinct namespace shift to jakarta.* occurred with the release of Jakarta EE 9 in 2020 to avoid confusion with the existing Java EE 8 platform. A dependency upgrade in this context is not just about version numbers but also about these fundamental namespace changes.

Mechanism: Binary Incompatibility

This is not a simple rename. The classes in javax.servlet and jakarta.servlet are distinct binaries. A dependency upgrade to Spring Boot 3 requires careful handling of binary incompatibilities because the expected package structure no longer exists on the classpath if you haven't migrated. If your application code imports javax.servlet.http.HttpServletRequest but your classpath contains Spring Boot 3 (which depends on Tomcat 10+ or Jetty 11+, both of which use jakarta.servlet), the JVM will throw a ClassNotFoundException or NoClassDefFoundError at runtime.

Affected Packages

You must refactor imports in your source code. Common shifts include:

Old Package (javax)New Package (jakarta)
javax.servlet.*jakarta.servlet.*
javax.persistence.*jakarta.persistence.*
javax.validation.*jakarta.validation.*
javax.annotation.*jakarta.annotation.*
javax.transaction.*jakarta.transaction.*

Automated Refactoring Strategy

Manually updating hundreds of imports is error-prone. Use your IDE's refactoring tool (IntelliJ IDEA or Eclipse) to "Rename" or "Replace" packages. Automated refactoring tools can assist with a dependency upgrade by identifying incompatible dependencies and suggesting replacements. Alternatively, use the Spring Boot Migration Tool if available in your build plugin.

For Maven, the spring-boot-maven-plugin can assist in identifying incompatible dependencies, but manual code refactoring is often required for custom business logic that directly references javax classes.

Breaking Changes in Spring Framework 6.x

Spring Boot 3 is built on Spring Framework 6.0, which introduces its own set of breaking changes beyond the Jakarta namespace.

Spring Security 6.x

Spring Security 6.0 aligns with the Jakarta migration and introduces stricter default behaviors. A dependency upgrade to Spring Security 6.x brings stricter default behaviors that require explicit configuration for many previously open endpoints. One major change is the removal of default security configurations that previously allowed unrestricted access to certain endpoints.

Example: WebSecurityConfigurerAdapter

In Spring Security 5, developers often extended WebSecurityConfigurerAdapter to configure security. This class is now deprecated and removed in Spring Security 6. The new mechanism requires using SecurityFilterChain beans.

Old Approach (Spring Boot 2.x):

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/public/**").permitAll()
            .anyRequest().authenticated();
    }
}

New Approach (Spring Boot 3.x):

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
            .requestMatchers("/public/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin();
        return http.build();
    }
}

This change enforces a more functional, bean-based configuration model, improving testability and reducing coupling.

Dependency Management and Testing

Maven/Gradle Dependency Updates

Ensure all third-party libraries are compatible with Jakarta EE 9+ and Java 17. Libraries that still depend on javax.* will cause conflicts. For example, older versions of Hibernate ORM may need upgrading to Hibernate 6.x, which uses jakarta.persistence. A successful dependency upgrade requires verifying that all third-party libraries are compatible with Jakarta EE 9+ and Java 17.

Maven Example:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <!-- Ensure this pulls Hibernate 6.x -->
</dependency>

Testing Implications

Your test suite must also run on Java 17. Additionally, if you use mock frameworks like Mockito, ensure they support Java 17's strong encapsulation features. A dependency upgrade of testing frameworks like Mockito is necessary for Java 17 compatibility, specifically requiring Mockito 5+.

Conclusion

Upgrading to Spring Boot 3 requires a systematic approach:

  1. Upgrade JDK: Set Java 17 as the minimum in your build configuration.
  2. Refactor Code: Replace all javax.* imports with jakarta.*.
  3. Update Dependencies: Ensure libraries like Hibernate, Spring Security, and testing frameworks are at versions compatible with Jakarta EE 9+ and Java 17.
  4. Refactor Security Config: Migrate from WebSecurityConfigurerAdapter to SecurityFilterChain beans.
  5. Run Tests: Validate that all tests pass under the new runtime and configuration models.

A comprehensive dependency upgrade is key to avoiding runtime errors during this migration. This process is painful but necessary. It aligns your application with the modern Java ecosystem, leveraging newer language features and standardized APIs that will be supported for years to come.

Related posts