DEV Community

Cover image for Spring Boot 3 And Java 17 Migration Guide
Henry Pham
Henry Pham

Posted on

Spring Boot 3 And Java 17 Migration Guide

Spring Boot 3.0 is a new major release that offers new features and improvements. However, it requires Java 17 as a minimum version and comes with numerous compatibility issues if you intend to upgrade.

I. Pros and Cons

You need to analyze the pros and cons of our migration. In my experience, there are some points you should review:

1.1. Pros

Java 17 Baseline

You’ll need to upgrade to JDK 17 before you can develop Spring Boot 3.0 applications. This means you can take advantage of the latest features and performance improvements that Java 17 offers.

GraalVM Native Image Support

GraalVM Native Images provide a new way to deploy and run Java applications. It provides various advantages, like an instant startup and reduced memory consumption (pain points of Spring Boot apps).

Improved observability with Micrometer and Micrometer Tracing

You can check more details here.

1.2. Cons

Time/Resouces constraints

Migrating to a new major release takes time and resources, especially for testing. This migration affects all your flows so needs to be tested carefully. While you can update your code within a few days, please plan for testing to span more than a week (the duration depends on the size of your project).

Risk of new bugs

As mentioned earlier, the migration affects all your flows. Therefore, if your test coverage doesn't cover all your code, please be careful. Test your end-to-end flows and scrutinize the logs for any new exceptions or discrepancies compared to the previous state.

II. Before we start

If you’re currently running with an earlier version of Spring Boot, I recommend that you upgrade to Spring Boot 2.7 before migrating to Spring Boot 3.0. It minimizes compatibility issues as much as possible.

Review Dependencies

You can review your dependencies and dependency management for 3.x to assess how your project is affected.

For dependencies that are not managed by Spring Boot, you can identify the compatible version before upgrading.

Review Deprecations

Classes, methods and properties that were deprecated in Spring Boot 2.x have been removed in this release. Before upgrading, please ensure that you are not calling any deprecated methods.

III. Migrate to Spring Boot 3 and Java 17

3.1. Spring Boot Template Project

I will use my project as an example for you guys to share how I migrated from Spring Boot 2.7 (Java 11) to Spring Boot 3 and Java 17.

Github: https://github.com/hieubz/spring-boot-template-project

This project includes the implementation of common backend features, designed to assist both myself and other Spring Boot developers in coding more efficiently. For further details, you can read more here.

If you find my project useful, please give it a star ⭐️!

3.2. Migration Steps

3.2.1. Configuration Properties Migration

Let's add the migrator by adding the following to your Maven pom.xml:

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

This will analyze your application’s environment and print diagnostics at startup console logs. Then you can based on that update your properties accordingly.

3.2.2. Update Dependencies

  • We start with the parent pom spring-boot-starter-parent and Java version
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <java.version>17</java.version>
    </properties>
Enter fullscreen mode Exit fullscreen mode

Tips: We shouldn't specify the version of Spring Data JPA, Spring Web, Spring Data Redis,... because their compatible versions are already declared in the parent POM.

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
Enter fullscreen mode Exit fullscreen mode
  • If you are working with MySQL, let's replace your mysql-connector-java by:
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.1.0</version>
        </dependency>
Enter fullscreen mode Exit fullscreen mode
  • If you are using logback for logging, let's update it to v1.4.11
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.4.11</version>
        </dependency>
Enter fullscreen mode Exit fullscreen mode
  • If you are using Openfeign for integrations, let's update it to v4.x
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>4.0.4</version>
        </dependency>
Enter fullscreen mode Exit fullscreen mode
  • If you are using Spring JDBC, let's update it to v6.x
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.0.12</version>
        </dependency>
Enter fullscreen mode Exit fullscreen mode
  • If you are using Jackson for data binding, let's update it to v2.15.x
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.15.3</version>
        </dependency>

        <!--  support Java 8 Date/time Serialize/Deserialize  -->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.15.3</version>
        </dependency>
Enter fullscreen mode Exit fullscreen mode
  • If you are using Redisson as a Redis client, let's update it to v3.24.x
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.24.3</version>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-data-31</artifactId>
            <version>3.24.3</version>
        </dependency>
Enter fullscreen mode Exit fullscreen mode
  • You should use the default spring-boot-starter-test for unit testing. However, If you are customizing mockito-core, let's update it to v5.3.x
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>5.3.1</version>
            <scope>test</scope>
        </dependency>
Enter fullscreen mode Exit fullscreen mode
  • If you are using jjwt for authentication, let's update it to 0.12.x
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.12.3</version>
        </dependency>
Enter fullscreen mode Exit fullscreen mode
  • If you are using Springdoc for API documentation, let's replace your springdoc-openapi-ui by:
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.2.0</version>
        </dependency>
Enter fullscreen mode Exit fullscreen mode
  • If you are using spring-kafka as your Kafka client, let's update it to v3.0.x
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
            <version>3.0.10</version>
        </dependency>
