Mastodon Icon GitHub Icon LinkedIn Icon RSS Icon

How to convert a Java/Spring project to Kotlin

At work, we have a sizeable RESTful service written in Java, and based on the Spring framework. It is not very complex – it mostly handles convenient access to our database. Nevertheless, it is a significant daily source of pain for me: it is poorly mantained and every new feature is patched hammring code into it.

After almost three years working on it, I registered that 90% of the problems we had were related to something null that should not have been null. After the n-th NullPointerException, I snapped. I needed something better: I decided I would convert the entire project in Kotlin for that reason alone.

Why not just use Optional<T>?

In recent years, Java got much better at almost everything. After years and years of stagnation, Java is now a not-unbearable language to work with. The addition of Stream was excellent but the introduction of the Optional<T> type is probably the best feature of any modern Java implementation.

Optional<T>, in short, wraps around a nullable variable providing type-safe access to that value. If you have Optional<String> foo, you cannot use it where a String is expected: you will first need to unwrap it and, as a consequence, check if the content of foo is not null.

Optional types are not an exoteric feature anymore. If you know Kotlin, Rust, Swift, or any other language designed in this decade, you know what I am talking about.1

The feature is a life-saver, but, unfortunately, it is still a bit clunky. It is not completely integrated into the language and legacy libraries, and converting an old-style Java code into an Optional-Java code pollute your code of a lot of ugly if (foo.isPresent()) checks.2

Kotlin, on the other hand, has been designed with this modern principle in mind and nullable/non-nullalbe types flow straightforwardly into the code. They are less verbose and they are widely supported. If you add to this the fact that the main Kotlin design principle is “painless Java interoperability”, you already know the best way to proceed.

Let’s begin by converting one class

At this point, I had a Java Spring codebase and I needed to configure my build system (Maven) to compile a Java/Kotlin hybrid Spring codebase. I knew it is possible, but I still felt fear and excitement.

Looking around, I found that IntelliJ contains a tool to add Kotlin’s support in your current project. You go in “Tool -> Kotlin -> Configure Kotlin in Project.” It was worth a try.

This guy. This is what used to set up Maven.
Figure 1. This guy. This is what used to set up Maven.

I clicked, IntelliJ started doing its magic and, after a while, Maven was configured for Kotlin. That was easy: I eagerly clicked Build and…

Unfortunately this automatic conversion does not work. At least in Maven and with my version of IntelliJ (2020.1). Somehow, the automation tool is broken and produces a Maven configuration that cannot compile Kotlin and Java code at the same time.

The good news is the solution was easy to find: I went to the official Kotlin documentation and copied the maven-compiler-plugin plugin configuration from the example code an pasted in my pom.xml. For your convenience, I copy the code below.

 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
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.5.1</version>
    <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>

I tried again to compile, this time, everything worked. I was in business.

Add support for Spring

The first thing I did was to convert my Spring entities classes into Kotlin classes (and finally getting rid of all the annoying setter and getter). The built-in conversion tool is very good, even if the result is not really Kotlin-idiomatic. However, for simple classes, it works fine.

After I converted 4/5 classes, I try to compile again. Everything worked. I deployed the artifact to do some real test in the dev environment and… the app did not start. Oh, oh.

In the logs, there were errors like this:

Cannot subclass final class gr.helvia.hbf.core.domain.Tenant

I instantaneously knew the cause: classes in Kotlin are final by default. They cannot be subclassed unless you mark them explicitely as open. Apparently, Spring needs to subclass your classes to do its magic.

This was annoying: I should had go over all the Kotlin entities and add open to their declaration. And what if other classes needed to be subclassed? Do I needed to repeat this try and error forever?

Luckily, there is yet another easy solution. You need to add the allopen plugin. You can find the documentation here. This plugin will automatically and implicitly add open to some specific Spring classes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<configuration>
    <jvmTarget>1.8</jvmTarget>
    <compilerPlugins>
        <plugin>spring</plugin>
        <plugin>all-open</plugin>
    </compilerPlugins>
    <pluginOptions>
        <option>all-open:annotation=org.springframework.data.mongodb.core.mapping.Document</option>
    </pluginOptions>
</configuration>

And now the fun part!

After this point, the only part remaining is the fun part. I proceeded by converting class by class, starting from the more internal and simple one.

The beautiful thing is that you instantaneously get some benefits. For instance, I identified at least 10 potential NullPointerExceptions in the Java implementation.

To spot them is easy: Kotlin is forced to null-assert (!!) every nullable value and, by default, any object in Java is considered nullable. So every now and then, you spot a lot of !! during object access: foo!!.bar!!.gee. This visual clue lets you clearly see when a null access may occur and you can easily see where you should be more careful.

After this, I usually start to removing nullable types from functions that are not supposed to accept null parameters and enjoy see this effect propagate on the codebase.

Conclusion

After a lot of conversion/compilation/testing cycles, 52.9% of the original codebase is now in Kotlin. Except for the small issues reported here, the entire process was quite straightforward, and people working on the front-end never realized this change.

Now working on this application makes me happier, and I made it harder to make silly mistakes. I am pretty satisfied with the result.


  1. Everyone but Go, of course. Go is doomed to repeat every single programming language design mistake of the last 50 years. I am sure that, at some point, Go will have its own non-nullable type. Meanwhile, you can keep if (err != nil)-ing like we were in the 80s. ↩︎

  2. The isPresent call is not the way of using Optional, btw. We can consider it an antypattern. Optional should be used in a more “functional” way with map and the ifPresent method. But this is another story. ↩︎

comments powered by Disqus