Add native-image support for Flyway
mhalbritter opened this issue ยท 16 comments
The Flyway smoke tests currently fail with GraalVM complaining about proxy generation, which could be related to @FlywayDataSource
:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'dataSource': Unsatisfied dependency expressed through method 'dataSource' parameter 0: Error creating bean with name 'spring.datasource-org.springframework.boot.autoconfigure.jdbc.DataSourceProperties': Proxy class defined by interfaces [interface org.springframework.beans.factory.annotation.Qualifier, interface org.springframework.core.annotation.SynthesizedAnnotation] not found. Generating proxy classes at runtime is not supported. Proxy classes need to be defined at image build time by specifying the list of interfaces that they implement. To define proxy classes use -H:DynamicProxyConfigurationFiles=<comma-separated-config-files> and -H:DynamicProxyConfigurationResources=<comma-separated-config-resources> options.
at org.springframework.beans.factory.aot.BeanInstanceSupplier.resolveArgument(BeanInstanceSupplier.java:349) ~[na:na]
at org.springframework.beans.factory.aot.BeanInstanceSupplier.resolveArguments(BeanInstanceSupplier.java:265) ~[na:na]
at org.springframework.beans.factory.aot.BeanInstanceSupplier.get(BeanInstanceSupplier.java:208) ~[na:na]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainInstanceFromSupplier(AbstractAutowireCapableBeanFactory.java:1224) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1209) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1156) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:566) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:526) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:930) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:926) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:592) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731) ~[flyway:3.0.0-SNAPSHOT]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:430) ~[flyway:3.0.0-SNAPSHOT]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:312) ~[flyway:3.0.0-SNAPSHOT]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) ~[flyway:3.0.0-SNAPSHOT]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) ~[flyway:3.0.0-SNAPSHOT]
at com.example.flyway.FlywayApplication.main(FlywayApplication.java:13) ~[flyway:na]
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'spring.datasource-org.springframework.boot.autoconfigure.jdbc.DataSourceProperties': Proxy class defined by interfaces [interface org.springframework.beans.factory.annotation.Qualifier, interface org.springframework.core.annotation.SynthesizedAnnotation] not found. Generating proxy classes at runtime is not supported. Proxy classes need to be defined at image build time by specifying the list of interfaces that they implement. To define proxy classes use -H:DynamicProxyConfigurationFiles=<comma-separated-config-files> and -H:DynamicProxyConfigurationResources=<comma-separated-config-resources> options.
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:611) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:526) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1374) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1294) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.aot.BeanInstanceSupplier.resolveArgument(BeanInstanceSupplier.java:332) ~[na:na]
... 20 common frames omitted
Caused by: com.oracle.svm.core.jdk.UnsupportedFeatureError: Proxy class defined by interfaces [interface org.springframework.beans.factory.annotation.Qualifier, interface org.springframework.core.annotation.SynthesizedAnnotation] not found. Generating proxy classes at runtime is not supported. Proxy classes need to be defined at image build time by specifying the list of interfaces that they implement. To define proxy classes use -H:DynamicProxyConfigurationFiles=<comma-separated-config-files> and -H:DynamicProxyConfigurationResources=<comma-separated-config-resources> options.
at com.oracle.svm.core.util.VMError.unsupportedFeature(VMError.java:89) ~[na:na]
at com.oracle.svm.reflect.proxy.DynamicProxySupport.getProxyClass(DynamicProxySupport.java:158) ~[na:na]
at java.lang.reflect.Proxy.getProxyConstructor(Proxy.java:48) ~[flyway:na]
at java.lang.reflect.Proxy.newProxyInstance(Proxy.java:1037) ~[flyway:na]
at org.springframework.core.annotation.SynthesizedMergedAnnotationInvocationHandler.createProxy(SynthesizedMergedAnnotationInvocationHandler.java:305) ~[na:na]
at org.springframework.core.annotation.TypeMappedAnnotation.createSynthesizedAnnotation(TypeMappedAnnotation.java:333) ~[na:na]
at org.springframework.core.annotation.AbstractMergedAnnotation.synthesize(AbstractMergedAnnotation.java:210) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.core.annotation.AbstractMergedAnnotation.synthesize(AbstractMergedAnnotation.java:200) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.core.annotation.AnnotationUtils.getAnnotation(AnnotationUtils.java:227) ~[na:na]
at org.springframework.core.annotation.AnnotationUtils.getAnnotation(AnnotationUtils.java:252) ~[na:na]
at org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils.isQualifierMatch(BeanFactoryAnnotationUtils.java:182) ~[na:na]
at org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils.qualifiedBeansOfType(BeanFactoryAnnotationUtils.java:68) ~[na:na]
at org.springframework.boot.context.properties.ConversionServiceDeducer$ConverterBeans.beans(ConversionServiceDeducer.java:99) ~[na:na]
at org.springframework.boot.context.properties.ConversionServiceDeducer$ConverterBeans.<init>(ConversionServiceDeducer.java:93) ~[na:na]
at org.springframework.boot.context.properties.ConversionServiceDeducer.getConversionServices(ConversionServiceDeducer.java:65) ~[na:na]
at org.springframework.boot.context.properties.ConversionServiceDeducer.getConversionServices(ConversionServiceDeducer.java:55) ~[na:na]
at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.getConversionServices(ConfigurationPropertiesBinder.java:183) ~[flyway:na]
at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.getBinder(ConfigurationPropertiesBinder.java:168) ~[flyway:na]
at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.bind(ConfigurationPropertiesBinder.java:95) ~[flyway:na]
at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.bind(ConfigurationPropertiesBindingPostProcessor.java:89) ~[flyway:na]
at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:78) ~[flyway:na]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:425) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1745) ~[flyway:6.0.0-SNAPSHOT]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:604) ~[flyway:6.0.0-SNAPSHOT]
... 29 common frames omitted
When this is fixed, i expect more flyway problems. Flyway enumerates migration files from the classpath, which is not supported on GraalVM.
Stephane will add the proxy hints to Spring Framework, as this proxy hint is not specific to @FlywayDataSource
but for all annotations meta-annotated with @Qualifier
.
I've created a PR to the reachability repo for Flyway. In the tests there is a custom ResourceProvider
which is needed because GraalVM doesn't support enumerating classpath resources. I guess we have to add something to Boot which gathers the migration files in the AOT phase, and then either generates code which contains the list of migration files, or writes a file, which is then read by a custom ResourceProvider
.
Even with the PR merged, Flyway still fails because it thinks there is no SLF4J available and tries to use Log4J2 as a logging system. I created another PR on the reachability repo to fix that for logback.
Besides that, i played around with migration finding in a native image (because classpath enumeration doesn't work :/), you can view the code here. I've implemented two approaches:
- find migrations in AOT phase and write them to an index file, which is later used to find the migrations
- find migrations in AOT phase and generate code for a bean which then knows how to find the migrations
After a review from Stephane:
Problems with the PoC: it uses a BeanFactoryInitializationAotProcessor
instead of a BeanRegistrationAotProcessor
. Using a BeanRegistrationAotProcessor
would allow to react on the presence of the default Flyway ResourceProvider
bean, and then replace that bean with code, which new
s up a ResourceProvider
with a fixed list of migrations.
Another problem is that the code re-implements how Flyway finds the migrations (using classpath enumeration). If would be much better if Flyway would provide an API, which we could call in AOT phase to get the list of migrations, which we then feed into our custom ResourceProvider
. Right now we risk that Flyway does something subtle different and we don't include all migrations, or include too much, etc.
Generally speaking, solution 2 (code generation) is the way to go, solution 1 (index file) can be dropped.
Problems with the new approach:
- The default
ResourceProvider
in Flyway seems to beScanner
, which is really hard tonew
. We can workaround this by introducing another signalling type, likePersistenceManagedTypes
done in JPA support.
I'm going to investigate how to refactor the PoC to accomodate the changes.
I've opened / commented on Flyway and GraalVM issues to get Flyway working without us having to write a bunch of workarounds.
Even if GraalVM team / Flyway team doesn't want to support it by themselves, I found a better workaround than to look for the migrations at AOT phase: Implement a Flyway custom ResourceProvider
which uses the resource:
scheme from GraalVM to find the migrations. The PoC is here: https://github.com/mhalbritter/flyway-native-image
All that is left with this ResourceProvider
in place is resource hints on db/migration/*.sql
(or whatever the user has configured).
Drawback of that approach is that, when the custom ResourceProvider
is in place, it get's called for all migration locations, maybe even one with filesystem:
or s3:
prefixes. It would be nice to delegate this back to the default ResourceProvider
, but it doesn't seem like there is an easy way to do that.
I'll put this on hold until either Flyway or GraalVM reacts on the issue.
So far I got no reaction from the Flyway team regarding GraalVM.
I'm currently playing around with Flyway in native-image and may have found a way to use their Scanner
to get support for AWS, file, etc. and only fall back to our own implementation when running in a native-image and reading classpath:
locations, using the new functionality in PathMatchingResourcePatternResolver to list resources in a native-image.
I've got something working here: https://github.com/mhalbritter/sb3-native-flyway-poc-2
There's a ResourceProviderCustomizer which is used when not running in AOT / native image. It's a FlywayConfigurationCustomizer
and is called by the Spring Boot FlywayAutoconfiguration
when Flyway is set up. This ResourceProviderCustomizer
is a noop implementation and does nothing. Its only purpose is to be a signal for the FlywayBeanRegistrationAotProcessor to kick in when running in AOT mode. This FlywayBeanRegistrationAotProcessor
replaces the no-op ResourceProviderCustomizer
with NativeImageResourceProviderCustomizer. When Spring Boot executes this customizer, it checks if the user has provided their own ResourceProvider
, and if not, installs NativeImageResourceProvider.
The NativeImageResourceProvider
uses the default Flyway Scanner
to support S3, Google Cloud etc. and additionally uses the PathMatchingResourcePatternResolver
to find migration files when running in a native image. When running in AOT mode on the JVM, it essentially delegates all work to the Flyway Scanner
.
One caveat: Flyway logs this:
2022-10-17T12:04:43.155+02:00 WARN 182256 --- [ main] o.f.core.internal.util.FeatureDetector : Unable to scan location: /db/migration (unsupported protocol: resource)
2022-10-17T12:04:43.155+02:00 WARN 182256 --- [ main] o.f.core.internal.util.FeatureDetector : Unable to scan location: /db/migration2 (unsupported protocol: resource)
2022-10-17T12:04:43.155+02:00 WARN 182256 --- [ main] o.f.c.i.s.classpath.ClassPathScanner : Unable to scan location: /db/migration (unsupported protocol: resource)
2022-10-17T12:04:43.155+02:00 WARN 182256 --- [ main] o.f.c.i.s.classpath.ClassPathScanner : Unable to scan location: /db/migration2 (unsupported protocol: resource)
this is due to the fact that the NativeImageResourceProviderCustomizer
passes all locations to Flyways Scanner
. We could remove the classpath locations from the locations passed to the Scanner
when running in a native image to get rid of those warnings.
Besides the WARN logs, this looks indeed like a viable solution for Flyway support.
I'm wondering if the FlywayBeanRegistrationAotProcessor
step is needed and if always adding the NativeImageResourceProviderCustomizer
with a native image check would work - I guess doing this has drawbacks that I'm currently missing?
Pinging @snicoll to know if there are better patterns we could use for this.
I'm not 100% convinced that the scanner created by NativeImageResourceProviderCustomizer behaves exactly the same as the default one from Flyway. That's the reason I was hesitant to always use the NativeImageResourceProviderCustomizer
on JVM and on native image.
We could remove the classpath locations from the locations passed to the
Scanner
when running in a native image to get rid of those warnings.
This, unfortunately, doesn't remove the warnings from ClassPathScanner
, only the one from FeatureDetector
.
Came along this warning too, tho all migrations are scanned and applied successfully. Is this just a warning or can it cause issues at later stages?
I'm not aware of any problems related to the warnings. As I said in #31999 (comment), this is due to the fact that we don't disable the built-in flyway scanner, which doesn't support native-image.