Skip to content

Commit 20f42f9

Browse files
committed
Speedup Flyway by preventing it from doing classpath scanning
This PR essentially takes the substitution we had for native mode and turns it into a class transformer. This way even in JVM mode our Flyway integration take advantage of the fact that the migration script locations are known at build time, thus no classpath scanning is needed. This solution isn't ideal from a maintenance perspective, but the transformation is relatively straightforward and our test suite for Flyway pretty extensive, so we should easily be able to adapt to future Flyway updates (ideally we would be able to remove this if /when flyway/flyway#2822 is done) Relates to: #9428
1 parent bcbbbdc commit 20f42f9

File tree

8 files changed

+165
-52
lines changed

8 files changed

+165
-52
lines changed

extensions/flyway/deployment/src/main/java/io/quarkus/flyway/FlywayProcessor.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;
44

5-
import java.io.File;
65
import java.io.IOException;
76
import java.net.URI;
87
import java.net.URISyntaxException;
@@ -41,6 +40,7 @@
4140
import io.quarkus.deployment.annotations.BuildStep;
4241
import io.quarkus.deployment.annotations.ExecutionTime;
4342
import io.quarkus.deployment.annotations.Record;
43+
import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem;
4444
import io.quarkus.deployment.builditem.CapabilityBuildItem;
4545
import io.quarkus.deployment.builditem.FeatureBuildItem;
4646
import io.quarkus.deployment.builditem.ServiceStartBuildItem;
@@ -66,6 +66,13 @@ CapabilityBuildItem capability() {
6666
return new CapabilityBuildItem(Capabilities.FLYWAY);
6767
}
6868

