Have you ever tried upgrading a Spring Boot 2.x project to 3.x, only to be overwhelmed by a flood of compile errors?

OSS support for Spring Boot 2.7.x ended at the close of 2023, and security patches have stopped. Migration is inevitable — but 3.x introduces many breaking changes, and without knowing the right steps, progress can grind to a halt quickly.

This article focuses on three key changes: javax→jakarta replacement, migration to SecurityFilterChain, and spring.factories removal — and how to handle each one. Feel free to use the checklist at the end as a working sheet.

What Changed in Spring Boot 3.x

There are three main breaking changes:

  • javax→jakarta package rename (transition from Java EE to Jakarta EE 9)
  • WebSecurityConfigurerAdapter removed (forced migration to SecurityFilterChain)
  • spring.factories removed (new AutoConfiguration registration mechanism)

In addition, Spring Boot 3.x requires Java 17 or later. Staying on Java 8 or 11 means the application won’t even start.

Whether to migrate all at once or incrementally depends on your test coverage and project size. For large projects with thin test coverage, it’s safer to carry out the migration on a feature branch and merge into main gradually.

Preparation Before Migration

Start by updating the Spring Boot version and Java version in build.gradle.

plugins {
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.4'
    id 'java'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

After making the change, run ./gradlew dependencies to check for any incompatible third-party libraries. Older versions of libraries like mapstruct, querydsl, and springfox often lack Jakarta EE support, so pay close attention to these.

javax → jakarta Package Rename

Since Spring Boot 3.x is based on Jakarta EE 9, you need to replace all javax.* imports with jakarta.*. The main targets are:

  • javax.servlet.*jakarta.servlet.*
  • javax.persistence.*jakarta.persistence.*
  • javax.validation.*jakarta.validation.*
  • javax.annotation.*jakarta.annotation.*

Important: javax.sql.*, javax.crypto.*, and javax.net.* are not part of Jakarta EE and should NOT be replaced. Replacing them will cause compile errors.

Bulk Replacement via Command Line

find src -name "*.java" | xargs sed -i \
  -e 's/javax\.servlet/jakarta.servlet/g' \
  -e 's/javax\.persistence/jakarta.persistence/g' \
  -e 's/javax\.validation/jakarta.validation/g' \
  -e 's/javax\.annotation/jakarta.annotation/g'

If you’re using IntelliJ IDEA, it’s easier to use “Edit > Find > Replace in Files” with the regex javax\.(servlet|persistence|validation|annotation) replaced by jakarta.$1. After replacing, always run a build to catch any missed occurrences.

Migration to SecurityFilterChain

Spring Security 6.x completely removes WebSecurityConfigurerAdapter. Building 2.x code as-is will result in compile errors.

Before (2.x)

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            .and()
            .formLogin();
    }
}

After (3.x)

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());
        return http.build();
    }
}

Here’s a summary of the changes:

  • Remove extends WebSecurityConfigurerAdapter and convert to a @Bean method
  • authorizeRequests()authorizeHttpRequests()
  • antMatchers()requestMatchers()
  • Lambda DSL is now standard; .and() chaining is no longer needed

UserDetailsService and PasswordEncoder can still be defined as @Bean the same way as before. For Basic authentication implementation patterns, see Spring Boot Security Basic Authentication; for JWT authentication, see JWT Authentication Implementation.

spring.factories Removal

If you’ve created custom AutoConfigurations or starters, the registration mechanism has changed.

Before (spring.factories)

# META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.example.MyAutoConfiguration

After (.imports file)

# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.MyAutoConfiguration

The new file path is META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, and you simply list one class name per line. For details on how AutoConfiguration works, see How Spring Boot AutoConfiguration Works.

Java 17 Compatibility Checklist

Java 17 strengthens encapsulation via the module system, which can break libraries that accessed internal classes via reflection. If you’re using Spring Boot 3.x-compatible dependency versions, --add-opens is largely unnecessary — so remove any old JVM options and verify behavior. Any code that directly uses sun.* packages will fail to compile and must be replaced with standard APIs.

Verifying Behavior After Migration

Once migration is complete, verify in the following order:

  1. Run ./gradlew build to confirm compilation and tests pass
  2. Start the application and confirm actuator/health returns UP
  3. Run smoke tests against key endpoints
  4. Review Security configuration using MockMvc-based tests

@SpringBootTest and @WebMvcTest will generally work as before, but make sure to review authentication setup in your tests to align with the updated Security configuration.

Migration Checklist

Preparation

  • Confirm migration to Java 17 or later
  • Update Spring Boot version to 3.x in build.gradle
  • Verify dependency compatibility with ./gradlew dependencies

javax→jakarta Replacement

  • Replace javax.servlet, javax.persistence, javax.validation, javax.annotation
  • Confirm javax.sql, javax.crypto were NOT replaced (no replacement needed)
  • Confirm build succeeds

Spring Security Migration

  • Replace all WebSecurityConfigurerAdapter inheritance with SecurityFilterChain @Bean
  • Update authorizeRequests()authorizeHttpRequests() and antMatchers()requestMatchers()

spring.factories Removal

  • Migrate EnableAutoConfiguration entries from META-INF/spring.factories to the .imports file

Verification

  • Build succeeds and tests pass
  • Application starts and actuator/health returns UP

Conclusion

The Spring Boot 3.x migration may look like a lot, but if you address the three key areas — javax→jakarta, SecurityFilterChain, and spring.factories — the vast majority of errors will be resolved. Work through each item on a migration branch one at a time, and you’ll likely finish faster than expected.

For managing environment-specific configuration, check out Using Spring Boot Profiles as well.