Back to blog

// OSSeva Blog

Migration

Spring Boot 3 Migration Guide: From Spring Boot 2.7 to 3.x, and On to 4.0

16 min read

The short answer

A Spring Boot 2 to 3 migration has four parts that cannot be skipped. You move to Java 17, rename the Java EE packages from javax to jakarta, rewrite security configuration for Spring Security 6, and retest persistence on Hibernate 6. Everything else is a list of renamed properties and changed defaults. Spring's own advice is to start from the latest 2.7.x release and to move to Spring Security 5.8 before you touch Spring Boot 3. Once on 3.x, the jump from 3.5 to 4.0 is smaller but has its own breaking changes: Jakarta EE 11, Spring Framework 7, Jackson 3 and modular starters.

The dates that set the deadline

These are the support dates spring.io publishes for each Spring Boot line. Open source support means free releases with security fixes. Commercial support is Broadcom's paid Tanzu Spring offering.

Spring Boot lineOpen source support ended or endsCommercial support ends
2.7.x30 June 2023 (last open source release 2.7.18)30 June 2029
3.5.x30 June 202630 June 2032
4.0.x31 December 202631 December 2027
4.1.x31 July 202731 July 2028

The practical reading: any application still on 2.7 has gone more than three years without free security fixes, and an application that finished its Spring Boot 3 migration on 3.5 is already past open source support again. Plan the 3.x work with 4.x in view.

Before you change a version number

  1. Upgrade to the latest 2.7.x. The Spring Boot 3.0 migration guide asks for this first, so that you build against the newest dependencies of the 2.x line. Spring Boot 2.7.18 requires Java 8 and is compatible up to and including Java 21.
  2. Clear deprecation warnings. Classes, methods and properties deprecated in Spring Boot 2.x were removed in 3.0. A clean 2.7 build with no deprecation warnings is the best predictor of a short migration.
  3. Move to Spring Security 5.8. The Spring Security team released 5.8 to make the jump to 6.0 smaller. Most of the 6.0 changes can be adopted on 5.8 and tested while the rest of the stack is still on Boot 2.
  4. List the dependencies Spring Boot does not manage. Spring Cloud is the usual one. Each release train supports specific Boot lines: 2022.0 (Kilburn) for Boot 3.0 and 3.1, 2025.0 (Northfields) for 3.5, and 2025.1 (Oakwood) for 4.0. Find the matching version of every unmanaged library before the upgrade, not during it.

Step 1: Java 17 as the baseline

Spring Boot 3.0 requires Java 17 or later and no longer supports Java 8. Spring Boot 4.0 keeps the same minimum and encourages the latest LTS release. Because 2.7.18 already runs on Java 17 and 21, the lowest-risk order is to move the JVM first, on Spring Boot 2.7, and ship that to production before changing the framework. JVM problems and framework problems then show up in separate releases.

The failures on Java 17 come from the build more than from your code. JDK 17 strongly encapsulates JDK internals, so libraries that reached into them through reflection fail at runtime unless upgraded. Annotation processors and bytecode tools (Lombok, Mockito and its ByteBuddy dependency, code coverage agents, older Maven and Gradle plugins) need versions that support Java 17. Upgrade those first and the rest of the codebase usually compiles.

Step 2: javax to jakarta

Spring Boot 3.0 moved every Jakarta EE dependency to the version in Jakarta EE 10, for example Servlet 6.0 and JPA 3.1. The packages move with it: javax.servlet becomes jakarta.servlet, javax.persistence becomes jakarta.persistence, and the same for validation, annotations, transactions, JAX-RS, JAXB, mail and JMS. Packages that belong to Java SE, such as javax.sql and javax.crypto, do not move, which is why a blind find-and-replace breaks the build.

The rename itself is mechanical. The work is the dependency tree. The migration guide warns that older Java EE artifacts must not remain on the classpath directly or transitively, so jakarta.servlet:jakarta.servlet-api replaces javax.servlet:javax.servlet-api everywhere, and every library that touches a servlet, an entity or a validator needs a Jakarta build. A WAR deployed to an external Tomcat 9 needs Tomcat 10.1 or a Jakarta EE 10 server. Our javax to jakarta migration guide covers the package list, the tools and the dependency audit in detail.