69+
@BuildStep
70+
void scannerTransformer(BuildProducer<BytecodeTransformerBuildItem> transformers) {
71+
transformers
72+
.produce(new BytecodeTransformerBuildItem(true, ScannerTransformer.FLYWAY_SCANNER_CLASS_NAME,
73+
new ScannerTransformer()));
74+
}
75+
6976
@Record(STATIC_INIT)
7077
@BuildStep
7178
void build(BuildProducer<FeatureBuildItem> featureProducer,
@@ -198,8 +205,9 @@ private Set<String> getApplicationMigrationsFromPath(final String location, fina
198205
try (final Stream<Path> pathStream = Files.walk(Paths.get(path.toURI()))) {
199206
return pathStream.filter(Files::isRegularFile)
200207
.map(it -> Paths.get(location, it.getFileName().toString()).toString())
201-
.map(it -> it.replace(File.separatorChar, '/'))
202-
.peek(it -> LOGGER.debug("Discovered: " + it))
208+
// we don't want windows paths here since the paths are going to be used as classpath paths anyway
209+
.map(it -> it.replace('\\', '/'))
210+
.peek(it -> LOGGER.debugf("Discovered path: %s", it))
203211
.collect(Collectors.toSet());
204212
}
205213
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package io.quarkus.flyway;
2+
3+
import static org.objectweb.asm.Opcodes.ALOAD;
4+
import static org.objectweb.asm.Opcodes.ASTORE;
5+
import static org.objectweb.asm.Opcodes.DUP;
6+
import static org.objectweb.asm.Opcodes.GETFIELD;
7+
import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
8+
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
9+
import static org.objectweb.asm.Opcodes.NEW;
10+
import static org.objectweb.asm.Opcodes.POP;
11+
import static org.objectweb.asm.Opcodes.PUTFIELD;
12+
import static org.objectweb.asm.Opcodes.RETURN;
13+
14+
import java.util.function.BiFunction;
15+
16+
import org.flywaydb.core.internal.scanner.Scanner;
17+
import org.flywaydb.core.internal.scanner.classpath.ResourceAndClassScanner;
18+
import org.objectweb.asm.ClassVisitor;
19+
import org.objectweb.asm.MethodVisitor;
20+
21+
import io.quarkus.flyway.runtime.QuarkusPathLocationScanner;
22+
import io.quarkus.gizmo.Gizmo;
23+
24+
/**
25+
* Transforms {@link Scanner} in a way to take advantage of our build time knowledge
26+
* This should be removed completely if https://github.com/flyway/flyway/issues/2822
27+
* is implemented
28+
*/
29+
class ScannerTransformer implements BiFunction<String, ClassVisitor, ClassVisitor> {
30+
31+
static final String FLYWAY_SCANNER_CLASS_NAME = Scanner.class.getName();
32+
private static final String FLYWAY_SCANNER_INTERNAL_CLASS_NAME = FLYWAY_SCANNER_CLASS_NAME.replace('.', '/');
33+
34+
private static final String FLYWAY_RESOURCE_AND_CLASS_SCANNER_CLASS_NAME = ResourceAndClassScanner.class.getName();
35+
private static final String FLYWAY_RESOURCE_AND_CLASS_SCANNER_INTERNAL_CLASS_NAME = FLYWAY_RESOURCE_AND_CLASS_SCANNER_CLASS_NAME
36+
.replace('.', '/');
37+
38+
private static final String QUARKUS_RESOURCE_AND_CLASS_SCANNER_CLASS_NAME = QuarkusPathLocationScanner.class.getName();
39+
private static final String QUARKUS_RESOURCE_AND_CLASS_SCANNER_INTERNAL_CLASS_NAME = QUARKUS_RESOURCE_AND_CLASS_SCANNER_CLASS_NAME
40+
.replace('.', '/');
41+
42+
private static final String CTOR_METHOD_NAME = "<init>";
43+
44+
@Override
45+
public ClassVisitor apply(String s, ClassVisitor cv) {
46+
return new ScannerVisitor(cv);
47+
}
48+
49+
private static final class ScannerVisitor extends ClassVisitor {
50+
51+
public ScannerVisitor(ClassVisitor cv) {
52+
super(Gizmo.ASM_API_VERSION, cv);
53+
}
54+
55+
@Override
56+
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
57+
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
58+
if (name.equals(CTOR_METHOD_NAME)) {
59+
return new ConstructorTransformer(mv);
60+
}
61+
return mv;
62+
}
63+
64+
/**
65+
* Replaces the constructor of the {@link Scanner} with:
66+
*
67+
* <pre>
68+
* public ScannerSubstitutions(Class<?> implementedInterface, Collection<Location> locations, ClassLoader classLoader,
69+
* Charset encoding, ResourceNameCache resourceNameCache, LocationScannerCache locationScannerCache) {
70+
* ResourceAndClassScanner quarkusScanner = new QuarkusPathLocationScanner(locations);
71+
* resources.addAll(quarkusScanner.scanForResources());
72+
* classes.addAll(quarkusScanner.scanForClasses());
73+
* }
74+
* </pre>
75+
*/
76+
private static class ConstructorTransformer extends MethodVisitor {
77+
78+
public ConstructorTransformer(MethodVisitor mv) {
79+
super(Gizmo.ASM_API_VERSION, mv);
80+
}
81+
82+
@Override
83+
public void visitCode() {
84+
super.visitVarInsn(ALOAD, 0);
85+
super.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", CTOR_METHOD_NAME, "()V", false);
86+
super.visitVarInsn(ALOAD, 0);
87+
super.visitTypeInsn(NEW, "java/util/ArrayList");
88+
super.visitInsn(DUP);
89+
super.visitMethodInsn(INVOKESPECIAL, "java/util/ArrayList", CTOR_METHOD_NAME, "()V", false);
90+
super.visitFieldInsn(PUTFIELD, FLYWAY_SCANNER_INTERNAL_CLASS_NAME, "resources", "Ljava/util/List;");
91+
super.visitVarInsn(ALOAD, 0);
92+
super.visitTypeInsn(NEW, "java/util/ArrayList");
93+
super.visitInsn(DUP);
94+
super.visitMethodInsn(INVOKESPECIAL, "java/util/ArrayList", CTOR_METHOD_NAME, "()V", false);
95+
super.visitFieldInsn(PUTFIELD, FLYWAY_SCANNER_INTERNAL_CLASS_NAME, "classes", "Ljava/util/List;");
96+
super.visitTypeInsn(NEW, QUARKUS_RESOURCE_AND_CLASS_SCANNER_INTERNAL_CLASS_NAME);
97+
super.visitInsn(DUP);
98+
super.visitVarInsn(ALOAD, 2);
99+
super.visitMethodInsn(INVOKESPECIAL, QUARKUS_RESOURCE_AND_CLASS_SCANNER_INTERNAL_CLASS_NAME,
100+
CTOR_METHOD_NAME, "(Ljava/util/Collection;)V", false);
101+
super.visitVarInsn(ASTORE, 7);
102+
super.visitVarInsn(ALOAD, 0);
103+
super.visitFieldInsn(GETFIELD, FLYWAY_SCANNER_INTERNAL_CLASS_NAME, "resources", "Ljava/util/List;");
104+
super.visitVarInsn(ALOAD, 7);
105+
super.visitMethodInsn(INVOKEINTERFACE, FLYWAY_RESOURCE_AND_CLASS_SCANNER_INTERNAL_CLASS_NAME,
106+
"scanForResources", "()Ljava/util/Collection;", true);
107+
super.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "addAll", "(Ljava/util/Collection;)Z", true);
108+
super.visitInsn(POP);
109+
super.visitVarInsn(ALOAD, 0);
110+
super.visitFieldInsn(GETFIELD, FLYWAY_SCANNER_INTERNAL_CLASS_NAME, "classes", "Ljava/util/List;");
111+
super.visitVarInsn(ALOAD, 7);
112+
super.visitMethodInsn(INVOKEINTERFACE, FLYWAY_RESOURCE_AND_CLASS_SCANNER_INTERNAL_CLASS_NAME,
113+
"scanForClasses", "()Ljava/util/Collection;", true);
114+
super.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "addAll", "(Ljava/util/Collection;)Z", true);
115+
super.visitInsn(POP);
116+
super.visitInsn(RETURN);
117+
super.visitMaxs(3, 8);
118+
}
119+
}
120+
}
121+
}

extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartNamedDataSourceTest.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package io.quarkus.flyway.test;
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotNull;
45

56
import javax.inject.Inject;
67

78
import org.flywaydb.core.Flyway;
9+
import org.flywaydb.core.api.MigrationInfo;
810
import org.jboss.shrinkwrap.api.ShrinkWrap;
911
import org.jboss.shrinkwrap.api.spec.JavaArchive;
1012
import org.junit.jupiter.api.DisplayName;
@@ -32,7 +34,12 @@ public class FlywayExtensionMigrateAtStartNamedDataSourceTest {
3234
@Test
3335
@DisplayName("Migrates at start for datasource named 'users' correctly")
3436
public void testFlywayConfigInjection() {
35-
String currentVersion = flywayUsers.info().current().getVersion().toString();
37+
MigrationInfo migrationInfo = flywayUsers.info().current();
38+
assertNotNull(migrationInfo, "No Flyway migration was executed");
39+
40+
String currentVersion = migrationInfo
41+
.getVersion()
42+
.toString();
3643
// Expected to be 1.0.0 as migration runs at start
3744
assertEquals("1.0.0", currentVersion);
3845
}

extensions/flyway/deployment/src/test/java/io/quarkus/flyway/test/FlywayExtensionMigrateAtStartTest.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package io.quarkus.flyway.test;
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotNull;
45

56
import javax.inject.Inject;
67

78
import org.flywaydb.core.Flyway;
9+
import org.flywaydb.core.api.MigrationInfo;
810
import org.jboss.shrinkwrap.api.ShrinkWrap;
911
import org.jboss.shrinkwrap.api.spec.JavaArchive;
1012
import org.junit.jupiter.api.DisplayName;
@@ -27,7 +29,12 @@ public class FlywayExtensionMigrateAtStartTest {
2729
@Test
2830
@DisplayName("Migrates at start correctly")
2931
public void testFlywayConfigInjection() {
30-
String currentVersion = flyway.info().current().getVersion().toString();
32+
MigrationInfo migrationInfo = flyway.info().current();
33+
assertNotNull(migrationInfo, "No Flyway migration was executed");
34+
35+
String currentVersion = migrationInfo
36+
.getVersion()
37+
.toString();
3138
// Expected to be 1.0.0 as migration runs at start
3239
assertEquals("1.0.0", currentVersion);
3340
}

extensions/flyway/deployment/src/test/resources/migrate-at-start-config-named-datasource.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
quarkus.log.category."org.flywaydb.core".level=DEBUG
2+
quarkus.log.category."io.quarkus.flyway".level=DEBUG
13
quarkus.datasource.users.db-kind=h2
24
quarkus.datasource.users.username=sa
35
quarkus.datasource.users.password=sa

extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/FlywayRecorder.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,21 @@
77
import javax.sql.DataSource;
88

99
import org.flywaydb.core.Flyway;
10+
import org.jboss.logging.Logger;
1011

1112
import io.quarkus.agroal.runtime.DataSources;
1213
import io.quarkus.arc.Arc;
13-
import io.quarkus.flyway.runtime.graal.QuarkusPathLocationScanner;
1414
import io.quarkus.runtime.annotations.Recorder;
1515

1616
@Recorder
1717
public class FlywayRecorder {
1818

19+
private static final Logger log = Logger.getLogger(FlywayRecorder.class);
20+
1921
private final List<FlywayContainer> flywayContainers = new ArrayList<>(2);
2022

2123
public void setApplicationMigrationFiles(List<String> migrationFiles) {
24+
log.debugv("Setting the following application migration files: {0}", migrationFiles);
2225
QuarkusPathLocationScanner.setApplicationMigrationFiles(migrationFiles);
2326
}
2427

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package io.quarkus.flyway.runtime.graal;
1+
package io.quarkus.flyway.runtime;
22

33
import java.nio.charset.StandardCharsets;
44
import java.util.ArrayList;
@@ -12,6 +12,11 @@
1212
import org.flywaydb.core.internal.scanner.classpath.ResourceAndClassScanner;
1313
import org.jboss.logging.Logger;
1414

15+
/**
16+
* This class is used in order to prevent Flyway from doing classpath scanning which is both slow
17+
* and won't work in native mode
18+
*/
19+
@SuppressWarnings("rawtypes")
1520
public final class QuarkusPathLocationScanner implements ResourceAndClassScanner {
1621
private static final Logger LOGGER = Logger.getLogger(QuarkusPathLocationScanner.class);
1722
private static final String LOCATION_SEPARATOR = "/";
@@ -20,6 +25,8 @@ public final class QuarkusPathLocationScanner implements ResourceAndClassScanner
2025
private final Collection<LoadableResource> scannedResources;
2126

2227
public QuarkusPathLocationScanner(Collection<Location> locations) {
28+
LOGGER.debugv("Locations: {0}", locations);
29+
2330
this.scannedResources = new ArrayList<>();
2431
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
2532

@@ -50,6 +57,9 @@ private boolean canHandleMigrationFile(Collection<Location> locations, String mi
5057

5158
if (migrationFile.startsWith(locationPath)) {
5259
return true;
60+
} else {
61+
LOGGER.debugf("Migration file '%s' will be ignored because it does not start with '%s'", migrationFile,
62+
locationPath);
5363
}
5464
}
5565

extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/graal/ScannerSubstitutions.java

Lines changed: 0 additions & 45 deletions
This file was deleted.

0 commit comments

Comments
 (0)