Spring Boot 1.5.x to 2.0.x Migration – first step of migration to Java 11

Spring Boot 1.5.x to 2.0.x Migration – first step of migration to Java 11

For a long time, we have relied on Java 8 for most of our projects. But with recent announcements from Oracle, the situation has changed and we had to plan some actions.

The commotion around Java is caused by the fact that Oracle has announced that, effective January 2019, ”Java SE 8 public updates will no longer be available for “Business, Commercial or Production use” without a commercial license.” The solution is Java 11 (JDK 11), as it is officially out as the Java Long-Term Support (LTS) release. Java Open JDK 11 version will be interchangeable with Oracle JDK version.

As announced in September 2017, with the Oracle JDK and builds of Oracle Open JDK being interchangeable for releases of Java SE 11 and later ones, the Oracle JDK will primarily be for commercial and support customers, while Open JDK builds from Oracle are for those who do not want commercial support or enterprise management tools.

There is also a declaration of six month free updates for Open JDK from Oracle. For these reasons, migration from Java 8 to 11 is a good idea. Actually, the safest path for growing your Java projects seems to be prepared for upgrading with each new release.

There are of course alternatives. You can use JDK from other vendors like: Azure, IBM, RedHat or recently announced Amazon.

 

Migration of large, complex codebase from Java 8 and Spring Boot 1.5.x to Java 11 and Spring Boot 2.x

In this series of articles we will present steps we had to perform to successfully migrate large and complex codebase to Spring Boot 2.x and Java 11. The project is microservices based insurance system. It’s been actively developed for 3 years, with the first production release in 2016.

First, the plan is to upgrade the project from Spring Boot 1.5.x to 2.x, and then to upgrade to Java 11.

 

Starting point

The project first commit is from September 2015 (developed ever since by team of 20+ members) and current production version consists of circa 6191 files. The project is based on microservices and it contains 32 separate backend modules.

These numbers should give you a brief glimpse of the project size. It is safe to say that this is a pretty big project. The technologies used in the project are mainly: Spring Boot, Spring Cloud, Zuul, Redis, JWT, Swagger, Feign, Hibernate. Maven is used as a build tool.

To migrate to Java 11 we plan the following milestones:

  1. Change project from Spring Boot 1.5.x to 2.0.x
  2. Change project from Spring Boot 2.0.x to 2.1.0
  3. Change Java 8 to Java 11

Why the first step is Spring Boot update?

Based on Wiki:

Spring Boot 2 is the first version to support Java 9 (Java 8 is also supported). If you are using 1.5 and wish to use Java 9 you should upgrade to 2.0 as we have no plans to support Java 9 on Spring Boot 1.5.x.

Java 10 is supported as of Spring Boot 2.0.1.RELEASE.
Java 11 is supported as of Spring Boot 2.1.0.M2.

Our plan for step one is outlined below:

  1. Build 3 modules on Spring Boot 2 and successfully run them locally.
  2. Build all modules on Spring Boot 2 and successfully run them locally.
  3. Deploy modules to test environment and test them.

 

Let’s go

At first, I had included this dependency in these modules:

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

It helps with Spring Boot migration process.

The basic change I had made after that was changing the Spring Boot version:

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.0.3.RELEASE</version> 
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.0.3.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency> 

I had also changed the Spring Cloud dependency version:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-parent</artifactId>
    <version>Finchley.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency> 

As it was based on SpringBoot 2.0.3.RELEASE, I had made these dependencies compatible with each other based on this matrix.

First run resulted in errors saying that there were missing versions of some dependencies (i.e. guava, jwt, javax.inject, spock-spring, hibernate-validator). We had to adjust dependencies and change feign dependency to more recent:

From:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
    <version>1.4.6.RELEASE</version>
</dependency> 

To:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency> 

Also there were many issues with package change etc.

 

Changes in imports in our microservices


import org.springframework.boot.web.support.SpringBootServletInitializer;
// changed into:
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;


import org.springframework.cloud.netflix.feign.EnableFeignClients;
// changed into:
import org.springframework.cloud.openfeign.EnableFeignClients;


import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
// changed into:
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;


import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
// changed into:
import org.springframework.boot.jdbc.DataSourceBuilder;


import org.springframework.data.querydsl.QueryDslPredicateExecutor;
// changed into:
import org.springframework.data.querydsl.QuerydslPredicateExecutor;


import org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor;
// changed into:
import org.springframework.ws.soap.security.wss4j2.Wss4jSecurityInterceptor;

The annotation below had been deleted and it did not really impact future process:

@Order(ManagementServerProperties.ACCESS_OVERRIDE_ORDER) 

 

Redis default implementation

Redis changed default implementation from Jedis to Lettuce. Because of that, I needed to add an additional dependency (commons-pool2):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency> 

And changing config from:

import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
(...)

@Bean
JedisConnectionFactory redisConnectionFactory() {
   JedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory();
   redisConnectionFactory.setHostName(hostName);
   redisConnectionFactory.setPort(port);
   return redisConnectionFactory;
}

To:

import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
(...)

@Bean
LettuceConnectionFactory lettuceConnectionFactory() {
   LettuceConnectionFactory redisConnectionFactory = new LettuceConnectionFactory();
   redisConnectionFactory.setHostName(hostName);
   redisConnectionFactory.setPort(port);
   return redisConnectionFactory;
}

Regarding Redis there was also change here:

From:

