In this article, I would like to consider the problems and their solutions, which we encountered during the migration of our small microservice from Java to Kotlin.

Stack

  • Java 11
  • Spring Web MVC (в рамках Spring Boot)
  • Spring Data JPA
  • Map Struct
  • Lombok
  • Maven

Beginning

Firstly, I would recommend anyone, who wants to put Kotlin in your project to start from tests. During this process, we configure almost all you need. You go through these steps:

  • Configure the project.
  • Add necessary libraries.
  • Catch many errors.
  • And, finally, migrate a part of your test.

At the same time, the main code responsible for the business logic is not affected.

Add kotlin

Maven

As usual, we start from https://start.spring.io/.

Let’s compare 2 pom.xml - one for Java and one for Kotlin.

  • Add Kotlin version
1
2
3
4
<properties>
 ....
  <kotlin.version>1.5.31</kotlin.version>
</properties>
  • Add the standard library for Kotlin and Jackson.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<dependency>
  <groupId>com.fasterxml.jackson.module</groupId>
  <artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
  <groupId>org.jetbrains.kotlin</groupId>
  <artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
  <groupId>org.jetbrains.kotlin</groupId>
  <artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
  • Add build section.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<build>
    <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
    <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
    <plugins>
        .....
        <plugin>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-plugin</artifactId>
            <configuration>
                <args>
                    <arg>-Xjsr305=strict</arg>
                </args>
                <compilerPlugins>
                    <plugin>spring</plugin>
                    <plugin>jpa</plugin>
                </compilerPlugins>
            </configuration>
            <dependencies>
                <dependency>
                    <groupId>org.jetbrains.kotlin</groupId>
                    <artifactId>kotlin-maven-allopen</artifactId>
                    <version>${kotlin.version}</version>
                </dependency>
                <dependency>
                    <groupId>org.jetbrains.kotlin</groupId>
                    <artifactId>kotlin-maven-noarg</artifactId>
                    <version>${kotlin.version}</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

More information about kotlin-maven-plugin you can find here.

But this configuration is not appropriate if you want to use Java with Kotlin. In this case you need to configure maven-compiler-plugin (documentation).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<build>
  <plugins>
    .....
    <plugin>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-maven-plugin</artifactId>
      <executions>
        <execution>
          <id>compile</id>
          <goals>
            <goal>compile</goal>
          </goals>
          <configuration>
            <sourceDirs>
              <sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
              <sourceDir>${project.basedir}/src/main/java</sourceDir>
            </sourceDirs>
          </configuration>
        </execution>
        <execution>
          <id>test-compile</id>
          <goals> <goal>test-compile</goal> </goals>
          <configuration>
            <sourceDirs>
              <sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
              <sourceDir>${project.basedir}/src/test/java</sourceDir>
            </sourceDirs>
          </configuration>
        </execution>
      </executions>
      <configuration>
        <args>
          <arg>-Xjsr305=strict</arg>
        </args>
        <compilerPlugins>
          <plugin>spring</plugin>
          <plugin>jpa</plugin>
        </compilerPlugins>
      </configuration>
      <dependencies>
        <dependency>
          <groupId>org.jetbrains.kotlin</groupId>
          <artifactId>kotlin-maven-allopen</artifactId>
          <version>${kotlin.version}</version>
        </dependency>
        <dependency>
          <groupId>org.jetbrains.kotlin</groupId>
          <artifactId>kotlin-maven-noarg</artifactId>
          <version>${kotlin.version}</version>
        </dependency>
      </dependencies>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <executions>
        <!-- Replacing default-compile as it is treated specially by maven -->
        <execution>
          <id>default-compile</id>
          <phase>none</phase>
        </execution>
        <!-- Replacing default-testCompile as it is treated specially by maven -->
        <execution>
          <id>default-testCompile</id>
          <phase>none</phase>
        </execution>
        <execution>
          <id>java-compile</id>
          <phase>compile</phase>
          <goals>
            <goal>compile</goal>
          </goals>
        </execution>
        <execution>
          <id>java-test-compile</id>
          <phase>test-compile</phase>
          <goals>
            <goal>testCompile</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Some information about plugins All-open and No-arg you can find below.

Kotlin plugins

Kotlin uses compiler plugins to change ast-tree during compilation. By default, spring-initializer adds 2 plugins: all-open and no-arg . Also, kapt and the plugin for Lombok are popular.

all-open

By default, all classes in Kotlin are final and they can not be overridden, so Spring can’t use them to create proxy-class. All-open plugin adds open to classes with specified annotations.

For spring there is a pre-configured kotlin-spring plugin. It works only with these annotations:

  • @Component
  • @Async
  • @Transactional
  • @Cacheable
  • @SpringBootTest

If you want to work with your custom annotations, you need to configure kotlin-allopen.

Also, I would like to highlight, that there are no JPA annotations, and you have to add them if you use JPA repositories.

No-arg

This plugin adds an empty constructor to each class. For JPA there is kotlin-jpa. It works with annotations:

  • @Entity
  • @Embeddable
  • @MappedSuperclass

But it doesn’t add open to these classes.

Kapt

It is an adapter for annotation processors (documentation). It is used, for example, by mapstruct to generate code for mappers with Kotlin.

Example, how you can add kapt plugin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<plugin>
  <groupId>org.jetbrains.kotlin</groupId>
  <artifactId>kotlin-maven-plugin</artifactId>
  <version>${kotlin.version}</version>
  <executions>
    <execution>
        <id>kapt</id>
        <goals>
            <goal>kapt</goal>
        </goals>
        <configuration>
            <sourceDirs>
                <sourceDir>src/main/kotlin</sourceDir>
                <sourceDir>src/main/java</sourceDir>
            </sourceDirs>
            <annotationProcessorPaths>
                <annotationProcessorPath>
                    <groupId>org.mapstruct</groupId>
                    <artifactId>mapstruct-processor</artifactId>
                    <version>${mapstruct.version}</version>
                </annotationProcessorPath>
            </annotationProcessorPaths>
        </configuration>
    </execution>
  ...
  </executions>
  ....
