A few actionable steps to prepare for your Spring 3 migration
Prepare for Spring 3 migration
As the last spring-boot version 2.7.x is close to ending support(Nov 18, 2023), I started migrating my project to Spring Boot 3 and wrote down the most important issues I encountered during the migration.
- Java 17
- Jakarta EE
- Kafka
- OpenAPI
- Security
There are already many migration guides, but I think it can start from these two documents:
After completing the Spring 3 migration, eventually, my project dependencies will look like this:
+----------------------+---------+
| Lib | Version |
+----------------------+---------+
| spring-boot | 3.0.6 |
| spring-core | 6.0.8 |
| spring-security-core | 6.0.3 |
| spring-webmvc | 6.0.8 |
| httpclient5 | 5.1.4 |
| spring-kafka | 3.0.6 |
| spring-kafka-test | 3.0.6 |
| kafka-clients | 3.3.2 |
+----------------------+---------+
Java 17
The minimum Java version required by Spring 3 is Java 17
Instant precision
Java 11 - Instant: 2022-12-12T18:04:27.267229Z
Java 17 - Instant: 2022-12-12T18:04:27.267229114Z
On certain platforms, Instant will get nine decimal digits. However, Postgres, MySQL and Kafka still store six decimal digits. Hence in some unit tests, the original timestamp (nine decimal digits) is not equal to the transformed one (six decimal digits).
Solution
Truncate the original timestamp into microseconds with the following command:
Instant.now().truncatedTo(ChronoUnit.MICROS)
Refer to the following references:
- Changes in Instant.now() between Java 11 and Java 17 in AWS Ubuntu standard 6.0
- Java 15 nanoseconds precision causing issues in the Linux environment
Jakarta EE
As javax namespace is removed from Java 17, Jakarta EE 9 is the minimum requirement in the Spring 3 release notes.
I would advise you to replace all javax to jakarta in this article, e.g.,
javax.persistence.EmbeddedId
javax.persistence.Entity
javax.persistence.Lob
javax.persistence.Table
to
jakarta.persistence.EmbeddedId
jakarta.persistence.Entity
jakarta.persistence.Lob
jakarta.persistence.Table
There should be a jakarta package instead of a javax package in the dependencies. Here’s what that looks like:
jakarta.persistence-api
jakarta.validation-api
jakarta.annotation-api
jakarta.servlet-api
Apache HttpClient in RestTemplate
Support for Apache HttpClient has been removed in Spring Framework 6.0 and immediately replaced by org.apache.httpcomponents.client5:httpclient5 (Note: this dependency has a different groupId). If you are noticing issues with HTTP client behavior, that RestTemplate is falling back to the JDK client.
org.apache.httpcomponents:httpclient can be brought transitively by other dependencies, so your application might rely on this dependency without declaring it.
See the Spring-Boot-3.0-Migration-Guide.
Here is my customised resttemplate example:
fun RestTemplateBuilder.withCustConnectionPool(maxConnTotal:Int): RestTemplateBuilder {
val connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
.setMaxConnTotal(maxConnTotal)
.build()
val client = HttoClientBuilder.create().apply {
// customized
}.setConnectionManager(connectionManager).build()
val rf: Supplier<ClientHttpRequestFactory> = Supplier { HttpComponentsClientHttpRequestFactory(client) }
return this.requestFactory(rf)
}
ConstructingBinding annotations
The ConfigurationProperties class annotation @ConfigurationProperties is no longer required by default to mark constructions with @ConstructorBinding, and you should remove it from the configuration class unless the configuration class has multiple constructors to configure the property binding explicitly.
It is mentioned in another Spring 3 migration guide.
OpenAPI generator
We often use OpenAPI generator to create the contracts. To generate code and provide dependencies for use with Spring Boot 3.x and use jakarta instead of javax in imports:
"useSpringBoot3" to "true",
"useJakartaEe" to "true"
Refer to the following reference guide:
Documentation for the spring Generator | OpenAPI Generator
The relevant dependencies are:
implementation("org.yaml:snakeyaml:2.0")
implementation("io.swagger.parser.v3.swagger-parser:2.1.15")
implementation("org.openapitools:openapi-generator-gradle-pluin-api:6.5.0")
Security
WebSecurityConfigurerAdapter is deprecated in Spring 3.
This can be change from:
@Configuration
open class SecurityConfiguration: WebSecurityConfigurerAdapter() {
@Override
override fun configure(val http: HttpSecurity) {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
httpBasic {}
}
}
}
to
@Configuration
open class SecurityConfiguration {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
httpBasic {}
}
return http.build()
}
}
and In-Memory Authentication can be changed from
@Configuration
open class SecurityConfiguration: WebSecurityConfigurerAdapter() {
override fun configure(auth: AuthenticationManagerBuilder auth) {
auth.inMemoryAuthentication()
.withUser("username")
.password("{noop}pass")
.authorities(listOf())
}
}
to
@Configuration
open class SecurityConfiguration {
@Bean
fun userDetailService():InmemoryUserDetailsManager {
val user: UserDetails = User.withDefaultPasswordEncoder()
.username("username")
.password("pass")
.authorities(listOf())
.build()
return InMemoryUserDetailsManager(user)
}
}
Note: You can’t use {noop} to indicate the encryptor
Refer to these guides for more information:
and
Spring Security without the WebSecurityConfigurerAdapter
Spring-Kafka
Transaction-Id
The transactional.id property of each producer is transactionIdPrefix + n, where n starts with 0 and is incremented for each new producer. In previous versions of Spring for Apache Kafka, the transactional.id was generated differently for transactions started by a listener container with a record-based listener to support fencing zombies.
This is not necessary anymore, with EOSMode.V2 being the only option starting with 3.0. For applications running with multiple instances, the transactionIdPrefix must be unique per instance.
The transaction-id-prefix design has been changed since 3.0. Refer to this commit:
Remove outdated information for transactional.id (#2524) · spring-projects/spring-kafka@7e336d1
In spring-kafka 3, we need to assign a unique Transaction-Id-Prefix for each application instance. The usual approach is to use a random string/hostname, as shown below:
fun uniqueTransactionIdPrefix(producerOnly: String = "TX-") {
return InetAddress.getLocalHost().getHostName() + this.transactionIdPrefix + producerOnly
}
Note: the producerOnly is used to differentiate a consumer-initiated producer or a producer-only.
It is not an ideal solution. As in the Kubernetes environment, the hostname could change after crash/restart. However, the restart might take longer than the transaction.timeout.ms, so it should be fine.
Refer to the following Stack Overflow question for more information:
transaction-id-prefix need to be identical for same application instance in spring kafka 3?
More about Kafka transaction
This article describes the Kafka transaction and idempotency:
Transactions in Apache Kafka | Confluent
and Spring’s official docs is another treasure:
This link is about how to configure transaction-id-prefix in spring-kafka 2.x. It is still helpful in spring 3 to understand the design of kafka-producer.
Basically, the Kafka producer has a unique ID, such as transaction-id-prefix-atomic_inc_id When it wants to use kafkatemplate to send data, it will take an idle producer from the cache, and return/close it after committing a transaction. The producer’s epoch will also be bumping after the transaction is committed.
An issue we often observe is ProducerFencedException, which means a producer has a higher epoch, but the other one (or zombie) with the same producerId still tries to commit it with a lower epoch; hence it gets fenced. It is caused by a client rebalance or a programming bug.
Refer to the following Stack Overflow question for more information:
Why my Spring Kafka unit test almost ran into ProducerFencedException every time
KafkaTemplate changes
In version 3.0, the methods that previously returned ListenableFuture have been changed to return CompletableFuture. To facilitate the migration, the 2.9 version added a method usingCompletableFuture() that provided the same methods with CompletableFuture return types. This method is no longer available.
It changes from this:
kafkaTemplate.send(producerRecord).addCallback(
{
result: SendResult<Any?, Any?>? ->
log.info("xxxx")
},
{
ex: Throwable? ->
log.error("xxx")
}
)
to
kafkaTemplate.send(producerRecord).whenComplete { result, ex ->
if (ex!=null) {
// exception
} else {
// successfully sent
}
}
JacksonObjectMapper
Problem
No Serializer found for class org.springframework.http.HttpMethod and no properties discovered to create BeanSerializer
The HttpMethod member variable name is changed to private in Spring 3, hence ObjectMapper is unable to serialize the private variable
Solution
Customize the serializer or allow jacksonObjectMapper to serialize private properties.
jacksonObjectMapper().setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
Happy coding!
Spring Boot 3 Migration was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.