Step 3: Spring Security 6 configuration

Security configuration is the part most likely to compile and still behave differently. Spring Security 6.0 made these changes, all of which can be adopted on 5.8 first:

  • WebSecurityConfigurerAdapter is removed. Publish a SecurityFilterChain bean for HTTP rules, a WebSecurityCustomizer bean for ignored paths, and an AuthenticationManager bean where you configured one through the adapter.
  • antMatchers, mvcMatchers and regexMatchers are removed. Use requestMatchers, which picks an MVC matcher when Spring MVC is on the classpath and an Ant matcher otherwise.
  • authorizeRequests gives way to authorizeHttpRequests, which uses AuthorizationManager instead of access decision voters. Custom AccessDecisionVoter and AccessDecisionManager classes need rewriting.
  • @EnableGlobalMethodSecurity becomes @EnableMethodSecurity. @Configuration is no longer part of the @Enable* annotations, so add it to each class that uses them.
  • Authorization applies to every dispatch type. Error pages and forwards are now checked too, which can turn a working error page into a 403. Spring Boot 3 sets the filter for every dispatch type, configurable with spring.security.filter.dispatcher-types.
  • The security context must be saved explicitly. SecurityContextHolderFilter only reads the context; custom authentication code that relied on automatic saving must call the SecurityContextRepository itself.
  • CSRF tokens are loaded lazily and protected against BREACH by default. Single-page applications that read the token from a cookie often need CsrfTokenRequestAttributeHandler configured to keep working.

A typical Boot 2 configuration and its Boot 3 form:

// Spring Boot 2.x
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/actuator/health").permitAll()
        .anyRequest().authenticated()
        .and().httpBasic();
  }
}

// Spring Boot 3.x
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
  @Bean
  SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
            .requestMatchers("/actuator/health").permitAll()
            .anyRequest().authenticated())
        .httpBasic(Customizer.withDefaults());
    return http.build();
  }
}

Step 4: Hibernate 6

Spring Boot 3.0 uses Hibernate 6.1, under the new org.hibernate.orm group ID. Hibernate 6 rewrote the query engine and type system, so the persistence layer needs its own test pass even when it compiles. The changes that break running applications most often:

  • Sequence naming. Hibernate 6 creates a sequence per entity hierarchy, named <entity>_seq, instead of a single hibernate_sequence. Entities using @GeneratedValue with the default AUTO strategy expect those sequences to exist. Setting hibernate.id.db_structure_naming_strategy=legacy keeps the old behaviour while you migrate.
  • Allocation size. Implicit sequences now follow the JPA default allocation size of 50. Seed data loaded with import.sql has to reset each sequence to a value that clears the existing IDs.
  • The old ID switch is gone. spring.jpa.hibernate.use-new-id-generator-mappings was removed because Hibernate no longer supports the old generator mappings.
  • Type mapping. UUID maps to a native uuid column where the database has one, which can fail schema validation on existing binary columns (hibernate.type.preferred_uuid_jdbc_type=BINARY restores the old mapping). Native queries returning BIGINT, including count(), now return Long instead of BigInteger.
  • Queries. The legacy Hibernate Criteria API is removed, so those queries move to JPA Criteria. DISTINCT is no longer needed to remove duplicate parents from a fetch join and is always passed through to SQL.

Step 5: Observability and Micrometer

Spring Boot 3.0 builds on Micrometer 1.10 and its Observation API, and this changes how metrics are produced. The Boot 2 instrumentation filters were removed: WebMvcMetricsFilter is replaced by Spring Framework's ServerHttpObservationFilter, and MetricsRestTemplateCustomizer by ObservationRestTemplateCustomizer. Custom *TagsProvider and *TagsContributor beans are deprecated and no longer used by default; their logic moves into observation conventions. Check dashboards and alerts after the upgrade, because metric tags can change.

