Spring Boot with Kotlin fails with "Parameter specified as non-null is null" when you specify default parameters for method arguments and these arguments are passed from class constructor and constructor arguments are private.
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.stereotype.Repository
import javax.annotation.PostConstruct
data class Token(val value: String)
@Configuration
class TokenConfig {
@Bean fun getToken() = Token("1952f7d5a300")
}
@Repository
// if you remove private, all works fine
class TokenRepo(private val defaultToken: Token) {
fun getToken(token: Token = defaultToken) = token
}
@SpringBootApplication
class Application(private val tokenRepo: TokenRepo) {
@PostConstruct
fun start() {
val token = tokenRepo.getToken()
println("My token: $token")
}
}
fun main(args: Array<String>) {
runApplication<Application>(*args)
}
Stacktrace
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method com.example.app.TokenRepo.getToken, parameter token
at com.example.app.TokenRepo.getToken(Main.kt)
at com.example.app.TokenRepo$$FastClassBySpringCGLIB$$edca23c2.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:750)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)
at com.example.app.TokenRepo$$EnhancerBySpringCGLIB$$ccfc06be.getToken(<generated>)
at com.example.app.TokenRepo.getToken$default(Main.kt:52)
at com.example.app.Application.start(Main.kt:59)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:389)
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:333)
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:157)
... 23 common frames omitted
Spring Boot version: 2.2.0.M2 Kotlin version: 1.3.31
Comment From: shenliuyang
class TokenRepo( val defaultToken: Token) {
fun getToken(token: Token = defaultToken) = token
}
remove private can fix it.
Comment From: bedla
I also have similar issuse with NPE when calling dependant bean.
Suppose @AsyncEnabled
is properly configured. We have two variants
- Case 1 private val
on field where first call fails on NPE
exception thrown
http-nio-8080-exec-1 : callSync
-> thrown NPE
task-2 : callAsync
-> return OK
- Case 2
protected open val
on field where both calls are OK
http-nio-8080-exec-1 : callSync
task-1 : callSync -> task
-> return OK
task-2 : callAsync
-> return OK
Seems that it is because invalid CGLIB proxy is generated at first case.
open class AsyncClass(
private val taskExecutor: TaskExecutor
// protected open val taskExecutor: TaskExecutor
) {
fun callSync() {
logThread("callSync")
taskExecutor.execute {
logThread("callSync -> task")
}
}
@Async
open fun callAsync() {
logThread("callAsync")
}
private fun logThread(message: String) {
println("${Thread.currentThread().name} : $message")
}
}
Comment From: shenliuyang
Hi. this problem is that. default value on JVM actually invoke synthetic method. Without private generated code is
public static Token getToken$default(TokenRepo var0, Token var1, int var2, Object var3) {
if (var3 != null) {
throw new UnsupportedOperationException("Super calls with default arguments not supported in this target, function: getToken");
} else {
if ((var2 & 1) != 0) {
var1 = var0.getDefaultToken();
}
return var0.getToken(var1);
}
}
with private generated code is
// $FF: synthetic method
public static Token getToken$default(TokenRepo var0, Token var1, int var2, Object var3) {
if (var3 != null) {
throw new UnsupportedOperationException("Super calls with default arguments not supported in this target, function: getToken");
} else {
if ((var2 & 1) != 0) {
var1 = var0.defaultToken;
}
return var0.getToken(var1);
}
}
Spring CGLIB proxy invoke like this (proxyInstance) -> (orignalInstance) when you invoke TokenRepo.getToken without parameter , actually invoke getToken$default(proxyInstance) so you got Null value. Finally U should remove all 'private' field when use it as default value. and controlled by spring Sorry for my poor English
Comment From: shenliuyang
@bedla add open to you method callSync will fix you problem. because spring cannot proxy final method. Use 'plugin.spring' plugin for (gradle, maven) also can solve it .
Comment From: bedla
@shenliuyang I already have kotlin compiler plugin for spring and it does not solve it. It works only with @Component
on bean class that I have intentionally did not used.
My point with this issue is that you can change behavior of cglib proxy byt changing how property taskExecutor
is declared.
It is similar like open
on method you suggested and you are right it is about final
modifier on callSync
method.
Reason why it is "working" in second case is because then protected open
is used Kotlin generated following bytecode (as you can see getTaskExecutor
method is ready to be proxied by cglib). But in first case it is usual field access inside final
method which is currently unsolvable by cglib.
public final void callSync() {
this.logThread("callSync");
this.getTaskExecutor().execute((Runnable) (new Runnable() {
public final void run() {
com.example.kotlincglibopen.AsyncClass.this.logThread("callSync -> task");
}
}));
}
@NotNull
protected TaskExecutor getTaskExecutor() {
return this.taskExecutor;
}
Comment From: shenliuyang
@bedla This problem also exist in java, if you define a final method use a private field. Create a custom Annotation annotate on AsyncClass and use allOpen { annotation("you.custom.Annotation") } all-open-plugin And i have a bit confusing, without any of (Component,Service,Controller,Repository) annotation on AsyncClass , Spring will do not any magic for it. Why it proxy by CGLIB.
Comment From: bedla
kotlin related issue https://youtrack.jetbrains.com/issue/KT-28586
Comment From: cypressious
Just got bit by this and removing private
from the constructor parameter as recommended in https://github.com/spring-projects/spring-framework/issues/22948#issuecomment-493376050 is what helped.
This is really weird. The class is annotated with @Service
and the kotlin-spring
plugin is doing its job since everything else is working as intended.
Comment From: shenliuyang
@cypressious Hi there is no magic, kotlin-spring just make class 's func open that annotated @Service @Repository ext . In java if you declare a final method in you service, and use private field in this method. you will get NPE. default parameter in kotlin is managed by invoker, get private default parameter the only way is generate synthetic method.
Spring cglib proxy proxyInstance->realInstance. proxyInstance created by Unsafe. (did not initilizer any field, all you field is null include final field)
Time is fly , one year ago , my English bad every day. hahahaha
Comment From: sdeleuze
Hi, sorry for the delay. I can still reproduce with Spring Framework 6.0.4, I will discuss that with the team.
Comment From: sdeleuze
Can't reproduce anymore with Spring Boot 3.1.2
and Kotlin 1.8.22
, so I close this issue. Please comment if still relevant and provide a repro Git repository or archive.