diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevConfig.java new file mode 100644 index 0000000000000..f2c978730ccc8 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevConfig.java @@ -0,0 +1,18 @@ +package io.quarkus.deployment.dev; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public class DevConfig { + + /** + * Whether or not Quarkus should disable it's ability to not do a full restart + * when changes to classes are compatible with JVM instrumentation. + * If this is set to true, Quarkus will always restart on changes and never perform class redefinition. + */ + @ConfigItem(defaultValue = "true") + boolean instrumentation; + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java index cdf1eb6e045f1..066dccb6017eb 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java @@ -32,10 +32,15 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; import org.jboss.jandex.Index; import org.jboss.jandex.IndexView; import org.jboss.jandex.Indexer; +import org.jboss.jandex.MethodParameterInfo; import org.jboss.logging.Logger; import io.quarkus.bootstrap.runner.Timing; @@ -45,12 +50,18 @@ import io.quarkus.dev.spi.DevModeType; import io.quarkus.dev.spi.HotReplacementContext; import io.quarkus.dev.spi.HotReplacementSetup; +import io.quarkus.runtime.Startup; +import io.quarkus.runtime.StartupEvent; public class RuntimeUpdatesProcessor implements HotReplacementContext, Closeable { private static final Logger log = Logger.getLogger(RuntimeUpdatesProcessor.class); private static final String CLASS_EXTENSION = ".class"; + private static final DotName STARTUP_NAME = DotName.createSimple(Startup.class.getName()); + private static final DotName STARTUP_EVENT_NAME = DotName.createSimple(StartupEvent.class.getName()); + private static final DotName OBSERVES_NAME = DotName.createSimple("javax.enterprise.event.Observes"); + static volatile RuntimeUpdatesProcessor INSTANCE; private final Path applicationRoot; @@ -209,12 +220,15 @@ public boolean doScan(boolean userInitiated) throws IOException { classTransformers.apply(name, bytes)); } Index current = indexer.complete(); - boolean ok = true; - for (ClassInfo clazz : current.getKnownClasses()) { - ClassInfo old = lastStartIndex.getClassByName(clazz.name()); - if (!ClassComparisonUtil.isSameStructure(clazz, old)) { - ok = false; - break; + boolean ok = instrumentationEnabled() + && !containsStartupCode(current); + if (ok) { + for (ClassInfo clazz : current.getKnownClasses()) { + ClassInfo old = lastStartIndex.getClassByName(clazz.name()); + if (!ClassComparisonUtil.isSameStructure(clazz, old)) { + ok = false; + break; + } } } @@ -258,6 +272,36 @@ public boolean doScan(boolean userInitiated) throws IOException { return false; } + private Boolean instrumentationEnabled() { + ClassLoader old = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); + return ConfigProvider.getConfig() + .getOptionalValue("quarkus.dev.instrumentation", boolean.class).orElse(true); + } finally { + Thread.currentThread().setContextClassLoader(old); + } + } + + private boolean containsStartupCode(Index index) { + if (!index.getAnnotations(STARTUP_NAME).isEmpty()) { + return true; + } + List observesInstances = index.getAnnotations(OBSERVES_NAME); + if (!observesInstances.isEmpty()) { + for (AnnotationInstance observesInstance : observesInstances) { + if (observesInstance.target().kind() == AnnotationTarget.Kind.METHOD_PARAMETER) { + MethodParameterInfo methodParameterInfo = observesInstance.target().asMethodParameter(); + short paramPos = methodParameterInfo.position(); + if (STARTUP_EVENT_NAME.equals(methodParameterInfo.method().parameters().get(paramPos).name())) { + return true; + } + } + } + } + return false; + } + @Override public void addPreScanStep(Runnable runnable) { preScanSteps.add(runnable); diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java index 26e51b017e58d..de0b2406f5a7b 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java @@ -38,9 +38,9 @@ /** * @author Clement Escoffier - * + *

* NOTE to anyone diagnosing failures in this test, to run a single method use: - * + *

* mvn install -Dit.test=DevMojoIT#methodName */ @DisableForNative @@ -234,6 +234,48 @@ public void testThatInstrumentationBasedReloadWorks() throws MavenInvocationExce //verify that this was an instrumentation based reload Assertions.assertEquals(firstUuid, DevModeTestUtils.getHttpResponse("/app/uuid")); + + source = new File(testDir, "src/main/java/org/acme/HelloService.java"); + filter(source, Collections.singletonMap("\"Stuart\"", "\"Stuart Douglas\"")); + + // Wait until we get "Stuart Douglas" + await() + .pollDelay(100, TimeUnit.MILLISECONDS) + .atMost(1, TimeUnit.MINUTES) + .until(() -> DevModeTestUtils.getHttpResponse("/app/name").contains("Stuart Douglas")); + + //this bean observes startup event, so it should be different UUID + String secondUUid = DevModeTestUtils.getHttpResponse("/app/uuid"); + Assertions.assertNotEquals(secondUUid, firstUuid); + + //now disable instrumentation based restart, and try again + //change it back to hello + DevModeTestUtils.getHttpResponse("/app/disable"); + source = new File(testDir, "src/main/java/org/acme/HelloResource.java"); + filter(source, Collections.singletonMap("return \"" + uuid + "\";", "return \"hello\";")); + + // Wait until we get "hello" + await() + .pollDelay(100, TimeUnit.MILLISECONDS) + .atMost(1, TimeUnit.MINUTES).until(() -> DevModeTestUtils.getHttpResponse("/app/hello").contains("hello")); + + //verify that this was not instrumentation based reload + Assertions.assertNotEquals(secondUUid, DevModeTestUtils.getHttpResponse("/app/uuid")); + secondUUid = DevModeTestUtils.getHttpResponse("/app/uuid"); + + //now re-enable + //and repeat + DevModeTestUtils.getHttpResponse("/app/enable"); + source = new File(testDir, "src/main/java/org/acme/HelloResource.java"); + filter(source, Collections.singletonMap("return \"hello\";", "return \"" + uuid + "\";")); + + // Wait until we get uuid + await() + .pollDelay(100, TimeUnit.MILLISECONDS) + .atMost(1, TimeUnit.MINUTES).until(() -> DevModeTestUtils.getHttpResponse("/app/hello").contains(uuid)); + + //verify that this was an instrumentation based reload + Assertions.assertEquals(secondUUid, DevModeTestUtils.getHttpResponse("/app/uuid")); } @Test diff --git a/integration-tests/maven/src/test/resources/projects/classic-inst/src/main/java/org/acme/HelloResource.java b/integration-tests/maven/src/test/resources/projects/classic-inst/src/main/java/org/acme/HelloResource.java index 526931d80d983..8b5962d3b5d4b 100644 --- a/integration-tests/maven/src/test/resources/projects/classic-inst/src/main/java/org/acme/HelloResource.java +++ b/integration-tests/maven/src/test/resources/projects/classic-inst/src/main/java/org/acme/HelloResource.java @@ -43,4 +43,16 @@ public String name() { public String uuid() { return uuid.toString(); } + + @GET + @Path("disable") + public void disable() { + System.setProperty("quarkus.dev.instrumentation", "false"); + } + + @GET + @Path("enable") + public void enable() { + System.setProperty("quarkus.dev.instrumentation","true"); + } } diff --git a/integration-tests/maven/src/test/resources/projects/classic-inst/src/main/java/org/acme/HelloService.java b/integration-tests/maven/src/test/resources/projects/classic-inst/src/main/java/org/acme/HelloService.java index beab6ed31e750..bfe7fc496c202 100644 --- a/integration-tests/maven/src/test/resources/projects/classic-inst/src/main/java/org/acme/HelloService.java +++ b/integration-tests/maven/src/test/resources/projects/classic-inst/src/main/java/org/acme/HelloService.java @@ -1,10 +1,17 @@ package org.acme; +import io.quarkus.runtime.StartupEvent; + import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; @ApplicationScoped public class HelloService { + public void start(@Observes StartupEvent startupEvent) { + + } + public String name() { return "Stuart"; }