Tracing moves too. Spring Cloud Sleuth's last minor version is 3.1, and its core moved into Micrometer Tracing, so Boot 3 applications use Micrometer Tracing with a Brave or OpenTelemetry bridge. Smaller actuator changes catch teams out as well:

  • Metrics export properties moved from management.metrics.export.<product> to management.<product>.metrics.export, for example management.prometheus.metrics.export.
  • The httptrace endpoint is renamed httpexchanges, and HttpTraceRepository becomes HttpExchangeRepository.
  • The /env and /configprops endpoints mask every value by default; management.endpoint.env.show-values controls it.
  • Only the health endpoint is exposed over JMX by default.

Step 6: Let the properties migrator find renamed properties

Several configuration properties were renamed or removed in 3.0. Spring Boot ships a module that reports them at startup and temporarily maps the old names to the new ones while the application runs:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-properties-migrator</artifactId>
  <scope>runtime</scope>
</dependency>

With Gradle, use runtimeOnly("org.springframework.boot:spring-boot-properties-migrator"). Common findings include spring.redis.* moving to spring.data.redis.*, Cassandra moving from spring.data.cassandra.* to spring.cassandra.*, server.max-http-header-size becoming server.max-http-request-header-size, and SAML2 identity-provider properties becoming asserting-party. Fix each one in your configuration, then remove the module. It is a migration aid, not something to ship.

Other changes that only show up at runtime

  • Trailing slashes. Spring Framework 6 no longer matches /greeting/ to a /greeting mapping by default, so those requests return 404.
  • Auto-configuration registration. Registering auto-configurations in spring.factories under EnableAutoConfiguration is no longer supported. Internal starters must list them in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.
  • HTTP clients. Spring Framework 6 dropped Apache HttpClient 4 support for RestTemplate in favour of httpclient5. Without it, RestTemplate can quietly fall back to the JDK client.
  • Logging. The default log date format is now ISO-8601 with a T separator and a time zone offset, which can break log parsers.
  • Coordinates and removals. The MySQL driver is now com.mysql:mysql-connector-j, the Elasticsearch high-level REST client is gone, and support for Apache ActiveMQ, Atomikos, EhCache 2 and Hazelcast 3 was removed.
  • Spring Batch 5. @EnableBatchProcessing now tells Boot's auto-configuration to back off, and running several jobs at startup needs spring.batch.job.name.

Step 7: Automate with OpenRewrite

OpenRewrite recipes do much of the mechanical work. org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0 chains the Spring Boot 2.7 upgrade, the Java 17 upgrade, the Spring Framework 6.0 and Spring Security 6.0 migrations, the Hibernate 6.1 migration, the Spring Batch 4 to 5 migration, the Spring Cloud 2022 upgrade and the Spring Boot 3.0 property renames. Later recipes build on it:

RecipeWhat it does
org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_02.7 to 3.0, including Java 17, jakarta, Security 6 and Hibernate 6.1
org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_5Up to 3.5, chaining the 3.x recipes before it
org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_03.5 to 4.0, chaining the 3.5 recipe first
org.openrewrite.java.spring.security6.UpgradeSpringSecurity_6_0The security changes on their own
org.openrewrite.java.migrate.UpgradeToJava17The Java 17 move on its own, for the first step

Run a recipe on a branch and review the diff like any other pull request. It will not decide how custom access decision voters should become authorization managers, or whether your sequences match Hibernate 6. Check the licence before adopting it in a commercial build: the OpenRewrite documentation lists these Spring recipes under the Moderne Source Available License, with precompiled artifacts for Moderne customers and source builds for non-commercial use. The Spring Boot Migrator project and IntelliJ IDEA's migration support are the other tools the Spring guide names.

Testing and validation

  1. Compile every module against Spring Boot 3 and fix compilation errors before running anything.
  2. Run unit tests and fix behaviour changes, not just renamed APIs.
  3. Run integration tests against a real application context with the properties migrator in place. Many auto-configuration changes only surface at runtime.
  4. Test authentication flows, error pages, transactions and ID generation end to end in a staging environment. These are where Security 6 and Hibernate 6 differ without failing a build.
  5. Compare metrics and traces with production before cutting over, so dashboards and alerts survive the Micrometer change.

