Thursday, October 13, 2022

Migrating Java 17 Spring Boot 2.7.3 application to Kotlin 1.7.20

Introduction

This blogpost describes the challenges encountered when migrating a Java 17 Spring Boot 2.7.3 application to Kotlin 1.7.20. 

Other libraries/tools used in the project:
- Swagger (OpenAPI 3.0.3)
- Spring boot 2.7.3
- Liquibase
- H2 in mem + file based
- JUnit5 with Mockito and Mockito-Kotlin
- MySql 8.0
- Actuator
- Maven 3
Tip: after migration of most of the .java files manually, I found this Spring tutorial which helps to avoid having to add 'open' to @Component, @Service etc annotated classes.
It includes the kotlin-spring-plugin and Kotlin JPA plugin to support Kotlin features better, including support for JSR-305 annotations + Spring nullability annotations. Usually you'd also want to include jackson-module-kotlin for serialization and deserialization of Kotlin classes.
Note that my application uses generated Java classes from an OpenAPI 3 Swagger yaml file, which are returned in the REST API, so therefore not needed.

Total code reduction after migration:
Java: 4718
Kotlin: 2860

So about 44% reduction in code. Not bad.

Steps

Convert POJOs

As first I converted a simple POJO which in my case had @Data and @Builder Lombok annotations.
Open that POJO Java file. Hit ctrl-alt-shift-k. Or, find it in the IntelliJ 'actions' search panel:




Then make sure to rebuild to project by enforcing Maven to rebuild if it didn't do that.
Then I had to redo it on the POJO class.

I was a bit surprised what came out:

    @Builder
    @Data
    class Ingredient {
        private val name: String = null
        private val recipeId: UUID = null
        private val createdAt: LocalDateTime = null
        private val updatedAt: LocalDateTime = null
    }

I would have expected the Lombok annotations to be gone. But on the other hand IntelliJ can't know how to fix them I guess (see below for more on Lombok migration).
And maybe because of the @Data it made all fields private...

I did do expect more like this:

    @Builder
    @Data
    class Ingredient(val name: String, recipeId: UUID, createdAt: LocalDateTime, 
                        updatedAt: LocalDateTime)

But even when I remove the Lombok annotations, still the fields are created as private fields, not as part of the primary constructor... Maybe because of the other annotations on some of the fields, like @Id and @Version?

I manually converted it some more, into this:

    data class Ingredient(val name: String, val recipeId: UUID, val createdAt: LocalDateTime, val         
                        updatedAt: LocalDateTime)

Then I converted all uses of the Builder in the Java class to the regular (primary) constructor of the Kotlin data class. E.g:

    new Ingredient(ingredient, recipeId, createdAt, createdAt)

Doing this for all POJOs would be quite some work. And later on, you want to convert those Java instance creations to Kotlin constructors anyway, with named parameters.
So I didn't do this for all classes, I started to skip this step of replacing builders with constructors.

Now first let's try to rebuild the project with 'mvn clean install' for example.

That gave an error: the newly created Kotlin Ingredient class (symbol) could not be found.   Note that IntelliJ was able to find all dependencies just fine.
The answer to that can be found here
I applied the solution where you move your .kt file into its new src/main/kotlin/x/y/z package. Make sure to mark that src/main/kotlin directory as a source directory in IntelliJ.

But still an error: 

    Cannot find symbol (Ingredient)

So that didn't fix it, so applied the accepted solution, so to make sure the compilation order is Kotlin then Java.
After this change in the pom.xml, IntelliJ couldn't find the Spring Boot application class anymore. 'mvn clean install' ran up to the tests, but many failed due to:

    java.lang.NoClassDefFoundError: kotlin/reflect/full/KClasses 

See next section below for how those were fixed.
I changed also in the Kotlin plugin in the pom.xml the JVM target to: <jvmTarget>1.17</jvmTarget>
After that, IntelliJ compiled fine again.

Then trying to run the application with 'mvn spring-boot:run' gave this error:

    Compilation failure
    Unknown JVM target version: 1.17
    Supported versions: 1.6, 1.8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18

So the <jvmTarget> needs to be 17 (not 1.17) apparently. And then it almost started, but I got the same error as when running the tests:

    java.lang.ClassNotFoundException: kotlin.reflect.full.KClasses

