Book's Upgrade: Migrating from Spring Boot 2.4 to 2.5

Book's Upgrade: Migrating from Spring Boot 2.4 to 2.5

This chapter describes how to upgrade the book’s source code to Spring Boot 2.5.5 and Java 17. No major changes but some parts of the code break, so we need to make some little adjustments here and there. We’ll also learn how to use the new RandomGenerator interface in tests.

Table of Contents

No major complications, but the update breaks the code

The last upgrade of the book repositories to Spring Boot 2.4 was quite extensive mainly because of the upgrade to a new major version of Spring Cloud (2020.0.0). This time, the impact on the book’s code is much smaller.

This is a practical approach to migrating a Spring Boot project from version 2.4 to version 2.5 and JDK 17. It uses the codebase from the Learn Microservices with Spring Boot book's practical example, but it applies to other projects too. If you want to dive into how Spring Boot works and set up a complete microservice architecture, get a copy now. You can also visit the book's main page for more details about the contents and extra chapters.

We’ll take as a baseline the last changes to upgrade the projects to Spring Boot 2.4. The code is available on GitHub (while you’re there, star it!).

Migrate to Spring Boot 2.5.5 and Java 17

First, we need to download and install the JDK version 17 in case we haven’t done that already.

Then, we update the pom.xml files in all the Spring Boot projects. See Listing 1 as an example (multiplication service).

  • The version of the spring-boot-starter-parent dependency has been set to 2.5.5. All the library dependencies will be also upgraded following this parent pom.
  • The java.version property is now 17, which means we want to build our project using the JDK 17. Make sure you set your JAVA_HOME environment variable accordingly.
  • The project version has changed to 2.5.5-SNAPSHOT to make it consistent with Spring Boot versioning. We could use any other versioning approach but this is convenient for the book upgrades.
  • We upgraded Spring Cloud to the latest version, 2020.0.4.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>microservices.book</groupId>
    <artifactId>multiplication</artifactId>
    <version>2.5.5-SNAPSHOT</version>
    <name>multiplication</name>
    <description>Multiplication Microservice - Learn Microservices with Spring Boot - Upgrade 2.5.5</description>

    <properties>
        <java.version>17</java.version>
        <spring-cloud.version>2020.0.4</spring-cloud.version>
    </properties>

    <dependencies>
        <!-- ... they remain the same, versions are updated because of the parent -->        
    </dependencies>    
</project>

Listing 1. Upgrading Spring Boot and Java

No more spying on Randoms

The JDK 17 introduces some changes around the Random class that breaks one of the tests in the Multiplication repository. See Listing 2 for the offending test.

@ExtendWith(MockitoExtension.class)
public class ChallengeGeneratorServiceTest {

    private ChallengeGeneratorService challengeGeneratorService;

    @Spy
    private Random random;

    @BeforeEach
    public void setUp() {
        challengeGeneratorService = new ChallengeGeneratorServiceImpl(random);
    }

    @Test
    public void generateRandomFactorIsBetweenExpectedLimits() {
        // 89 is max - min range
        given(random.nextInt(89)).willReturn(20, 30);

        // when we generate a challenge
        Challenge challenge = challengeGeneratorService.randomChallenge();

        // then the challenge contains factors as expected
        then(challenge).isEqualTo(new Challenge(31, 41));
    }

}

Listing 2. The ChallengeGeneratorServiceTest class

When we run this test with Java 17, we get this error:

Mockito cannot mock this class: class java.util.Random.

Followed by the root cause, a few lines below:

Underlying exception : java.lang.IllegalStateException: Cannot access annotation property public abstract boolean jdk.internal.util.random.RandomSupport$RandomGeneratorProperties.isStochastic()

Instead of trying to figure out why this doesn’t work, we can use this opportunity to improve our code and remove the existing trick. We were using Mockito.spy() to change the behavior of an existing Random object and make it return the numbers we want instead of random numbers. We did this because there was no better way to mock a plain-java Random generator before JDK 17.

The good news is that now there is a better way. Random implements the new interface RandomGenerator (see Javadoc). Mocking interfaces with Mockito is simpler than Spying objects, and it’s a better practice.

Therefore, we can change our implementation of ChallengeGeneratorServiceImpl to accept the interface instead of the specific implementation. See Listing 3. Now, the constructor we created for tests accepts and assigns a RandomGenerator object. The default constructor assigns a Random object to it, which works because this class implements this interface from JDK 17 onwards.