From 3.0 to 3.5

Once an application runs on 3.0, each later minor release is a smaller step, but each one can remove APIs deprecated in earlier 3.x releases. Moving one minor at a time and reading each release's notes keeps those removals visible. The UpgradeSpringBoot_3_5 recipe applies the intermediate recipes in order if you want to go straight there. 3.5 is the last 3.x line, and its open source support ended on 30 June 2026.

Spring Boot 3.5 to 4.0

The Spring Boot 4.0 migration guide follows the same pattern: upgrade to the latest 3.5.x first, clear deprecations because they are removed in 4.0, then move. The main changes:

  • Java 17 is still the minimum. Kotlin applications need Kotlin 2.2 or later, and GraalVM native images need GraalVM 25 or later.
  • Jakarta EE 11 and Servlet 6.1. Spring Boot 4 requires Spring Framework 7. Undertow does not support Servlet 6.1 yet, so the Undertow starter is gone.
  • Modular starters. Spring Boot is now split into smaller modules, and technologies that previously worked with only a third-party jar now need a starter, for example spring-boot-starter-flyway or spring-boot-starter-liquibase. spring-boot-starter-classic brings everything back while you sort out the dependencies.
  • Jackson 3. Jackson 3 uses new group IDs and the tools.jackson package, except for annotations. Jackson2ObjectMapperBuilderCustomizer becomes JsonMapperBuilderCustomizer, and a deprecated spring-boot-jackson2 module exists as a stop-gap.
  • The properties migrator applies again. Add it for the 4.0 upgrade and remove it afterwards, as in step 6.

Open source support for 4.0 ends on 31 December 2026, so a team starting the move now should target 4.1, supported to 31 July 2027.

When the migration outlasts the support window

Large estates rarely move every service before the date. OSSeva's Spring continuation support patches Spring Boot 2.7 and the 3.x lines including 3.5, Spring Framework 5.3 and Spring Security 5.8, so the services still waiting for their migration slot get security fixes while the work proceeds. The Spring Boot 2, Spring Boot 3.5 and Spring Boot 4.0 end-of-life pages set out the dates, and the Spring Boot extended support page lists coverage by version.

Frequently asked questions

Does Spring Boot 3 require Java 17?

Yes. Spring Boot 3.0 requires Java 17 or later and dropped Java 8. Spring Boot 4.0 has the same minimum.

Can I go straight from Spring Boot 2.7 to 3.5?

Yes. OpenRewrite's UpgradeSpringBoot_3_5 recipe chains the 3.0 to 3.4 steps. Do the Java 17 and Spring Security 5.8 work on 2.7 first either way, so the framework upgrade carries less at once.

Should I upgrade to Spring Boot 3.5 or 4?

For a 2.7 application, 3.5 first. The Spring Boot 4.0 guide expects you to start from the latest 3.5.x with deprecations cleared. Plan for 4.1 straight after, since 3.5 is already past open source support.

What does spring-boot-properties-migrator do?

It reports configuration properties that were renamed or removed, and maps the old names to the new ones at runtime while you migrate. Remove it once your configuration uses the new names.

Does OpenRewrite do the whole migration?

No. It handles renames, dependency upgrades and common configuration patterns. Custom security voters, Hibernate sequence and type changes, observability conventions and anything that depends on runtime behaviour still need engineers and tests.

Is Spring Boot 2.7 still supported?

Not by the open source project. Free releases ended with 2.7.18, after open source support ended on 30 June 2023. Broadcom's commercial support runs to 30 June 2029, and third-party extended support such as OSSeva's also covers it.

Tags

Spring BootJavaJakarta EEMigrationSpring SecurityHibernateOpenRewrite

Ready to get your open source under control?

Talk to an OSSeva engineer about CVE coverage, compliance, and migration support for your stack.