From Java to Kotlin. There and back again

pyltsinm

pyltsin

Posted on April 1, 2022

From Java to Kotlin. There and back again

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
<properties>
 ....
  <kotlin.version>1.5.31</kotlin.version>
</properties>

Enter fullscreen mode Exit fullscreen mode
  • Add the standard library for Kotlin and Jackson
<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>
Enter fullscreen mode Exit fullscreen mode
  • Add build section
    <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>
Enter fullscreen mode Exit fullscreen mode

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)

<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>
Enter fullscreen mode Exit fullscreen mode

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:

<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>
Enter fullscreen mode Exit fullscreen mode

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:
@SpringBootApplication
class DemoKotlinApplication

fun main(args: Array<String>) {
    runApplication<DemoKotlinApplication>(*args)
}
Enter fullscreen mode Exit fullscreen mode
  • some new extension-methods for Kotlin were added, like RestOperationsExtensions.kt

  • it is recommended to use val arguments with constructor

@Component
class YourBean(
  private val mongoTemplate: MongoTemplate, 
  private val solrClient: SolrClient
)
Enter fullscreen mode Exit fullscreen mode

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

@Component
class YourBean {

    @Autowired
    lateinit var mongoTemplate: MongoTemplate

    @Autowired
    lateinit var solrClient: SolrClient
}
Enter fullscreen mode Exit fullscreen mode

It is similar to

@Component
public class YourBean {
   @Autowired
   public MongoTemplate mongoTemplate;
   @Autowired
   public SolrClient solrClient;
}
Enter fullscreen mode Exit fullscreen mode

Also, you can use injection with set-methods:

    var hello: HelloService? = null
        @Autowired
        set(value) {
            field = value
            println("test")
        }
Enter fullscreen mode Exit fullscreen mode
  • You can create properties classes with @ConstructorBinding
@ConfigurationProperties("test")
@ConstructorBinding
class TestConfig(
    val name:String
) 
Enter fullscreen mode Exit fullscreen mode

or with lateinit

@ConfigurationProperties("test")
class TestConfig {
    lateinit var name: String
}
Enter fullscreen mode Exit fullscreen mode
  • 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.

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)
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

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:

import mu.KotlinLogging
private val logger = KotlinLogging.logger {} 
class FooWithLogging {
    val message = "world"
    fun bar() {
        logger.debug { "hello $message" }
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

References

💖 💪 🙅 🚩
pyltsinm
pyltsin

Posted on April 1, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related