Note also this warning showed up during the build, that needs to be checked and fixed, because it refers to Kotlin JDK8 and we use Java 17:

    [INFO] Scanning for projects...
    [WARNING] 
    [WARNING] Some problems were encountered while building the effective model for com.project:kotlinrecipes:jar:1.0.0
    [WARNING] 'dependencies.dependency.(groupId:artifactId:type:classifier)' must be unique: org.jetbrains.kotlin:kotlin-stdlib-jdk8:jar -> duplicate declaration of version ${kotlin.version} @ line 159, column 15
    [WARNING] 
    [WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.


Fixing the Java tests Part 1

Fixing the tests with the error java.lang.ClassNotFoundException: kotlin.reflect.full.KClasses

Adding this dependency, as also mentioned here, fixed it:

<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
</dependency>

Outstanding question for this stdlib dependency for me was, why is Kotlin stdlib using Java 8?

<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>   // Can't this be Java 17?
<version>${kotlin.version}</version>
</dependency>

And: the Kotlin standard library kotlin-stdlib targets Java 6 and above. There are extended versions of the standard library that add support for some of the features of JDK 7 and JDK 8.
And when you include kotlin-stdlib-jdk8, it will pull kotlin-stdlib-jdk7 and kotlin-stdlib
And also note: https://stackoverflow.com/questions/65731542/why-is-there-no-kotlin-stdlib-jdk11. So basically, all fine since Kotlin just doesn't use any API from Java's JDK higher than 8.

After this, the application started fine, connected to the MySql Docker instance and the REST endpoints worked all fine.

Migrating the Spring Boot application class

The IntelliJ converter worked fine. The @Bean annotation in that class was migrated correctly too. Constants were correctly put in a companion object {} block.
But when starting the application this message showed up:

    org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: @Configuration class 'RecipesApplication' may not be final. Remove the final modifier to continue.

Strange: it says the class might not be final, remove the final modifier... Sounds contradicting!
I made the class 'open' (because the default is public final) and then it worked.

Then for the @Bean I got:

    org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: @Bean method 'encoder' must not be private or final; change the method's modifiers to continue

I made the method 'encoder' also 'open' and then it worked.

If you have other issues, check this post for more tips. 

Migrating a Spring Boot @RestController

I applied the IntelliJ converter. Its result was pretty good. Inheritance from the OpenAPI 3 generated Java code was correctly applied. 
I had to add the Kotlin logging to replace the @Slf4j static logger, see here.
Note the default static 'log' field generated by @Slf4j is after applying that named private val logger = KotlinLogging.logger {}. But you can name it as you want of course.

When running the application, again the controller also had to be made open, to allow it to be subclassed (as can be seen in the tip in the introduction section, Spring and some other frameworks require classes to be open (extendable)).
After this change, the controller worked fine.

I also modified the generated code a bit by adding a @NotNull annotation (for something which had already a '?' so was incorrect anyway I realized afterwards), but then at runtime only I sadly got this error:

    javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method UserController#loginUser(JwtRequest) redefines the configuration of UsersApi#loginUser(JwtRequest).

Removing the incorrectly added @NotNull fixed that problem; and it is unnecessary in combination with the '?' too anyway.

Migrating @Component service

Got this error after adding the logger:

    java.lang.NullPointerException: Cannot invoke "mu.KLogger.info(String)" because "this.logger" is null

Strange, the @RestController did not have that issue. Though that one is overriding a (generated OpenAPI 3) class.
This question triggered me, so I added the 'open' keyword to the methods in the @Component class, to make it non-final so Spring has access to it with its proxies too.

That worked.

Converting Spring @Repository

For interfaces, the question was: a findByUsername(username) can return null when it doesn't find the user. How to best define that in Kotlin? Allow null to be returned (but at least the caller then has to handle the null possibility)? Or use Optional in that case? Or is there another better solution?
No clear best answer to me, e.g: https://discuss.kotlinlang.org/t/how-to-deal-with-database-null-return-according-kotlin-null-safety-feature/2546
I went for having the repo return '?'. But another repo already was using Optional, so left that there too. Will have to decide on consistency here...
See this post on how null can be seen positively and also the String.toIntOrNull() function extension built into Kotlin! :) Based on that, I went for allowing repository functions to return 0.
 

Converting @MappedCollection(idColumn = "RECIPE_ID")

