pyltsin
Posted on April 1, 2022
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>
- 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>
- 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>
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>
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>
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)
}
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
)
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
}
It is similar to
@Component
public class YourBean {
@Autowired
public MongoTemplate mongoTemplate;
@Autowired
public SolrClient solrClient;
}
Also, you can use injection with set-methods:
var hello: HelloService? = null
@Autowired
set(value) {
field = value
println("test")
}
- You can create properties classes with
@ConstructorBinding
@ConfigurationProperties("test")
@ConstructorBinding
class TestConfig(
val name:String
)
or with lateinit
@ConfigurationProperties("test")
class TestConfig {
lateinit var name: String
}
- if you want to generate meta-information, you should configure
spring-boot-configuration-processor
withkapt
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)
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>
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>
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" }
}
}
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
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
November 8, 2023