Spring Boot 3 was released at the end of 2022 but some projects are still using Spring Boot 2.7. I’ve recently migrated several projects and libraries from Spring Boot 2.7.15 to Spring Boot 3.2.4 and I’d like to share some insights into the changes I had to make to ensure everything functioned smoothly.

Just a heads up, the update required quite a bit of file changes. The majority is due to the transition from Java EE to Jakarta EE, resulting in many javax. imports being replaced with jakarta. imports.

While I aimed to provide as thorough an overview as possible, it’s important to note that your service might encounter specific situations not addressed here. This is because the implementation discussed pertains to a specific approach utilized by the projects that I have worked with.

Dependency changes

The first thing is to change the dependencies. Let’s start with the spring dependencies. The examples will be shown using gradle groovy configuration.

Upgrading Spring

Look for the usage of the plugin id org.springframework.boot and set the version as 3.2.4.

this

plugins {
 id 'org.springframework.boot' version '2.7.11'

becomes

plugins {
  id 'org.springframework.boot' version '3.2.5'

We also need to update the Spring Cloud org.springframework.cloud:spring-cloud-dependencies from version 2021.x.x to 2023.0.1

Before we had something like this:

implementation platform("org.springframework.cloud:spring-cloud-dependencies:2021.0.9")

and now we have something like this:

implementation platform("org.springframework.cloud:spring-cloud-dependencies:2023.0.1")

make sure to replace it in all subprojects and all places you use it.

Replace Sleuth with Micrometer

Spring Sleuth was replaced with Micrometer, so we have to remove the old spring Sleuth started, if your project has the following dependencies, please remove them.

I have created a more detailed post about Migrating Spring Sleuth to Micrometer Observability in Spring Boot 3

Replace hibernate-types

We use PostgreSQL and a library for custom types com.vladmihalcea:hibernate-types-5. But this library was moved to another package, so we have to replace the following

implementation("com.vladmihalcea:hibernate-types-5:2.3.5")

with the following

implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.7.3'

They are the same in a way, just the package changed. Due to this, all imports starting with

  1. com.vladmihalcea.hibernate became io.hypersistence.utils.hibernate
  2. com.vladmihalcea.spring became io.hypersistence.utils.spring

You can learn more here

Wiremock replacement old package

In our case, some libraries had an old version of Wiremock. the library was migrated to another group ad module, so the old one was.

testImplementation "com.github.tomakehurst:wiremock-jre8:2.35.0"

replace it with the new one

testImplementation 'org.wiremock:wiremock-standalone:3.5.2'

Java EE to Jakarta EE dependencies

With the change of the Java EE to Jakarta EE, I few libraries changed.

  • ‘javax.xml.bind:jaxb-api:2.3.1’ became ‘jakarta.xml.bind:jakarta.xml.bind-api:4.0.2’
  • ‘javax.persistence:javax.persistence-api:2.2’ becomes jakarta.persistence:jakarta.persistence-api:3.1.0

Other library updates that you may need to update

These are other libraries that changed and your project may or may not have them, we recommend checking your project for each of them and updating accordingly. Some of them are:

  • Tomcat 10 (org.apache.tomcat.embed from 9.0.78 to 10.1.13
  • Hibernate 6 ( org.hibernate:hibernate-core from 5.6.15.Final to 6.2.9.Final)
  • Flyway 9 (org.flywaydb:flyway-core from 8.5.13 to 9.16.3)
  • Hazelcast 5 (com.hazelcast:hazelcast from 4.2.6 to 5.2.4)
  • sl4j 2 (org.slf4j:slf4j-api and org.slf4j:slf4j-simple from 1.7.36 to 2.0.12)
  • Javers (org.javers:javers-spring form 6.x.x to 7.4.2
  • Shedlock (net.javacrumbs.shedlock:shedlock-spring from 4.44.0 to 5.11.0
  • JAXB runtime (org.glassfish.jaxb:jaxb-runtime) from 2.3.1 to 4.0.3

Compile Changes

Java EE to Jakarta EE

Most of the packages from Java EE (javax.*) became Jakarta EE (jakarta.*). Due to that, we need to upgrade a lot of other libraries to match this and avoid ClassNotFoundException.

You can use IntelliJ to do most of the work for you (though it may miss some stuff). Navigate to Refactor → Migrate Packages and Classes → Java EE to Jakarta EE.

Select the Whole project and Run.

On the window that opens, select Do Refactor

Tracing Import

As we removed the Spring Sleuth dependency above and added Micrometer, all the imports that used sleuth will break. You can do a simple find and replace and it will help you fix almost everything.

You can find the code

import org.springframework.cloud.sleuth.

And replace it with

import io.micrometer.tracing.

These should be enough to replace almost everything.

@Type and @TypeDefs Mapping

@TypeDefs and @TypeDef have been removed and @Type annotation changed, so we cannot specify a string anymore.

We remove all the @TypeDef and @TypeDefs as you don’t need them anymore.

As for the @Type, we remove them to replace it with the class directly.


@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)})  
public class Company {  
  
    @Type(type = "jsonb")  
    Map<String, String> inviters;
}

with @Type

public class Company {  

  @Column(columnDefinition = "jsonb")
  @Type(JsonBinaryType.class)
  Map<String, String> inviters;

Repository

In case you have repositories that extend PagingAndSortingRepository you need to replace it with ListCrudRepository or ListPagingAndSortingRepository. PagingAndSortingRepository still exists, but it does not have some basic stuff like count, save, etc.

Controller Parameters

Starting in Spring 6 (Spring Boot 3) Spring no longer attempts to deduce parameter names by parsing bytecode which means that if you have something like this

public void patchAccounts(@PathVariable String id, ...)

it will break as it expects a name, like this one

public void patchAccounts(@PathVariable("id") id, ...)

This is also true for @RequestParam where you have to specify the name now.

The best way is to fix everything in the code, but if you think that is too much, you can use an arg in the Java compiler and add the following configuration to your build.grade

tasks.withType(JavaCompile).configureEach {
    options.compilerArgs.add("-parameters")
}

Feign decode404

Feign clients decode404 was removed in favor of dismiss404. You can just find and replace it in the project. According to the documentation, the behavior is the same, just a name change.

Flyway ignores missing migration

In version 8 of Flyway, the property spring.flyway.ignore-missing-migrations was deprecated and it was removed in version 9.

To have the same effect, you can have

spring.flyway.ignore-migration-patterns=*:missing

Check for migrated and broken properties

Spring has a special dependency that you can use to detect properties that changed during the migration. Basically, you add the dependency, fix the things, and remove it.

Add the following dependency:

runtimeOnly("org.springframework.boot:spring-boot-properties-migrator")

Now you try to compile and it is gonna complain about the properties. Fix them and remove the dependency.

Unable to resolve LocalServerPort

If your plugin breaks because it cannot find the import

import org.springframework.boot.web.server.LocalServerPort

change the import to

import org.springframework.boot.test.web.server.LocalServerPort

Possible problems when running

Flyway migration stuck when it contains CONCURRENTLY

If you have a migration that contains CONCURRENTLY is running indefinitely, you have to add the following configuration to your Flyway:

Flyway flyway = Flyway.configure()
        // other previous configurations
        .configuration(Map.of("flyway.postgresql.transactional.lock", "false"))
        .load();

you can see the discussion [here](https://github.com/spring-projects/spring-boot/issues/32629#issuecomment-2043481055

@ExceptionHandler is returning XML instead of JSON

If your exception is returning XML instead of JSON, it means that you didn’t specify a header with content type and you probably have jackson XML in the classpath, so you have the return of something like this.

return ResponseEntity.status(BAD_REQUEST).body(error);

just add .contentType(MediaType.APPLICATION_JSON) to the builder and it should work ok

return ResponseEntity.status(BAD_REQUEST)
        .contentType(MediaType.APPLICATION_JSON)
        .body(error);

HTTP Endpoints defaulting to XML

By default, we were not setting JSON as the HTTP response type, but after the upgrade it stopped doing that it started returning XML (probably because we also had a Jackson XML library in the classpath).

To set the default content type, we changed our WebMvcConfigurer to set JSON as the default.

public class WebMvcConfiguration implements WebMvcConfigurer {  
  
    @Override  
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer)  
    {  
        configurer.defaultContentType(MediaType.APPLICATION_JSON);  
    }
}

These were the primary adjustments we needed to implement to ensure that the applications functioned smoothly on Spring Boot 3.2, similar to how they operated on Spring Boot 2.7. While I’ve aimed to provide detailed descriptions, it’s worth noting that each project may have unique configurations, and certain issues may arise that aren’t covered here. Nonetheless, I hope this information proves helpful in navigating potential challenges and finding suitable solutions.

Cheers.