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.
❗ Warning
I am lucky. At work I am the guy in charge of deciding the technical stack. Therefore, I can assume the risk of playing and spending time messing with the core application of our business. If you are not, it is always a bad idea to switch technology on an impulse. Really. Do your research first! Talk with who is in charge.
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.
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.
|
|
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.
ℹ️ Info
If you are using MongoDB the allopen
spring
configuration is not enough. You also need to open all the classes annotated with the Document
decorator. If you need so, you can copy my 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.
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. ↩︎The
isPresent
call is not the way of usingOptional
, btw. We can consider it an antypattern. Optional should be used in a more “functional” way withmap
and theifPresent
method. But this is another story. ↩︎