From b986a9b12ea4206ea2cda8a0a75843576cd3cb9d Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 18 Oct 2022 11:56:50 +0200 Subject: [PATCH] Add Flyway native-image support The ResourceProviderCustomizer, which is used by FlywayAutoConfiguration gets replaced with NativeImageResourceProviderCustomizer when running in AOT mode. The NativeImageResourceProvider does the heavy lifting when running in a native image: it uses PathMatchingResourcePatternResolver to find the migration files. Closes gh-31999 --- .../spring-boot-autoconfigure/build.gradle | 3 +- .../flyway/FlywayAutoConfiguration.java | 24 ++- .../flyway/NativeImageResourceProvider.java | 146 ++++++++++++++++++ ...NativeImageResourceProviderCustomizer.java | 49 ++++++ .../flyway/ResourceProviderCustomizer.java | 32 ++++ ...ustomizerBeanRegistrationAotProcessor.java | 76 +++++++++ .../resources/META-INF/spring/aot.factories | 3 + .../flyway/FlywayAutoConfigurationTests.java | 20 +++ ...eImageResourceProviderCustomizerTests.java | 64 ++++++++ ...izerBeanRegistrationAotProcessorTests.java | 113 ++++++++++++++ 10 files changed, 528 insertions(+), 2 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/NativeImageResourceProvider.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/NativeImageResourceProviderCustomizer.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/ResourceProviderCustomizer.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/ResourceProviderCustomizerBeanRegistrationAotProcessor.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/NativeImageResourceProviderCustomizerTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/ResourceProviderCustomizerBeanRegistrationAotProcessorTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 7bb8026bf3d..569589509b0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -234,6 +234,7 @@ dependencies { testImplementation("org.mockito:mockito-junit-jupiter") testImplementation("org.skyscreamer:jsonassert") testImplementation("org.springframework:spring-test") + testImplementation("org.springframework:spring-core-test") testImplementation("org.springframework.graphql:spring-graphql-test") testImplementation("org.springframework.kafka:spring-kafka-test") testImplementation("org.springframework.security:spring-security-test") @@ -258,4 +259,4 @@ tasks.named("checkSpringConfigurationMetadata").configure { "spring.datasource.tomcat.*", "spring.groovy.template.configuration.*" ] -} \ No newline at end of file +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java index 76e070e4f38..5e405171965 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java @@ -34,6 +34,8 @@ import org.flywaydb.core.api.configuration.FluentConfiguration; import org.flywaydb.core.api.migration.JavaMigration; import org.flywaydb.database.sqlserver.SQLServerConfigurationExtension; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -42,6 +44,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayAutoConfigurationRuntimeHints; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayDataSourceCondition; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; @@ -56,6 +59,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.io.ResourceLoader; @@ -81,6 +85,7 @@ import org.springframework.util.StringUtils; * @author András Deák * @author Semyon Danilov * @author Chris Bono + * @author Moritz Halbritter * @since 1.1.0 */ @AutoConfiguration(after = { DataSourceAutoConfiguration.class, JdbcTemplateAutoConfiguration.class, @@ -89,6 +94,7 @@ import org.springframework.util.StringUtils; @Conditional(FlywayDataSourceCondition.class) @ConditionalOnProperty(prefix = "spring.flyway", name = "enabled", matchIfMissing = true) @Import(DatabaseInitializationDependencyConfigurer.class) +@ImportRuntimeHints(FlywayAutoConfigurationRuntimeHints.class) public class FlywayAutoConfiguration { @Bean @@ -108,17 +114,24 @@ public class FlywayAutoConfiguration { @EnableConfigurationProperties(FlywayProperties.class) public static class FlywayConfiguration { + @Bean + ResourceProviderCustomizer resourceProviderCustomizer() { + return new ResourceProviderCustomizer(); + } + @Bean public Flyway flyway(FlywayProperties properties, ResourceLoader resourceLoader, ObjectProvider dataSource, @FlywayDataSource ObjectProvider flywayDataSource, ObjectProvider fluentConfigurationCustomizers, - ObjectProvider javaMigrations, ObjectProvider callbacks) { + ObjectProvider javaMigrations, ObjectProvider callbacks, + ResourceProviderCustomizer resourceProviderCustomizer) { FluentConfiguration configuration = new FluentConfiguration(resourceLoader.getClassLoader()); configureDataSource(configuration, properties, flywayDataSource.getIfAvailable(), dataSource.getIfUnique()); configureProperties(configuration, properties); configureCallbacks(configuration, callbacks.orderedStream().toList()); configureJavaMigrations(configuration, javaMigrations.orderedStream().toList()); fluentConfigurationCustomizers.orderedStream().forEach((customizer) -> customizer.customize(configuration)); + resourceProviderCustomizer.customize(configuration); return configuration.load(); } @@ -349,4 +362,13 @@ public class FlywayAutoConfiguration { } + static class FlywayAutoConfigurationRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + hints.resources().registerPattern("db/migration/*"); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/NativeImageResourceProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/NativeImageResourceProvider.java new file mode 100644 index 00000000000..5c84a754c7d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/NativeImageResourceProvider.java @@ -0,0 +1,146 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.flyway; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.ResourceProvider; +import org.flywaydb.core.api.resource.LoadableResource; +import org.flywaydb.core.internal.resource.classpath.ClassPathResource; +import org.flywaydb.core.internal.scanner.Scanner; +import org.flywaydb.core.internal.util.StringUtils; + +import org.springframework.core.NativeDetector; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +/** + * A Flyway {@link ResourceProvider} which supports GraalVM native-image. + *

+ * It delegates work to Flyways {@link Scanner}, and additionally uses + * {@link PathMatchingResourcePatternResolver} to find migration files in a native image. + * + * @author Moritz Halbritter + */ +class NativeImageResourceProvider implements ResourceProvider { + + private final Scanner scanner; + + private final ClassLoader classLoader; + + private final Collection locations; + + private final Charset encoding; + + private final boolean failOnMissingLocations; + + private final List resources = new ArrayList<>(); + + private final Lock lock = new ReentrantLock(); + + private boolean initialized; + + NativeImageResourceProvider(Scanner scanner, ClassLoader classLoader, Collection locations, + Charset encoding, boolean failOnMissingLocations) { + this.scanner = scanner; + this.classLoader = classLoader; + this.locations = locations; + this.encoding = encoding; + this.failOnMissingLocations = failOnMissingLocations; + } + + @Override + public LoadableResource getResource(String name) { + if (!NativeDetector.inNativeImage()) { + return this.scanner.getResource(name); + } + LoadableResource resource = this.scanner.getResource(name); + if (resource != null) { + return resource; + } + if (this.classLoader.getResource(name) == null) { + return null; + } + return new ClassPathResource(null, name, this.classLoader, this.encoding); + } + + @Override + public Collection getResources(String prefix, String[] suffixes) { + if (!NativeDetector.inNativeImage()) { + return this.scanner.getResources(prefix, suffixes); + } + ensureInitialized(); + List result = new ArrayList<>(this.scanner.getResources(prefix, suffixes)); + this.resources.stream().filter((r) -> StringUtils.startsAndEndsWith(r.resource.getFilename(), prefix, suffixes)) + .map((r) -> (LoadableResource) new ClassPathResource(r.location(), + r.location().getPath() + "/" + r.resource().getFilename(), this.classLoader, this.encoding)) + .forEach(result::add); + return result; + } + + private void ensureInitialized() { + this.lock.lock(); + try { + if (!this.initialized) { + initialize(); + this.initialized = true; + } + } + finally { + this.lock.unlock(); + } + } + + private void initialize() { + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + for (Location location : this.locations) { + if (!location.isClassPath()) { + continue; + } + Resource root = resolver.getResource(location.getDescriptor()); + if (!root.exists()) { + if (this.failOnMissingLocations) { + throw new FlywayException("Location " + location.getDescriptor() + " doesn't exist"); + } + continue; + } + Resource[] resources; + try { + resources = resolver.getResources(root.getURI() + "/*"); + } + catch (IOException ex) { + throw new UncheckedIOException("Failed to list resources for " + location.getDescriptor(), ex); + } + for (Resource resource : resources) { + this.resources.add(new ResourceWithLocation(resource, location)); + } + } + } + + private record ResourceWithLocation(Resource resource, Location location) { + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/NativeImageResourceProviderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/NativeImageResourceProviderCustomizer.java new file mode 100644 index 00000000000..1dd7b6e55ec --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/NativeImageResourceProviderCustomizer.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.flyway; + +import java.util.Arrays; + +import org.flywaydb.core.api.configuration.FluentConfiguration; +import org.flywaydb.core.api.migration.JavaMigration; +import org.flywaydb.core.internal.scanner.LocationScannerCache; +import org.flywaydb.core.internal.scanner.ResourceNameCache; +import org.flywaydb.core.internal.scanner.Scanner; + +/** + * Registers {@link NativeImageResourceProvider} as a Flyway + * {@link org.flywaydb.core.api.ResourceProvider}. + * + * @author Moritz Halbritter + */ +class NativeImageResourceProviderCustomizer extends ResourceProviderCustomizer { + + @Override + public void customize(FluentConfiguration configuration) { + if (configuration.getResourceProvider() == null) { + Scanner scanner = new Scanner<>(JavaMigration.class, + Arrays.asList(configuration.getLocations()), configuration.getClassLoader(), + configuration.getEncoding(), configuration.isDetectEncoding(), false, new ResourceNameCache(), + new LocationScannerCache(), configuration.isFailOnMissingLocations()); + NativeImageResourceProvider resourceProvider = new NativeImageResourceProvider(scanner, + configuration.getClassLoader(), Arrays.asList(configuration.getLocations()), + configuration.getEncoding(), configuration.isFailOnMissingLocations()); + configuration.resourceProvider(resourceProvider); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/ResourceProviderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/ResourceProviderCustomizer.java new file mode 100644 index 00000000000..21fcdb7aabc --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/ResourceProviderCustomizer.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.flyway; + +import org.flywaydb.core.api.configuration.FluentConfiguration; + +/** + * A Flyway customizer which gets replaced with + * {@link NativeImageResourceProviderCustomizer} when running in a native image. + * + * @author Moritz Halbritter + */ +class ResourceProviderCustomizer { + + void customize(FluentConfiguration configuration) { + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/ResourceProviderCustomizerBeanRegistrationAotProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/ResourceProviderCustomizerBeanRegistrationAotProcessor.java new file mode 100644 index 00000000000..140c8468a85 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/ResourceProviderCustomizerBeanRegistrationAotProcessor.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.flyway; + +import java.lang.reflect.Executable; + +import javax.lang.model.element.Modifier; + +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; +import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.javapoet.CodeBlock; + +/** + * Replaces the {@link ResourceProviderCustomizer} bean with a + * {@link NativeImageResourceProviderCustomizer} bean. + * + * @author Moritz Halbritter + */ +class ResourceProviderCustomizerBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { + + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + if (registeredBean.getBeanClass().equals(ResourceProviderCustomizer.class)) { + return BeanRegistrationAotContribution + .withCustomCodeFragments((codeFragments) -> new AotContribution(codeFragments, registeredBean)); + } + return null; + } + + private static class AotContribution extends BeanRegistrationCodeFragmentsDecorator { + + private final RegisteredBean registeredBean; + + protected AotContribution(BeanRegistrationCodeFragments delegate, RegisteredBean registeredBean) { + super(delegate); + this.registeredBean = registeredBean; + } + + @Override + public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, Executable constructorOrFactoryMethod, + boolean allowDirectSupplierShortcut) { + GeneratedMethod generatedMethod = beanRegistrationCode.getMethods().add("getInstance", (method) -> { + method.addJavadoc("Get the bean instance for '$L'.", this.registeredBean.getBeanName()); + method.addModifiers(Modifier.PRIVATE, Modifier.STATIC); + method.returns(NativeImageResourceProviderCustomizer.class); + CodeBlock.Builder code = CodeBlock.builder(); + code.addStatement("return new $T()", NativeImageResourceProviderCustomizer.class); + method.addCode(code.build()); + }); + return generatedMethod.toMethodReference().toCodeBlock(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/aot.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/aot.factories index 064b3107ba0..ece7966c11f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/aot.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/aot.factories @@ -3,3 +3,6 @@ org.springframework.boot.autoconfigure.template.TemplateRuntimeHints org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingProcessor + +org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ +org.springframework.boot.autoconfigure.flyway.ResourceProviderCustomizerBeanRegistrationAotProcessor diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java index 9f308020498..0f067db51b6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java @@ -25,6 +25,7 @@ import java.util.UUID; import javax.sql.DataSource; +import org.assertj.core.api.Assertions; import org.flywaydb.core.Flyway; import org.flywaydb.core.api.Location; import org.flywaydb.core.api.MigrationVersion; @@ -41,9 +42,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InOrder; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayAutoConfigurationRuntimeHints; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -94,6 +98,7 @@ import static org.mockito.Mockito.mock; * @author András Deák * @author Takaaki Shimbo * @author Chris Bono + * @author Moritz Halbritter */ @ExtendWith(OutputCaptureExtension.class) class FlywayAutoConfigurationTests { @@ -656,6 +661,21 @@ class FlywayAutoConfigurationTests { .isEqualTo("SPS")); } + @Test + void containsResourceProviderCustomizer() { + this.contextRunner.withPropertyValues("spring.flyway.url:jdbc:hsqldb:mem:" + UUID.randomUUID()) + .run((context) -> assertThat(context).hasSingleBean(ResourceProviderCustomizer.class)); + } + + @Test + void shouldRegisterResourceHints() { + RuntimeHints runtimeHints = new RuntimeHints(); + new FlywayAutoConfigurationRuntimeHints().registerHints(runtimeHints, getClass().getClassLoader()); + Assertions.assertThat(RuntimeHintsPredicates.resource().forResource("db/migration/")).accepts(runtimeHints); + Assertions.assertThat(RuntimeHintsPredicates.resource().forResource("db/migration/V1__init.sql")) + .accepts(runtimeHints); + } + private ContextConsumer validateFlywayTeamsPropertyOnly(String propertyName) { return (context) -> { assertThat(context).hasFailed(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/NativeImageResourceProviderCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/NativeImageResourceProviderCustomizerTests.java new file mode 100644 index 00000000000..6928a3f8945 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/NativeImageResourceProviderCustomizerTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.flyway; + +import java.util.Collection; + +import org.flywaydb.core.api.ResourceProvider; +import org.flywaydb.core.api.configuration.FluentConfiguration; +import org.flywaydb.core.api.resource.LoadableResource; +import org.flywaydb.core.internal.resource.NoopResourceProvider; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link NativeImageResourceProviderCustomizer}. + * + * @author Moritz Halbritter + */ +class NativeImageResourceProviderCustomizerTests { + + private final NativeImageResourceProviderCustomizer customizer = new NativeImageResourceProviderCustomizer(); + + @Test + void shouldInstallNativeImageResourceProvider() { + FluentConfiguration configuration = new FluentConfiguration(); + assertThat(configuration.getResourceProvider()).isNull(); + this.customizer.customize(configuration); + assertThat(configuration.getResourceProvider()).isInstanceOf(NativeImageResourceProvider.class); + } + + @Test + void nativeImageResourceProviderShouldFindMigrations() { + FluentConfiguration configuration = new FluentConfiguration(); + this.customizer.customize(configuration); + ResourceProvider resourceProvider = configuration.getResourceProvider(); + Collection migrations = resourceProvider.getResources("V", new String[] { ".sql" }); + LoadableResource migration = resourceProvider.getResource("V1__init.sql"); + assertThat(migrations).containsExactly(migration); + } + + @Test + void shouldBackOffOnCustomResourceProvider() { + FluentConfiguration configuration = new FluentConfiguration(); + configuration.resourceProvider(NoopResourceProvider.INSTANCE); + this.customizer.customize(configuration); + assertThat(configuration.getResourceProvider()).isEqualTo(NoopResourceProvider.INSTANCE); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/ResourceProviderCustomizerBeanRegistrationAotProcessorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/ResourceProviderCustomizerBeanRegistrationAotProcessorTests.java new file mode 100644 index 00000000000..669599ebdf7 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/ResourceProviderCustomizerBeanRegistrationAotProcessorTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.flyway; + +import java.util.Arrays; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.aot.AotServices; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.aot.ApplicationContextAotGenerator; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.test.tools.CompileWithForkedClassLoader; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.javapoet.ClassName; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ResourceProviderCustomizerBeanRegistrationAotProcessor}. + * + * @author Moritz Halbritter + */ +class ResourceProviderCustomizerBeanRegistrationAotProcessorTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + private final ResourceProviderCustomizerBeanRegistrationAotProcessor processor = new ResourceProviderCustomizerBeanRegistrationAotProcessor(); + + @Test + void beanRegistrationAotProcessorIsRegistered() { + assertThat(AotServices.factories().load(BeanRegistrationAotProcessor.class)) + .anyMatch(ResourceProviderCustomizerBeanRegistrationAotProcessor.class::isInstance); + } + + @Test + void shouldIgnoreNonResourceProviderCustomizerBeans() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(String.class); + this.beanFactory.registerBeanDefinition("test", beanDefinition); + BeanRegistrationAotContribution contribution = this.processor + .processAheadOfTime(RegisteredBean.of(this.beanFactory, "test")); + assertThat(contribution).isNull(); + } + + @Test + @CompileWithForkedClassLoader + void shouldReplaceResourceProviderCustomizer() { + compile(createContext(ResourceProviderCustomizerConfiguration.class), (freshContext) -> { + freshContext.refresh(); + ResourceProviderCustomizer bean = freshContext.getBean(ResourceProviderCustomizer.class); + assertThat(bean).isInstanceOf(NativeImageResourceProviderCustomizer.class); + }); + } + + private GenericApplicationContext createContext(Class... types) { + GenericApplicationContext context = new AnnotationConfigApplicationContext(); + Arrays.stream(types).forEach((type) -> context.registerBean(type)); + return context; + } + + @SuppressWarnings("unchecked") + private void compile(GenericApplicationContext context, Consumer freshContext) { + TestGenerationContext generationContext = new TestGenerationContext(TestTarget.class); + ClassName className = new ApplicationContextAotGenerator().processAheadOfTime(context, generationContext); + generationContext.writeGeneratedContent(); + TestCompiler.forSystem().with(generationContext).compile((compiled) -> { + GenericApplicationContext freshApplicationContext = new GenericApplicationContext(); + ApplicationContextInitializer initializer = compiled + .getInstance(ApplicationContextInitializer.class, className.toString()); + initializer.initialize(freshApplicationContext); + freshContext.accept(freshApplicationContext); + }); + } + + static class TestTarget { + + } + + @Configuration(proxyBeanMethods = false) + static class ResourceProviderCustomizerConfiguration { + + @Bean + ResourceProviderCustomizer resourceProviderCustomizer() { + return new ResourceProviderCustomizer(); + } + + } + +}