package microservices.book.multiplication.challenge;

import org.springframework.stereotype.Service;

import java.util.Random;
import java.util.random.RandomGenerator;

@Service
public class ChallengeGeneratorServiceImpl implements ChallengeGeneratorService {

    private final static int MINIMUM_FACTOR = 11;
    private final static int MAXIMUM_FACTOR = 100;

    private final RandomGenerator randomGenerator;

    ChallengeGeneratorServiceImpl() {
        this.randomGenerator = new Random();
    }

    protected ChallengeGeneratorServiceImpl(final RandomGenerator randomGenerator) {
        this.randomGenerator = randomGenerator;
    }

    private int next() {
        return randomGenerator.nextInt(MAXIMUM_FACTOR - MINIMUM_FACTOR) + MINIMUM_FACTOR;
    }

    @Override
    public Challenge randomChallenge() {
        return new Challenge(next(), next());
    }
}

Listing 3. The ChallengeServiceGeneratorServiceTest class

With this new code in place, we can alter the test to look much more straightforward. We simply need to use a @Mock annotation over the interface, and instruct Mockito to return the predefined values as we did before. See Listing 4.

@ExtendWith(MockitoExtension.class)
public class ChallengeGeneratorServiceTest {

    private ChallengeGeneratorService challengeGeneratorService;

    @Mock
    private RandomGenerator randomGenerator;

    @BeforeEach
    public void setUp() {
        challengeGeneratorService = new ChallengeGeneratorServiceImpl(randomGenerator);
    }

    @Test
    public void generateRandomFactorIsBetweenExpectedLimits() {
        // 89 is max - min range
        given(randomGenerator.nextInt(89)).willReturn(20, 30);

        // when we generate a challenge
        Challenge challenge = challengeGeneratorService.randomChallenge();

        // then the challenge contains factors as expected
        then(challenge).isEqualTo(new Challenge(31, 41));
    }

}

Listing 4. The new ChallengeServiceGeneratorServiceTest class

Removing application-default.properties

Prior to this last update of Spring Boot, I created a application-default.properties file under the test resource folder in the Gamification service. The only setting configured there was the active profile. See Listing 5.

spring.profiles.active=test

Listing 5. The application-default.properties file

Since the default profile in Spring Boot is named default, this property would be loaded and therefore the other file application-test.properties would be also processed in search of additional properties. The contents of that file are shown in Listing 6, and the goal is to disable Spring Cloud Consul for tests.

spring.cloud.consul.enabled=false

Listing 6. The application-test.properties file

That strange setup was necessary to make all the Spring Boot tests pick up that configuration without needing to add an annotation to all of them (see this SO thread). However, when I upgraded to the latest Spring Boot version and ran the tests for the Gamification service I got an error. See Listing 7.

java.lang.IllegalStateException: Failed to load ApplicationContext
Caused by: org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property 'spring.profiles.active' imported from location 'class path resource [application-default.properties]' is invalid in a profile specific resource [origin: class path resource [application-default.properties] - 1:24]

Listing 7. Can’t use a default profile to point to an active profile

That solution doesn’t work anymore. Accidentally, I introduced the annotations in the Gamification service tests that point to a new test.properties file which contains the property to disable Consul. That means both the application-test.properties and application-default.properties are unnecessary, and we can safely remove them. See Listing 8 for an example of an annotation that points to the test.properties file.

@SpringBootTest
@TestPropertySource("classpath:/test.properties")
class GamificationApplicationTests {

	@Test
	void contextLoads() {
	}

}

Listing 7. Can’t use a default profile to point to an active profile

Summary

That’s all, both the Gateway and the Logs service don’t require changes apart from the pom.xml updates. You can rebuild the Docker images and give a try to the Docker configuration. I also changed the docker-compose.yml file in case you want to see the result.

If you still don’t have the book, I strongly recommend you get your copy now. I keep updating the book’s source code and will be sharing more content around the same use case.

The book's source code with all the updates is available on GitHub: U.2.5.5. While you are there, please give it a star!
Learn Microservices with Spring Boot - Second Edition
Moisés Macero's Picture

About Moisés Macero

Software Developer, Architect, and Author.
Are you interested in my workshops?

Málaga, Spain https://thepracticaldeveloper.com

Comments