Enter fullscreen mode Exit fullscreen mode

3.2.3. Rebuild and change the code

  • After updating your dependencies, let's rebuild your code first and check for any issues
  mvn clean package
Enter fullscreen mode Exit fullscreen mode
  • Spring Boot 3.0 has migrated from Java EE to Jakarta EE APIs for all dependencies. So you should face the javax issue in your first build:

Javax_persistence_not_exist

Then you just need to replace all javax in your imports by jakarta (should use Ctrl+Shift+R to replace on IntelliJ)

replace_javax_by_jakarta

  • If the Spring migrator is working, you can see a WARNING log in the startup console:

spring_migrator

Simply update as suggested.

  • In Hibernate 6, you can use MySQLDialect for all MySQL versions (MySQL5Dialect, MySQL8Dialect have been deprecated)
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
Enter fullscreen mode Exit fullscreen mode
  • JPA SpringPhysicalNamingStrategy is replaced by CamelCaseToUnderscoresNamingStrategy
org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy

==> org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
Enter fullscreen mode Exit fullscreen mode
  • Spring Security is not working if you are using WebSecurityConfigurerAdapter (deprecated). Besides, in v3.x, you have to use lambda to configure filterChain.

So you have to remove the extension of WebSecurityConfigurerAdapter class.

// Now
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {}

// Then without extension
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class WebSecurityConfig {}
Enter fullscreen mode Exit fullscreen mode

Then do several updates for AuthenticationManager bean creation:

// Now with the class extends WebSecurityConfigurerAdapter
  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    // Get AuthenticationManager bean
    return super.authenticationManagerBean();
  }

// Then without extension
  @Bean
  public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
    return authConfig.getAuthenticationManager();
  }
Enter fullscreen mode Exit fullscreen mode

And SecurityFilterChain:

/** We have to use Lambda for SecurityFilterChain configuration **/

// Now with the class extends WebSecurityConfigurerAdapter
  @Override
  protected void configure(HttpSecurity http) throws Exception {
      http.cors().and().csrf().disable();
      http.authorizeRequests().antMatchers(AUTH_WHITELIST).permitAll().anyRequest().authenticated();
      http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
      http.exceptionHandling().authenticationEntryPoint(authEntryPointJwt);
      http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
  }

// Then with lambda
  @Bean
  protected SecurityFilterChain configure(HttpSecurity http) throws Exception {
      http.cors(AbstractHttpConfigurer::disable).csrf(AbstractHttpConfigurer::disable);
      http.authorizeHttpRequests(auth -> auth.requestMatchers(AUTH_WHITELIST).permitAll().anyRequest().authenticated());
      http.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
      http.exceptionHandling(ex -> ex.authenticationEntryPoint(authEntryPointJwt));
      http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
      return http.build();
  }
Enter fullscreen mode Exit fullscreen mode
  • Powermock is not working with Spring Boot 3 and JDK 17. Its latest update is in Feb 2022. So you need to move to Mockito if you are using Powermock.

  • The jjwt library updates its API, so we update our code:

// Now
  public String generateJwtToken(Long userId) {
    return Jwts.builder()
        .setSubject(userId.toString())
        .setIssuedAt(new Date())
        .setExpiration(new Date(new Date().getTime() + jwtExpirationMs))
        .signWith(SignatureAlgorithm.HS256, jwtSecret.getBytes(StandardCharsets.UTF_8))
        .compact();
  }


// Then
  public String generateJwtToken(Long userId) {
    return Jwts.builder()
        .subject(userId.toString())
        .issuedAt(new Date())
        .expiration(new Date(new Date().getTime() + jwtExpirationMs))
        .signWith(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8)), Jwts.SIG.HS256)
        .compact();
  }
Enter fullscreen mode Exit fullscreen mode

And the JWT parser:

// Now
public Claims getJwtTokenClaim(String jwt) {
   return Jwts.parser()
        .setSigningKey(jwtSecret.getBytes(StandardCharsets.UTF_8))
        .parseClaimsJws(jwt)
        .getBody();
}

// Then
public Claims getJwtTokenClaim(String jwt) {
    return Jwts.parser()
        .verifyWith(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8)))
        .build()
        .parseSignedClaims(jwt)
        .getPayload();
}
Enter fullscreen mode Exit fullscreen mode
  • Finally, rebuild and check for any issues. In my project, there are no issues left, so I stopped updating the code here.

3.2.4. Testing

Since this is a major upgrade, carefully test all APIs for discrepancies or exceptions. Review them one by one and monitor logs.

IV. Conclusion

We just completed the migration to Spring Boot 3 and JDK 17. There are many issues during this process, so stay calm and get things done :))). I hope this is helpful for anyone planning the migration.

Anw, If you find this project useful, please give it a star ⭐️!
Henry Pham.

Top comments (0)