spring-attic / spring-native

Spring Native is now superseded by Spring Boot 3 official native support
https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html
Apache License 2.0
2.74k stars 356 forks source link

Circular depends-on relationship with Flyway and Liquibase #1480

Closed jnizet closed 2 years ago

jnizet commented 2 years ago

I've noticed that Flyway wasn't listed in the supported libraries in the documentation, and also noticed that there is an issue(#778) already about the missing Flyway support, but anyway, before realizing that, I tried to use a ntive image with Flyway, and got a different issue than the one described in #778, that could reveal something wrong elsewhere, because the bug seems to be about bean wiring rather than about an actual Flyway support issue, so I decided to post it.

Here's the exception I get when using Flyway in a minimal project (see code below)

2022-02-05 13:10:22.224  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-02-05 13:10:22.224  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 23 ms
2022-02-05 13:10:22.231  WARN 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'demoController': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'demoRepository': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'flywayInitializer': Circular depends-on relationship between 'flywayInitializer' and 'flyway'
2022-02-05 13:10:22.231  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
2022-02-05 13:10:22.233 ERROR 1 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'demoController': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'demoRepository': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'flywayInitializer': Circular depends-on relationship between 'flywayInitializer' and 'flyway'
    at org.springframework.aot.beans.factory.InjectedConstructionResolver.resolve(InjectedConstructionResolver.java:88) ~[na:na]
    at org.springframework.aot.beans.factory.InjectedElementResolver.resolve(InjectedElementResolver.java:19) ~[na:na]
    at org.springframework.aot.beans.factory.InjectedElementResolver.create(InjectedElementResolver.java:50) ~[na:na]
    at org.springframework.aot.beans.factory.BeanDefinitionRegistrar$InstanceSupplierContext.create(BeanDefinitionRegistrar.java:193) ~[na:na]
    at org.springframework.aot.ContextBootstrapInitializer.lambda$initialize$1(ContextBootstrapInitializer.java:147) ~[na:na]
    at org.springframework.aot.beans.factory.ThrowableFunction.apply(ThrowableFunction.java:18) ~[na:na]
    at org.springframework.aot.beans.factory.BeanDefinitionRegistrar.lambda$instanceSupplier$0(BeanDefinitionRegistrar.java:97) ~[na:na]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1249) ~[na:na]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1191) ~[na:na]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) ~[na:na]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[na:na]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[na:na]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[na:na]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[na:na]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[na:na]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:953) ~[na:na]
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) ~[na:na]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[na:na]
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) ~[na:na]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:732) ~[com.example.demo.DemoApplicationKt:na]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:414) ~[com.example.demo.DemoApplicationKt:na]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:302) ~[com.example.demo.DemoApplicationKt:na]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) ~[com.example.demo.DemoApplicationKt:na]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) ~[com.example.demo.DemoApplicationKt:na]
    at com.example.demo.DemoApplicationKt.main(DemoApplication.kt:41) ~[na:na]
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'demoRepository': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'flywayInitializer': Circular depends-on relationship between 'flywayInitializer' and 'flyway'
    at org.springframework.aot.beans.factory.InjectedConstructionResolver.resolve(InjectedConstructionResolver.java:88) ~[na:na]
    at org.springframework.aot.beans.factory.InjectedElementResolver.resolve(InjectedElementResolver.java:19) ~[na:na]
    at org.springframework.aot.beans.factory.InjectedElementResolver.create(InjectedElementResolver.java:50) ~[na:na]
    at org.springframework.aot.beans.factory.BeanDefinitionRegistrar$InstanceSupplierContext.create(BeanDefinitionRegistrar.java:193) ~[na:na]
    at org.springframework.aot.ContextBootstrapInitializer.lambda$initialize$3(ContextBootstrapInitializer.java:149) ~[na:na]
    at org.springframework.aot.beans.factory.ThrowableFunction.apply(ThrowableFunction.java:18) ~[na:na]
    at org.springframework.aot.beans.factory.BeanDefinitionRegistrar.lambda$instanceSupplier$0(BeanDefinitionRegistrar.java:97) ~[na:na]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1249) ~[na:na]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1191) ~[na:na]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) ~[na:na]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[na:na]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[na:na]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[na:na]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[na:na]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[na:na]
    at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) ~[na:na]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1389) ~[na:na]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1309) ~[na:na]
    at org.springframework.aot.beans.factory.InjectedConstructionResolver.lambda$resolve$0(InjectedConstructionResolver.java:83) ~[na:na]
    at org.springframework.aot.beans.factory.InjectedConstructionResolver.resolveDependency(InjectedConstructionResolver.java:97) ~[na:na]
    at org.springframework.aot.beans.factory.InjectedConstructionResolver.resolve(InjectedConstructionResolver.java:83) ~[na:na]
    ... 24 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'flywayInitializer': Circular depends-on relationship between 'flywayInitializer' and 'flyway'
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:317) ~[na:na]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[na:na]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322) ~[na:na]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[na:na]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322) ~[na:na]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[na:na]
    at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) ~[na:na]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1389) ~[na:na]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1309) ~[na:na]
    at org.springframework.aot.beans.factory.InjectedConstructionResolver.lambda$resolve$0(InjectedConstructionResolver.java:83) ~[na:na]
    at org.springframework.aot.beans.factory.InjectedConstructionResolver.resolveDependency(InjectedConstructionResolver.java:97) ~[na:na]
    at org.springframework.aot.beans.factory.InjectedConstructionResolver.resolve(InjectedConstructionResolver.java:83) ~[na:na]
    ... 44 common frames omitted

And here's the code of the minimal repro I used to generate this stack trace (generated from Spring initializr, with Kotlin, gradle, JDBC, Web, Flyway and H2). This code works fine in normal, non-native mode.

