Browse Source
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-31999pull/33505/head
10 changed files with 528 additions and 2 deletions
@ -0,0 +1,146 @@
@@ -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. |
||||
* <p> |
||||
* 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<Location> locations; |
||||
|
||||
private final Charset encoding; |
||||
|
||||
private final boolean failOnMissingLocations; |
||||
|
||||
private final List<ResourceWithLocation> resources = new ArrayList<>(); |
||||
|
||||
private final Lock lock = new ReentrantLock(); |
||||
|
||||
private boolean initialized; |
||||
|
||||
NativeImageResourceProvider(Scanner<?> scanner, ClassLoader classLoader, Collection<Location> 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<LoadableResource> getResources(String prefix, String[] suffixes) { |
||||
if (!NativeDetector.inNativeImage()) { |
||||
return this.scanner.getResources(prefix, suffixes); |
||||
} |
||||
ensureInitialized(); |
||||
List<LoadableResource> 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) { |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,49 @@
@@ -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<JavaMigration> 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); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,32 @@
@@ -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) { |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,76 @@
@@ -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(); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,64 @@
@@ -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<LoadableResource> 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); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,113 @@
@@ -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<GenericApplicationContext> 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<GenericApplicationContext> 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(); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue