blagerweij/liquibase-sessionlock

NPE at startup when using Spring Native

Closed this issue · 3 comments

I'm using Spring Boot 3 with Liquibase and liquibase-sessionlock. It works well.

Then I'm trying to build a Spring Native docker image as described here: https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html#native-image.developing-your-first-application.buildpacks.gradle
The image is built successfully.

But when I run it an NPE error occurs:

2023-03-22T17:07:26.914Z ERROR 1 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'liquibase': java.lang.NullPointerException
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1762) ~[org.myapp.MyApplication:6.0.6]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:599) ~[org.myapp.MyApplication:6.0.6]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:521) ~[org.myapp.MyApplication:6.0.6]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[org.myapp.MyApplication:6.0.6]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[org.myapp.MyApplication:6.0.6]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[org.myapp.MyApplication:6.0.6]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[org.myapp.MyApplication:6.0.6]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:313) ~[org.myapp.MyApplication:6.0.6]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[org.myapp.MyApplication:6.0.6]
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1132) ~[org.myapp.MyApplication:6.0.6]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:907) ~[org.myapp.MyApplication:6.0.6]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:584) ~[org.myapp.MyApplication:6.0.6]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[org.myapp.MyApplication:3.0.4]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:732) ~[org.myapp.MyApplication:3.0.4]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:434) ~[org.myapp.MyApplication:3.0.4]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:310) ~[org.myapp.MyApplication:3.0.4]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1304) ~[org.myapp.MyApplication:3.0.4]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1293) ~[org.myapp.MyApplication:3.0.4]
	at org.myapp.MyApplication.main(MyApplication.java:12) ~[org.myapp.MyApplication:na]
Caused by: liquibase.exception.LiquibaseException: java.lang.NullPointerException
	at liquibase.Liquibase.runInScope(Liquibase.java:2452) ~[na:na]
	at liquibase.Liquibase.update(Liquibase.java:236) ~[na:na]
	at liquibase.Liquibase.update(Liquibase.java:221) ~[na:na]
	at liquibase.integration.spring.SpringLiquibase.performUpdate(SpringLiquibase.java:328) ~[org.myapp.MyApplication:na]
	at liquibase.integration.spring.SpringLiquibase.afterPropertiesSet(SpringLiquibase.java:283) ~[org.myapp.MyApplication:na]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1808) ~[org.myapp.MyApplication:6.0.6]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1758) ~[org.myapp.MyApplication:6.0.6]
	... 18 common frames omitted
Caused by: java.lang.NullPointerException: null
	at java.base@17.0.6/java.lang.reflect.Method.invoke(Method.java:561) ~[org.myapp.MyApplication:na]
	at com.github.blagerweij.sessionlock.SessionLockService.lambda$static$1(SessionLockService.java:257) ~[org.myapp.MyApplication:na]
	at com.github.blagerweij.sessionlock.SessionLockService.getLog(SessionLockService.java:267) ~[org.myapp.MyApplication:na]
	at com.github.blagerweij.sessionlock.SessionLockService.acquireLock(SessionLockService.java:156) ~[org.myapp.MyApplication:na]
	at com.github.blagerweij.sessionlock.SessionLockService.waitForLock(SessionLockService.java:77) ~[org.myapp.MyApplication:na]
	at liquibase.Liquibase.lambda$update$1(Liquibase.java:239) ~[na:na]
	at liquibase.Scope.lambda$child$0(Scope.java:180) ~[org.myapp.MyApplication:na]
	at liquibase.Scope.child(Scope.java:189) ~[org.myapp.MyApplication:na]
	at liquibase.Scope.child(Scope.java:179) ~[org.myapp.MyApplication:na]
	at liquibase.Scope.child(Scope.java:158) ~[org.myapp.MyApplication:na]
	at liquibase.Liquibase.runInScope(Liquibase.java:2447) ~[na:na]
	... 24 common frames omitted

Tested with the following framework & library versions:

Spring Boot: 3.0.4
org.graalvm.buildtools.native: 0.9.20
liquibase-core: 4.17.2
liquibase-sessionlock: 1.6.2

Since the SessionLockService implementation uses reflection, you must follow the guidance to "provide your own hints for reflection" - see https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html#native-image.advanced.custom-hints.

You would need something like:

import liquibase.Scope;
import liquibase.logging.LogService;
import org.springframework.util.ReflectionUtils;
...

public class LiquibaseSessionLockRuntimeHints implements RuntimeHintsRegistrar {

    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        // Register method for reflection
        Method scopeMethod = ReflectionUtils.findMethod(Scope.class, "getLog", Class.class);
        if (scopeMethod != null) {
            hints.reflection().registerMethod(scopeMethod, ExecutableMode.INVOKE);
        }

        Method logServiceMethod = ReflectionUtils.findMethod(LogService.class, "getLog", Class.class);
        if (logServiceMethod != null) {
            hints.reflection().registerMethod(logServiceMethod, ExecutableMode.INVOKE);
        }
    }
}

With this class added, and @ImportRuntimeHints annotation added to your @SpringBootApplication class, the NPE should disappear.

The reason why we're using that ugly reflection code in the first place is to support the different Liquibase releases (3.x, 4.x). It was hard to get hold of the LogService in a way that would work with all releases. I can see how this would break on a GraalVM, let me see if we can find an alternative approach.

Should be resolved in v1.6.4