package com.example.demo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.EnableTransactionManagement
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@EnableTransactionManagement
@SpringBootApplication
class DemoApplication

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

@RestController
@Transactional
@RequestMapping("/demos")
class DemoController(private val demoRepository: DemoRepository) {
    @GetMapping
    fun getDemos() = demoRepository.getDemos()
}

@Repository
class DemoRepository(private val jdbcTemplate: NamedParameterJdbcTemplate) {
    fun getDemos(): List<Demo> {
        return jdbcTemplate.query("select id, name from demo") { rs, rowNum ->
            Demo(rs.getLong("id"), rs.getString("name"))
        }
    }
}

data class Demo(val id: Long, val name: String)
snicoll commented 2 years ago

@jnizet can you please share a sample we can run ourselves rather than the code in text above. I've created a sample with the dependencies you've described and I can't reproduce the problem.

jnizet commented 2 years ago

@snicoll sure. Here is a zip file demo.zip

snicoll commented 2 years ago

@jnizet Thanks. How do I reproduce the problem? I did build the project and ran it using java -DspringAot=true -jar build/libs/demo-0.0.1-SNAPSHOT.jar. It starts fine for me.

snicoll commented 2 years ago

Ignore me JB, I thought this was AOT specific and it isn't. Building a native image lets me reproduce the problem.

@sdeleuze can you please take over?

jnizet commented 2 years ago

I was answering you, but you found it. Sorry for not having been clear enough.

sdeleuze commented 2 years ago

I can reproduce this bug with 0.11.2 but not with 0.11.3-SNAPSHOT so I assume it has been fixed in the interim. @jnizet If you can reproduce with 0.11.3-SNAPSHOT please reopen.

sdeleuze commented 2 years ago

Hum, unless I miss something, I can reproduce after a new build without changing anything, so this error could be not always reproductible. Anyway I reopen.

cdprete commented 2 years ago

I've the same issue, but with Liquibase....

2022-02-11 23:19:36.297  INFO 1 --- [           main] o.s.nativex.NativeListener               : AOT mode enabled

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.3)

2022-02-11 23:19:36.302  INFO 1 --- [           main] c.c.phonebook.PhoneBookApplication       : Starting PhoneBookApplication using Java 17.0.2 on fcb20acfe492 with PID 1 (/workspace/com.cdprete.phonebook.PhoneBookApplication started by cnb in /workspace)
2022-02-11 23:19:36.302  INFO 1 --- [           main] c.c.phonebook.PhoneBookApplication       : No active profile set, falling back to default profiles: default
2022-02-11 23:19:36.325  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-02-11 23:19:36.326  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-02-11 23:19:36.326  INFO 1 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.56]
2022-02-11 23:19:36.330  INFO 1 --- [           main] o.a.c.c.C.[.[localhost].[/phone-book]    : Initializing Spring embedded WebApplicationContext
2022-02-11 23:19:36.330  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 28 ms
2022-02-11 23:19:36.335  WARN 1 --- [           main] i.m.c.i.binder.jvm.JvmGcMetrics          : GC notifications will not be available because MemoryPoolMXBeans are not provided by the JVM
2022-02-11 23:19:36.345  INFO 1 --- [           main] o.s.boot.web.servlet.RegistrationBean    : Filter userInfoExtractorFilter was not registered (possibly already registered?)
2022-02-11 23:19:36.347  WARN 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSourceScriptDatabaseInitializer': Circular depends-on relationship between 'dataSourceScriptDatabaseInitializer' and 'liquibase'
2022-02-11 23:19:36.347  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
2022-02-11 23:19:36.349 ERROR 1 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSourceScriptDatabaseInitializer': Circular depends-on relationship between 'dataSourceScriptDatabaseInitializer' and 'liquibase'
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:317) ~[na:na]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[na:na]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322) ~[na:na]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[na:na]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322) ~[na:na]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[na:na]
        at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1154) ~[na:na]
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:908) ~[na:na]
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[na:na]
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) ~[na:na]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:732) ~[com.cdprete.phonebook.PhoneBookApplication:2.6.3]
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:414) ~[com.cdprete.phonebook.PhoneBookApplication:2.6.3]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:302) ~[com.cdprete.phonebook.PhoneBookApplication:2.6.3]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) ~[com.cdprete.phonebook.PhoneBookApplication:2.6.3]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) ~[com.cdprete.phonebook.PhoneBookApplication:2.6.3]
        at com.cdprete.phonebook.PhoneBookApplication.main(PhoneBookApplication.java:16) ~[com.cdprete.phonebook.PhoneBookApplication:na]
sdeleuze commented 2 years ago

Based on my tests, it seems it was cause by skipping the sorting of DatabaseInitializerDetector and DependsOnDatabaseInitializationDetector instances in Target_DatabaseInitializationDependencyConfigurer substitution.

After this change, @jnizet demo breaks with this error which seems Flyway specific and tracked by #778:

Caused by: java.lang.NullPointerException: null
        at org.flywaydb.core.internal.util.FileCopyUtils.copy(FileCopyUtils.java:121) ~[na:na]
        at org.flywaydb.core.internal.util.FileCopyUtils.copyToString(FileCopyUtils.java:81) ~[na:na]
        at org.flywaydb.core.internal.license.VersionPrinter.readVersion(VersionPrinter.java:67) ~[na:na]
        at org.flywaydb.core.internal.license.VersionPrinter.<clinit>(VersionPrinter.java:32) ~[na:na]
        ... 63 common frames omitted
alexcheng1982 commented 2 years ago

For the 0.11.2 version, a workaround is setting the spring.sql.init.enabled property to false and using only Flyway/Liquibase to initialize the database (no SQL scripts).