@Bean(name = PERSON_HISTORY_CACHE_MANAGER)
public CacheManager personHistoryCacheManager(@Qualifier("personTemplate")RedisTemplate personTemplate) {
    RedisCacheManager cacheManager = new RedisCacheManager(personTemplate);
    cacheManager.setDefaultExpiration(expirationValidityMinutes * 60); 
    return cacheManager;
}

To:

@Bean(name = PERSON_HISTORY_CACHE_MANAGER)
public CacheManager personHistoryCacheManager(RedisConnectionFactory redisConnectionFactory) {
    Duration expiration = Duration.ofSeconds(expirationValidityMinutes * 60);
    
    return RedisCacheManager
            .builder(redisConnectionFactory)
            .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig().entryTtl(expiration))
            .build();
}

 

GuavaCacheManager to CaffeineCacheManager

GuavaCacheManager changed into CaffeineCacheManager which needs an additional library (if you want to configure CaffeineSpec like using guavaCacheManager.setCacheBuilder in the java file instead of using application properties):

<dependency>
   <groupId>com.github.ben-manes.caffeine</groupId>
   <artifactId>caffeine</artifactId>
</dependency> 

 

CrudRepository methods

CrudRepistory changed its names of methods and also started to return Optional type by default.

The methods changed from operation on collections:

delete() -> deleteAll()

save() -> saveAll()

And also with finding giving now an Optional:

findOne() -> findById()

findById() return an Optional, because of that a good idea is to use orElseThrow() and throwing an exception if the element was not found.

 

Problem with inject

I had problems with inject which had been fixed by adding this dependency:

<dependency>
    <groupId>com.google.inject</groupId>
    <artifactId>guice</artifactId>
    <version>3.0</version>
</dependency> 

 

Datasource URL property

The property like:

primary.datasource.url=jdbc:postgresql://192.168.0.111:5432/someAdress?currentSchema=some_registries

changed to:

primary.datasource.jdbc-url=jdbc:postgresql://192.168.0.111:5432/someAdress?currentSchema=some_registries

Spring Boot 2.x.x changed default JDBC connection pool from Tomcat to faster and better HikariCP. Here comes incompatibility, because HikariCP uses different property of jdbc url.

 

Replace hibernate-validator dependency

Then I changed dependency from:

<dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>${hibernate-validator.version}</version>
</dependency> 

To:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>2.0.3.RELEASE</version>
</dependency> 

I started getting this error while starting some modules:

2018-10-22 11:33:19.075 ERROR [moto,,,] 12260 --- [on(3)-127.0.0.1] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'documentationPluginsBootstrapper' defined in URL [jar:file:somePackages/someJar!/springfox/documentation/spring/web/plugins/DocumentationPluginsBootstrapper.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'webMvcRequestHandlerProvider' defined in URL [jar:file:somePackages/someJar!/springfox/documentation/spring/web/plugins/WebMvcRequestHandlerProvider.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Invocation of init method failed; nested exception is java.lang.ArrayStoreException

(...)

Caused by: java.lang.ArrayStoreException: null

I tried to change Swagger dependencies to new ones.

From:

<swagger.version>2.7.0</swagger.version>
<swagger-maven-plugin.version>3.1.5</swagger-maven-plugin.version>
<swagger2markup-maven-plugin.version>1.3.1</swagger2markup-maven-plugin.version> 

To:

<swagger.version>2.9.2</swagger.version>
<swagger-maven-plugin.version>3.1.7</swagger-maven-plugin.version>
<swagger2markup-maven-plugin.version>1.3.7</swagger2markup-maven-plugin.version> 

But it did not help. Suppression of Swagger related code led to changing cxf version:

From:

<properties>
    <cxf.version>3.1.11</cxf.version>
</properties> 

To:

<properties>
    <cxf.version>3.2.6</cxf.version>
</properties> 

Also Guava version changed from 18 to 21.

From:

<properties>
    <guava.version>18.0</guava.version>
</properties> 

To:

<properties>
    <guava.version>21.0</guava.version>
</properties> 

And that fixed this issue in one module. In the next one there was also an error saying:

nested exception is java.lang.NoClassDefFoundError: Could not initialize class org.hibernate.validator.internal.engine.ConfigurationImpl

This was not obvious but I fixed it by changing dependency from:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.jboss.logging</groupId>
            <artifactId>jboss-logging</artifactId>
         </exclusion>
     </exclusions>
</dependency> 

To:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency> 

As we can see here – exclusions should be used with caution. The error even did not gave a hint of what was the main cause of it. The starter-type dependencies are made, so they work well together. If we remove one part, something can go wrong.

 

Remove migrator

I got rid of spring-boot-properties-migrator dependency, as I did not need it anymore.

 

Build successfully

After this step all modules were started and built successfully.
It was time to deploy them and see if there are some other issues.

 

Problem with JWT Token

While I was starting my tests, there was an issue with bearer token – when I was sending a request directly via service everything was OK but after I sent request indirectly via zuul then I always got 401, even though token was valid (the same one that was passing in a direct request).

This was fixed by changing these dependencies:

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>${spring-security-oauth2.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>${spring-security-jwt.version}</version>
</dependency> 

To:

  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-oauth2</artifactId>
  </dependency>
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-security</artifactId>
  </dependency> 

 

Summary

I think that the migration process could be easier if there was more information about it. I could not find many answers to problems that I had to deal with regarding it.
I hope that this article is going to make someone’s life easier and will result in less headaches 😉

 

Author: Kamil Witkowski, Java Developer, ASC

Have you enjoyed the article? If yes, please share it with your network!

Add comment

avatar
  Subscribe  
Notify of