</plugin>

In my experience, this plugin can break your build, if you use Lombok because the plugin can’t parse java-file where Lombok methods are used.

Lombok plugin

This plugin is used to cope with this problem, but it still has only beta versions and doesn’t support @Builder.

Cook Spring

Spring is an awesome framework, and, of course, it supports Kotlin. But there are some interesting features.

Some presentations:

Features

  • the main entry point with Kotlin:

    1
    2
    3
    4
    5
    6
    
    @SpringBootApplication
    class DemoKotlinApplication
    
    fun main(args: Array<String>) {
        runApplication<DemoKotlinApplication>(*args)
    }
    
  • some new extension-methods for Kotlin were added, like RestOperationsExtensions.kt

  • it is recommended to use val arguments with constructor.

    1
    2
    3
    4
    5
    
    @Component
    class YourBean(
    private val mongoTemplate: MongoTemplate, 
    private val solrClient: SolrClient
    )
    

    but there are some other options, for example, you can use latenin :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    @Component
    class YourBean {
    
        @Autowired
        lateinit var mongoTemplate: MongoTemplate
    
        @Autowired
        lateinit var solrClient: SolrClient
    }
    

    It is similar to

    1
    2
    3
    4
    5
    6
    7
    
    @Component
    public class YourBean {
    @Autowired
    public MongoTemplate mongoTemplate;
    @Autowired
    public SolrClient solrClient;
    }
    

    Also, you can use injection with set-methods:

    1
    2
    3
    4
    5
    6
    
    var hello: HelloService? = null
        @Autowired
        set(value) {
            field = value
            println("test")
        }
    
  • You can create properties classes with @ConstructorBinding.

    1
    2
    3
    4
    5
    
    @ConfigurationProperties("test")
    @ConstructorBinding
    class TestConfig(
        val name:String
    ) 
    

    or with lateinit

    1
    2
    3
    4
    
    @ConfigurationProperties("test")
    class TestConfig {
        lateinit var name: String
    }
    
  • if you want to generate meta-information, you should configure spring-boot-configuration-processor with kapt.

Kotlin with Hibernate

The main errors and problems are described in the Haulmont article.

Once again, I will draw your attention that you need to configure no-args and all-open plugins and implement hashCode and equals methods.

Kotlin with Jackson

You should add Jackson Module Kotlin in your project. After that, you can’t specify a type of object explicitly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue

data class MyStateObject(val name: String, val age: Int)

...
val mapper = jacksonObjectMapper()

val state = mapper.readValue<MyStateObject>(json)
// or
val state: MyStateObject = mapper.readValue(json)
// or
myMemberWithType = mapper.readValue(json)

Kotlin with MapStruct

MapStruct works through the annotation processor. Therefore, it is necessary to configure kapt correctly. At the same time, if models use Lombok annotations, then there could be problems with mappers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<plugin>
  <groupId>org.jetbrains.kotlin</groupId>
  <artifactId>kotlin-maven-plugin</artifactId>
  <version>${kotlin.version}</version>
  <executions>
    <execution>
        <id>kapt</id>
        <goals>
            <goal>kapt</goal>
        </goals>
        <configuration>
            <sourceDirs>
                <sourceDir>src/main/kotlin</sourceDir>
                <sourceDir>src/main/java</sourceDir>
            </sourceDirs>
            <annotationProcessorPaths>
                <annotationProcessorPath>
                    <groupId>org.mapstruct</groupId>
                    <artifactId>mapstruct-processor</artifactId>
                    <version>${mapstruct.version}</version>
                </annotationProcessorPath>
            </annotationProcessorPaths>
        </configuration>
    </execution>
  ...
  </executions>
  ....
</plugin>

Kotlin with Lombok

Honestly, it is a pain. Of course, you can try to use Lombok compiler plugin, but often if you use Kapt and Lombok at the same time, you may encounter many problems. By default, kapt uses all annotation processors and disables their work with javac, but for lombok it doesn’t work correctly and you need to disable this behavior.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.5.1</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <annotationProcessorPaths>
            <annotationProcessorPath>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </annotationProcessorPath>
        </annotationProcessorPaths>
    </configuration>
</plugin>

At the same time, Lombok works correctly with kapt only if the annotation processor, which is used by kapt, does not depend on Lombok. In our case, it was not true. It was the reason, why we had to translate the entire domain model to Kotlin in the first step. Also one of the options is to use Delombok.

Kotlin with Mockito

Mockito does not work correctly with Kotlin types out of the box. Spring recommends using Mockk. There is also a special module for Mockito, which adds support for Kotlin - Mockito-Kotlin.

In our project we used Mockito-Kotlin. We found only one problem: you need to be careful, because many methods are duplicated in different modules, for example, any() will now be in 2 places - org.mockito.kotlin and org.mockito.Mockito .

Kotlin and logging

We chose kotlin-logging. It is a really convenient library and you can use it like this:

1
2
3
4
5
6
7
8
import mu.KotlinLogging
private val logger = KotlinLogging.logger {} 
class FooWithLogging {
    val message = "world"
    fun bar() {
        logger.debug { "hello $message" }
    }
}

Conclusions

I would like to finish the article with brief conclusions. Using Java and Kotlin together in one project requires additional settings, but almost everything is solved and we get the opportunity to use 2 languages in one project. The biggest problem for us was incompatibility Lombok and Kotlin.

Reference https://dev.to/pyltsinm/from-java-to-kotlin-there-and-back-again-5h8a