Only had to make sure using the arrayOf() and using a MutableSet because you usually want to add elements to the child (collection):

        @OneToMany(cascade = arrayOf(CascadeType.ALL), orphanRemoval = true, 
                    targetEntity = Ingredient::class)
        @JoinColumn(name = "recipe_id")
        var ingredients: MutableSet<Ingredient> = HashSet<Ingredient>(),

Use @NotNull or not

Is it useful to have the @NotNull annotation, while it is only done at runtime? And doesn't Kotlin also throw an exception when you pass in a null value to a parameter that is already non-nullable by default (i.e it doesn't have the '?' appended to its type)?
Seems like double because Kotlin inserts the @NotNull into the code
Maybe you'd put it in when Java code is calling your Kotlin code, to make it more explicit - and the Java code can then validate on it?

Converting @Configuration class

A class annotated with that has also to be open: 

    @Configuration
    internal open class JdbcConfig : AbstractJdbcConfiguration() {...}

I noticed the Kotlin converter from IntelliJ didn't like always comments at the end of a line of Java code. Sometimes the '()' of a method call were then put on the wrong line.

Fixing the Java tests Part 2

While still as Java code, some failed with this:

    org.mockito.exceptions.base.MockitoException: 
    Cannot mock/spy class com.project.kotlinrecipes.user.infra.JwtUserDetailsServiceImpl
    Mockito cannot mock/spy because :
     - final class

So that was easy, I made those classes 'open'.

I also had to make methods 'open' used in Mockito.when() matchers, because otherwise it complains: 

    org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
    Invalid use of argument matchers!
    0 matchers expected, 1 recorded:

But also verify() started to fail:

    verify(jwtTokenUtil, Mockito.times(0)).validateToken(isA(String.class), isA(UserDetails.class));
    java.lang.NullPointerException: Parameter specified as non-null is null: method
    com.project.kotlinrecipes.infra.security.JwtTokenUtil.validateToken, parameter userDetails

That also meant added the 'open' keyword to that method.

Converting JUnit 5 tests with Mockito to Kotlin

IntelliJ's auto-converter works pretty good. Except that it converts

    private JdbcIngredientRowMapper jdbcIngredientRowMapper;

    @BeforeEach
    public void setUp() {
        jdbcIngredientRowMapper = new JdbcIngredientRowMapper();
    }

to:

    private var jdbcIngredientRowMapper: JdbcIngredientRowMapper? = null

    @BeforeEach
    fun setUp() {
        jdbcIngredientRowMapper = JdbcIngredientRowMapper()
    }

But it can be made nullsafe by changing it to:

    private lateinit var jdbcIngredientRowMapper: JdbcIngredientRowMapper
    
    @BeforeEach
    fun setUp() {
        jdbcIngredientRowMapper = JdbcIngredientRowMapper()
    }

I also introduced the `test description` notation, e.g:

    @Test
    fun `Should map row`() {}

I use in some tests:

    @ParameterizedTest
    @MethodSource("filterNullPermutations")

That filterNullPermutations has to be a static method. IntelliJ made it a companion object with that method 'private'. I added @JvmStatic to make it accessible for the @MethodSource.

I had to add mockito-kotlin library for better interoperability for this case:  
ArgumentMatchers.isA(class) for Kotlin methods that don't allow null values be put in (which isA() and any() for example can return).
And by adding this library I could now also use 'whenever' instead of '`when`'.

isA(MyClass.class) in Java had to be converted to:  

    whenever(jwtTokenUtil.generateToken(isA<UserDetails>))).thenReturn(BEARER_TOKEN_VALUE)

Or even shorter:

       whenever(jwtTokenUtil.generateToken(isA())).thenReturn(BEARER_TOKEN_VALUE)

using the mockito-kotlin library, which creates an instance (instead of the regular mockito which can return null). See here for more explanation. 

Replaced all mock() with Kotlin style: val mockBookService : BookService = mock()

The generated code did not work for this TestRestTemplate.exchange() call in Java:

        // Set up find parameters
        Map<String, String> uriVariables = new HashMap<>();
        uriVariables.put("vegetarian", "true");
        uriVariables.put("numberOfServings", "3");
        uriVariables.put("includedIngredients", "onion");
        uriVariables.put("excludedIngredients", "fish");
        uriVariables.put("instructions", "First");

        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
        HttpEntity<?> entity = new HttpEntity<>(headers);

        // When
        ResponseEntity<List<RecipeResponse>> foundRecipesEntity = testRestTemplate.exchange("/recipes/findByFilter?vegetarian={vegetarian}&numberOfServings={numberOfServings}&includedIngredients={includedIngredients}&excludedIngredients={excludedIngredients}&instructions={instructions}",
                HttpMethod.GET, entity, new ParameterizedTypeReference<>() {}, uriVariables);


