Skip to content

Commit f0e72f0

Browse files
committed
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 spring-projectsgh-31999
1 parent db248b8 commit f0e72f0

10 files changed

+528
-2
lines changed

spring-boot-project/spring-boot-autoconfigure/build.gradle

+2-1
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ dependencies {
234234
testImplementation("org.mockito:mockito-junit-jupiter")
235235
testImplementation("org.skyscreamer:jsonassert")
236236
testImplementation("org.springframework:spring-test")
237+
testImplementation("org.springframework:spring-core-test")
237238
testImplementation("org.springframework.graphql:spring-graphql-test")
238239
testImplementation("org.springframework.kafka:spring-kafka-test")
239240
testImplementation("org.springframework.security:spring-security-test")
@@ -258,4 +259,4 @@ tasks.named("checkSpringConfigurationMetadata").configure {
258259
"spring.datasource.tomcat.*",
259260
"spring.groovy.template.configuration.*"
260261
]
261-
}
262+
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java

+23-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import org.flywaydb.core.api.migration.JavaMigration;
3535
import org.flywaydb.database.sqlserver.SQLServerConfigurationExtension;
3636

37+
import org.springframework.aot.hint.RuntimeHints;
38+
import org.springframework.aot.hint.RuntimeHintsRegistrar;
3739
import org.springframework.beans.factory.ObjectProvider;
3840
import org.springframework.boot.autoconfigure.AutoConfiguration;
3941
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -42,6 +44,7 @@
4244
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
4345
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
4446
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
47+
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayAutoConfigurationRuntimeHints;
4548
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayDataSourceCondition;
4649
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
4750
import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration;
@@ -56,6 +59,7 @@
5659
import org.springframework.context.annotation.Conditional;
5760
import org.springframework.context.annotation.Configuration;
5861
import org.springframework.context.annotation.Import;
62+
import org.springframework.context.annotation.ImportRuntimeHints;
5963
import org.springframework.core.convert.TypeDescriptor;
6064
import org.springframework.core.convert.converter.GenericConverter;
6165
import org.springframework.core.io.ResourceLoader;
@@ -81,6 +85,7 @@
8185
* @author András Deák
8286
* @author Semyon Danilov
8387
* @author Chris Bono
88+
* @author Moritz Halbritter
8489
* @since 1.1.0
8590
*/
8691
@AutoConfiguration(after = { DataSourceAutoConfiguration.class, JdbcTemplateAutoConfiguration.class,
@@ -89,6 +94,7 @@
8994
@Conditional(FlywayDataSourceCondition.class)
9095
@ConditionalOnProperty(prefix = "spring.flyway", name = "enabled", matchIfMissing = true)
9196
@Import(DatabaseInitializationDependencyConfigurer.class)
97+
@ImportRuntimeHints(FlywayAutoConfigurationRuntimeHints.class)
9298
public class FlywayAutoConfiguration {
9399

94100
@Bean
@@ -108,17 +114,24 @@ public FlywaySchemaManagementProvider flywayDefaultDdlModeProvider(ObjectProvide
108114
@EnableConfigurationProperties(FlywayProperties.class)
109115
public static class FlywayConfiguration {
110116

117+
@Bean
118+
ResourceProviderCustomizer resourceProviderCustomizer() {
119+
return new ResourceProviderCustomizer();
120+
}
121+
111122
@Bean
112123
public Flyway flyway(FlywayProperties properties, ResourceLoader resourceLoader,
113124
ObjectProvider<DataSource> dataSource, @FlywayDataSource ObjectProvider<DataSource> flywayDataSource,
114125
ObjectProvider<FlywayConfigurationCustomizer> fluentConfigurationCustomizers,
115-
ObjectProvider<JavaMigration> javaMigrations, ObjectProvider<Callback> callbacks) {
126+
ObjectProvider<JavaMigration> javaMigrations, ObjectProvider<Callback> callbacks,
127+
ResourceProviderCustomizer resourceProviderCustomizer) {
116128
FluentConfiguration configuration = new FluentConfiguration(resourceLoader.getClassLoader());
117129
configureDataSource(configuration, properties, flywayDataSource.getIfAvailable(), dataSource.getIfUnique());
118130
configureProperties(configuration, properties);
119131
configureCallbacks(configuration, callbacks.orderedStream().toList());
120132
configureJavaMigrations(configuration, javaMigrations.orderedStream().toList());
121133
fluentConfigurationCustomizers.orderedStream().forEach((customizer) -> customizer.customize(configuration));
134+
resourceProviderCustomizer.customize(configuration);
122135
return configuration.load();
123136
}
124137

@@ -349,4 +362,13 @@ private static final class FlywayUrlCondition {
349362

350363
}
351364

365+
static class FlywayAutoConfigurationRuntimeHints implements RuntimeHintsRegistrar {
366+
367+
@Override
368+
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
369+
hints.resources().registerPattern("db/migration/*");
370+
}
371+
372+
}
373+
352374
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.flyway;
18+
19+
import java.io.IOException;
20+
import java.io.UncheckedIOException;
21+
import java.nio.charset.Charset;
22+
import java.util.ArrayList;
23+
import java.util.Collection;
24+
import java.util.List;
25+
import java.util.concurrent.locks.Lock;
26+
import java.util.concurrent.locks.ReentrantLock;
27+
28+
import org.flywaydb.core.api.FlywayException;
29+
import org.flywaydb.core.api.Location;
30+
import org.flywaydb.core.api.ResourceProvider;
31+
import org.flywaydb.core.api.resource.LoadableResource;
32+
import org.flywaydb.core.internal.resource.classpath.ClassPathResource;
33+
import org.flywaydb.core.internal.scanner.Scanner;
34+
import org.flywaydb.core.internal.util.StringUtils;
35+
36+
import org.springframework.core.NativeDetector;
37+
import org.springframework.core.io.Resource;
38+
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
39+
40+
/**
41+
* A Flyway {@link ResourceProvider} which supports GraalVM native-image.
42+
* <p>
43+
* It delegates work to Flyways {@link Scanner}, and additionally uses
44+
* {@link PathMatchingResourcePatternResolver} to find migration files in a native image.
45+
*
46+
* @author Moritz Halbritter
47+
*/
48+
class NativeImageResourceProvider implements ResourceProvider {
49+
50+
private final Scanner<?> scanner;
51+
52+
private final ClassLoader classLoader;
53+
54+
private final Collection<Location> locations;
55+
56+
private final Charset encoding;
57+
58+
private final boolean failOnMissingLocations;
59+
60+
private final List<ResourceWithLocation> resources = new ArrayList<>();
61+
62+
private final Lock lock = new ReentrantLock();
63+
64+
private boolean initialized;
65+
66+
NativeImageResourceProvider(Scanner<?> scanner, ClassLoader classLoader, Collection<Location> locations,
67+
Charset encoding, boolean failOnMissingLocations) {
68+
this.scanner = scanner;
69+
this.classLoader = classLoader;
70+
this.locations = locations;
71+
this.encoding = encoding;
72+
this.failOnMissingLocations = failOnMissingLocations;
73+
}
74+
75+
@Override
76+
public LoadableResource getResource(String name) {
77+
if (!NativeDetector.inNativeImage()) {
78+
return this.scanner.getResource(name);
79+
}
80+
LoadableResource resource = this.scanner.getResource(name);
81+
if (resource != null) {
82+
return resource;
83+
}
84+
if (this.classLoader.getResource(name) == null) {
85+
return null;
86+
}
87+
return new ClassPathResource(null, name, this.classLoader, this.encoding);
88+
}
89+
90+
@Override
91+
public Collection<LoadableResource> getResources(String prefix, String[] suffixes) {
92+
if (!NativeDetector.inNativeImage()) {
93+
return this.scanner.getResources(prefix, suffixes);
94+
}
95+
ensureInitialized();
96+
List<LoadableResource> result = new ArrayList<>(this.scanner.getResources(prefix, suffixes));
97+
this.resources.stream().filter((r) -> StringUtils.startsAndEndsWith(r.resource.getFilename(), prefix, suffixes))
98+
.map((r) -> (LoadableResource) new ClassPathResource(r.location(),
99+
r.location().getPath() + "/" + r.resource().getFilename(), this.classLoader, this.encoding))
100+
.forEach(result::add);
101+
return result;
102+
}
103+
104+
private void ensureInitialized() {
105+
this.lock.lock();
106+
try {
107+
if (!this.initialized) {
108+
initialize();
109+
this.initialized = true;
110+
}
111+
}
112+
finally {
113+
this.lock.unlock();
114+
}
115+
}
116+
117+
private void initialize() {
118+
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
119+
for (Location location : this.locations) {
120+
if (!location.isClassPath()) {
121+
continue;
122+
}
123+
Resource root = resolver.getResource(location.getDescriptor());
124+
if (!root.exists()) {
125+
if (this.failOnMissingLocations) {
126+
throw new FlywayException("Location " + location.getDescriptor() + " doesn't exist");
127+
}
128+
continue;
129+
}
130+
Resource[] resources;
131+
try {
132+
resources = resolver.getResources(root.getURI() + "/*");
133+
}
134+
catch (IOException ex) {
135+
throw new UncheckedIOException("Failed to list resources for " + location.getDescriptor(), ex);
136+
}
137+
for (Resource resource : resources) {
138+
this.resources.add(new ResourceWithLocation(resource, location));
139+
}
140+
}
141+
}
142+
143+
private record ResourceWithLocation(Resource resource, Location location) {
144+
}
145+
146+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.flyway;
18+
19+
import java.util.Arrays;
20+
21+
import org.flywaydb.core.api.configuration.FluentConfiguration;
22+
import org.flywaydb.core.api.migration.JavaMigration;
23+
import org.flywaydb.core.internal.scanner.LocationScannerCache;
24+
import org.flywaydb.core.internal.scanner.ResourceNameCache;
25+
import org.flywaydb.core.internal.scanner.Scanner;
26+
27+
/**
28+
* Registers {@link NativeImageResourceProvider} as a Flyway
29+
* {@link org.flywaydb.core.api.ResourceProvider}.
30+
*
31+
* @author Moritz Halbritter
32+
*/
33+
class NativeImageResourceProviderCustomizer extends ResourceProviderCustomizer {
34+
35+
@Override
36+
public void customize(FluentConfiguration configuration) {
37+
if (configuration.getResourceProvider() == null) {
38+
Scanner<JavaMigration> scanner = new Scanner<>(JavaMigration.class,
39+
Arrays.asList(configuration.getLocations()), configuration.getClassLoader(),
40+
configuration.getEncoding(), configuration.isDetectEncoding(), false, new ResourceNameCache(),
41+
new LocationScannerCache(), configuration.isFailOnMissingLocations());
42+
NativeImageResourceProvider resourceProvider = new NativeImageResourceProvider(scanner,
43+
configuration.getClassLoader(), Arrays.asList(configuration.getLocations()),
44+
configuration.getEncoding(), configuration.isFailOnMissingLocations());
45+
configuration.resourceProvider(resourceProvider);
46+
}
47+
}
48+
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.flyway;
18+
19+
import org.flywaydb.core.api.configuration.FluentConfiguration;
20+
21+
/**
22+
* A Flyway customizer which gets replaced with
23+
* {@link NativeImageResourceProviderCustomizer} when running in a native image.
24+
*
25+
* @author Moritz Halbritter
26+
*/
27+
class ResourceProviderCustomizer {
28+
29+
void customize(FluentConfiguration configuration) {
30+
}
31+
32+
}

0 commit comments

Comments
 (0)