I recently had a chance to help one of our engineers new to both Spring and Kotlin - New even to the Java ecosystem itself - with a problem they encountered while trying to wire up some objects using Spring (Boot).

The problem was a familiar friend, the NullPointerException. One of the benefits that Kotlin can provide an engineering organization is a substantial reduction in the occurrences of this particular error. It offers several language constructs designed to help you avoid this:

  1. Types that can hold null values need to explicitly identify themselves as such using the ? token. For example, var myField: String? defines a field that can hold a null string. The compiler will not allow you to assign null to a type that does not explicitly define itself as such.
  2. You can conditionally access methods and fields on nullable types using the ?. operator. service?.helloWorld() will only call the helloWorld function if the service itself is not null.
  3. It encourages you to use immutable instances at every opportunity using the val keyword. For example, val myField = "Hello, World!" defines an immutable string. You know it's reference can never be null.
  4. Even when dealing with nullable types, you must explicitly write code that tells the compiler, "I know what I'm doing," via the !! operator.

Even with these language safety features, when you start adding application development frameworks into the mix, such as Spring, it remains easy to shoot yourself in the foot as a beginner. One of the easiest ways to do this is to misuse @Autowired properties by mixing them with the Kotlin init blocks.

Spring property injection

There are several ways to allow Spring to inject dependencies using its Dependency Injection (DI) / Inversion of Control (IoC) features. One that is frequently used is the use of @Autowired attached to class properties (other options, such as @Resource and @Inject are available, which suffer the same problems).

When a property has this annotation, Spring will construct the object and then assign values to all the properties.

@Component
class MyService {
    @Autowired val dependency: MyDependency
}

You can roughly imagine Spring doing the equivalent of the following:

val service = MyService()
service.dependency = MyDependency()

Thankfully, because of Kotlin's null safety features, this class definition does not compile since the val keyword defines an immutable property, and Kotlin does not understand that Spring will assign this value at some point.

e: file:///home/marka/src/blog/autowired/src/main/kotlin/MyService.kt:7:5 Property must be initialized or be abstract

If the developer is using Jetbrain's IntelliJ IDE, they may follow it's guidance and make the property nullable, assign null to it, and let spring override it later (using reflection).

@Component
class MyService {
    @Autowired val dependency: MyDependency? = null
}

Alternatively they may try and work around this problem using one of Kotlin's "get out of jail" cards, late initialization of properties and variables using the lateinit keyword.

@Component
class MyService {
    @Autowired lateinit var dependency: MyDependency
}

Note that when using lateinit you must also declare the property as mutable using the var keyword since the variable holds a null value until it has been assigned.

Sure enough, the code successfully compiles in this case, and you have obtained a false sense of security and achievement since you've opened the door to a NullPointerException.

Kotlin initializer blocks

One common way to execute code during the initialization of a class in Kotlin is to use an initializer block. Since Kotlin default constructors cannot contain code, initializer blocks allow you to execute code immediately after constructing an object and before control is handed back to the caller.

@Component
class MyService {
    @Autowired lateinit var dependency: MyDependency
    
    init {
        print("Hello, World!")
    }
}

This prints to the console "Hello, World!" when an instance of the class is constructed.

If you recall from earlier that Spring property injection in Spring occurs after it has created an instance of your class, you may, at this point, see where things are going. Since the initializer block is executed immediately if you try and use your dependency inside the initializer block, you are forced to tell the Kotlin compiler, "I know what I am doing," and use the !! operator.

@Component
class MyService {
    @Autowired lateinit var dependency: MyDependency
    
    init {
        dependency.sayHello()
    }
}

And sure enough, you have successfully bypassed the Kotlin null safety features, and the spring application will fail to start.

Failed to instantiate [MyService]: Constructor threw exception

A poor workaround

Since you already use Spring, you can lean on Spring to execute your additional initialization.

Jakarta provides a @PostConstruct annotation. Spring will call any class method annotated as such after all properties have been initialized by Spring.

@Component
class MyService {
    @Autowired lateinit var dependency: MyDependency

    @PostConstruct
    fun postConstruct() {
        dependency.sayHello()
    }
}

This is an unsatisfactory solution to the problem for several reasons.

  1. Your class's contract with the callers is unclear since the dependency requirement is hidden. (how would a consumer of your class know they need to both set the dependency property and call your postConstruct method)
  2. It limits your ability to use Kotlin's built-in standard functionality and forces you to use something very framework specific.
  3. If you are using Jetbrains' IntelliJ IDE you will forever have a persistent yellow squiggle under dependency telling you that the variable hasn't been initialized, even though it has been.

Using constructor injection for safety and transparency

The easiest way to ensure the safety and transparency of your class's contract with its callers is to force callers to provide all its dependencies using a constructor.

@Component
class MyService(private val dependency: MyDependency) {
    init {
        dependency.sayHello()
    }
}

This ensures safety by ensuring the following:

  1. Your class can never be in a partially initialized state.
  2. You can maintain Kotlin's null safety protections by declaring that the constructor parameter is immutable and cannot be null.
  3. You can use Kotlin's standard class initialization language features without worry of accessing uninitialized properties.

It improves transparency by ensuring:

  1. Users of your class know precisely what dependencies they need to provide for your class to function correctly.
  2. The user of your class does not need to know about non-standard initialization methods. They need to call when constructing the class.

Using constructor injection subjectively makes your code feel tidier and clearer to read.

Recap

In this post, we discussed how Spring late initialization of properties via the @Autowired reduces the effectiveness of some of the null safety features provided by the Kotlin language. We also discussed how it reduces the transparency of your classes contract with it's callers.

Whenever I work with software engineers on Spring based projects, I try to steer them away from using Autowired properties since they can cause general confusion and lack of clarity within codebases.

The problems we have discussed here also affect any other JVM language, and are not specific to Kotlin, the problems encountered cut a bit deeper when encountered with a language like Kotlin, and it's built in safety features.

We're always on the lookout for backend Kotlin engineers. If you are interested in working with us on Kotlin based projects, please head over to our Careers page to understand more about the company and check out our currently open positions.

Author: Mark Allanson