Became after IntelliJs converter applied:

        // Set up find parameters
        val uriVariables: MutableMap<String, String?> = HashMap()
        uriVariables["vegetarian"] = "true"
        uriVariables["numberOfServings"] = "3"
        uriVariables["includedIngredients"] = "onion"
        uriVariables["excludedIngredients"] = "fish"
        uriVariables["instructions"] = "First"
        val headers = HttpHeaders()
        headers[HttpHeaders.ACCEPT] = MediaType.APPLICATION_JSON_VALUE
        val entity: HttpEntity<*> = HttpEntity<Any>(headers)

        // When
        val foundRecipesEntity: ResponseEntity<List<RecipeResponse>> =
            testRestTemplate.exchange<List<RecipeResponse>>("/recipes/findByFilter?vegetarian={vegetarian}&numberOfServings={numberOfServings}&includedIngredients={includedIngredients}&excludedIngredients={excludedIngredients}&instructions={instructions}",
                HttpMethod.GET, entity, object : ParameterizedTypeReference<List<RecipeResponse?>?>() {}, uriVariables
            )

But .exchange() was red underlined, no matching method to invoke found. I had to change it into this:

        // Set up find parameters
        val uriVariables: MutableMap<String, String> = HashMap()
        uriVariables["vegetarian"] = "true"
        uriVariables["numberOfServings"] = "3"
        uriVariables["includedIngredients"] = "onion"
        uriVariables["excludedIngredients"] = "fish"
        uriVariables["instructions"] = "First"
        val headers = HttpHeaders()
        headers[HttpHeaders.ACCEPT] = MediaType.APPLICATION_JSON_VALUE

        // When
        val foundRecipesEntity: ResponseEntity<List<RecipeResponse>>? =
            testRestTemplate.exchange(
                "/recipes/findByFilter?vegetarian={vegetarian}&numberOfServings={numberOfServings}&includedIngredients={includedIngredients}&excludedIngredients={excludedIngredients}&instructions={instructions}",
                HttpMethod.GET, HttpEntity("parameters", headers),
                typeReference<List<RecipeResponse>>(), uriVariables
            )

With the typeReference method added (you can also do it inline BTW):

    private inline fun <reified T> typeReference() = object : ParameterizedTypeReference<T>() {}

And after some more cleaning up this worked too (can you spot the differences with the generated Kotlin from IntelliJ?):

        // Set up find parameters
        val uriVariables: MutableMap<String, String> = HashMap()
        uriVariables["vegetarian"] = "true"
        uriVariables["numberOfServings"] = "3"
        uriVariables["includedIngredients"] = "onion"
        uriVariables["excludedIngredients"] = "fish"
        uriVariables["instructions"] = "First"
        val headers = HttpHeaders()
        headers[HttpHeaders.ACCEPT] = MediaType.APPLICATION_JSON_VALUE
        val entity: HttpEntity<*> = HttpEntity<Any>(headers)

        // When
        val foundRecipesEntity: ResponseEntity<List<RecipeResponse>> =
            testRestTemplate.exchange(
                "/recipes/findByFilter?vegetarian={vegetarian}&numberOfServings={numberOfServings}&includedIngredients={includedIngredients}&excludedIngredients={excludedIngredients}&instructions={instructions}",
                HttpMethod.GET, entity,
                typeReference<List<RecipeResponse>>(), uriVariables
            )


Note that MockK can be the next improvements to the Kotlin code, to allow more Kotlin-style of notation.

Method documentation generation

I noticed my IntelliJ 2022.2.1 does not generate @param, @return etc documentation when typing /** above a (private) function definition.

Miscellaneous

I still have a few references to Java classes in the code, like this one:

        httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter::class.java)

Could not find a way to avoid having to reference a Java class in Kotlin this directly.

And at the end of the process I removed all Lombok annotations and its dependency in the pom.xml.

Bonus tip

Spring's Kotlin extensions overview can be found here.


No comments: