Skip to content

Commit

Permalink
Add support for ClassLoader config
Browse files Browse the repository at this point in the history
Fixes #11962
  • Loading branch information
stuartwdouglas committed Jan 18, 2021
1 parent a43d7b8 commit 3a6b561
Show file tree
Hide file tree
Showing 12 changed files with 338 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.quarkus.deployment.configuration;

import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;

/**
* WARNING: This is not normal quarkus config, this is only read from application.properties.
* <p>
* This is because it is needed before any of the config infrastructure is setup.
*/
@ConfigRoot(phase = ConfigPhase.BUILD_TIME)
public class ClassLoadingConfig {

/**
* Artifacts that are loaded in a parent first manner. This can be used to work around issues where a given
* class needs to be loaded by the system ClassLoader. Note that if you
* make a library parent first all its dependencies should generally also be parent first.
* <p>
* Artifacts should be configured as a comma separated list of artifact ids, with the group, artifact-id and optional
* classifier separated by a colon.
* <p>
* WARNING: This config property can only be set in application.properties
*/
@ConfigItem(defaultValue = "")
public Optional<String> parentFirstArtifacts;

/**
* Artifacts that are loaded in the runtime ClassLoader in dev mode, so they will be dropped
* and recreated on change.
* <p>
* This is an advanced option, it should only be used if you have a problem with
* libraries holding stale state between reloads. Note that if you use this any library that depends on the listed libraries
* will also need to be reloadable.
* <p>
* This setting has no impact on production builds.
* <p>
* Artifacts should be configured as a comma separated list of artifact ids, with the group, artifact-id and optional
* classifier separated by a colon.
* <p>
* WARNING: This config property can only be set in application.properties
*/
@ConfigItem(defaultValue = "")
public Optional<String> reloadableArtifacts;

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
import io.quarkus.dev.spi.DevModeType;
import io.quarkus.dev.spi.HotReplacementSetup;
import io.quarkus.runner.bootstrap.AugmentActionImpl;
import io.quarkus.runner.bootstrap.StartupActionImpl;
import io.quarkus.runtime.ApplicationLifecycleManager;
import io.quarkus.runtime.configuration.QuarkusConfigFactory;
import io.quarkus.runtime.logging.LoggingSetupRecorder;
Expand Down Expand Up @@ -75,7 +74,7 @@ private synchronized void firstStart(QuarkusClassLoader deploymentClassLoader, L
boolean augmentDone = false;
//ok, we have resolved all the deps
try {
StartupAction start = (StartupActionImpl) augmentAction.createInitialRuntimeApplication();
StartupAction start = augmentAction.createInitialRuntimeApplication();
//this is a bit yuck, but we need replace the default
//exit handler in the runtime class loader
//TODO: look at implementing a common core classloader, that removes the need for this sort of crappy hack
Expand Down Expand Up @@ -291,7 +290,6 @@ public void close() {
//the main entry point, but loaded inside the augmentation class loader
@Override
public void accept(CuratedApplication o, Map<String, Object> params) {
Timing.staticInitStarted(o.getBaseRuntimeClassLoader());
//https://github.com/quarkusio/quarkus/issues/9748
//if you have an app with all daemon threads then the app thread
//may be the only thread keeping the JVM alive
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public static void setRuntimeDefaultProfile(final String profile) {
runtimeDefaultProfile = profile;
}

//NOTE: changes made here must be replicated in BootstrapProfileManager
public static String getActiveProfile() {
if (launchMode == LaunchMode.TEST) {
String profile = System.getProperty(QUARKUS_TEST_PROFILE_PROP);
Expand Down
48 changes: 29 additions & 19 deletions docs/src/main/asciidoc/class-loading-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,6 @@ a normal production Quarkus application.
For all other use cases (e.g. tests, dev mode, and building the application) Quarkus
uses the class loading architecture outlined here.

== Reading Class Bytecode

It is important to use the correct `ClassLoader`. The recommended approach is to get it by calling the
`Thread.currentThread().getContextClassLoader()` method.

Example:


[source,java,subs=attributes+]
----
@BuildStep
GeneratedClassBuildItem instrument(final CombinedIndexBuildItem index) {
final String classname = "com.example.SomeClass";
final ClassLoader cl = Thread.currentThread().getContextClassLoader();
final byte[] originalBytecode = IoUtil.readClassAsBytes(cl, classname);
final byte[] enhancedBytecode = ... // class instrumentation from originalBytecode
return new GeneratedClassBuildItem(true, classname, enhancedBytecode));
}
----

== Bootstrapping Quarkus

Expand Down Expand Up @@ -191,3 +172,32 @@ file). Simply add an `excludedArtifacts` section as shown below:

This should only be done if the extension depends on a newer version of these artifacts. If the extension does not bring
in a replacement artifact as a dependency then classes the application needs might end up missing.

== Configuring Class Loading

It is possible to configure some aspects of class loading in dev and test mode. This can be done using `application.properties`.
Note that class loading config is different to normal config, in that it does not use the standard Quarkus config mechanisms
(as it is needed too early), so only supports `application.properties`. The following options are supported.


include::{generated-dir}/config/quarkus-class-loading-configuration-class-loading-config.adoc[opts=optional, leveloffset=+1]

== Reading Class Bytecode

It is important to use the correct `ClassLoader`. The recommended approach is to get it by calling the
`Thread.currentThread().getContextClassLoader()` method.

Example:


[source,java,subs=attributes+]
----
@BuildStep
GeneratedClassBuildItem instrument(final CombinedIndexBuildItem index) {
final String classname = "com.example.SomeClass";
final ClassLoader cl = Thread.currentThread().getContextClassLoader();
final byte[] originalBytecode = IoUtil.readClassAsBytes(cl, classname);
final byte[] enhancedBytecode = ... // class instrumentation from originalBytecode
return new GeneratedClassBuildItem(true, classname, enhancedBytecode));
}
----
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.quarkus.vertx.http.devmode;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusDevModeTest;
import io.restassured.RestAssured;

/**
* tests the reload-dependencies option
*/
public class LiveReloadArtifactTest {

@RegisterExtension
static final QuarkusDevModeTest test = new QuarkusDevModeTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addAsResource(new StringAsset("quarkus.class-loading.reloadable-artifacts=io.vertx:vertx-web-client\n"),
"application.properties")
.addClasses(LiveReloadEndpoint.class));

@Test
public void test() {
String firstClassToString = RestAssured.get("/test").then().statusCode(200).extract().body().asString();
test.modifySourceFile(LiveReloadEndpoint.class, s -> s.replace("\"/test\"", "\"/test2\""));

String secondClassToString = RestAssured.get("/test2").then().statusCode(200).extract().body().asString();
Assertions.assertNotEquals(firstClassToString, secondClassToString);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.quarkus.vertx.http.devmode;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import javax.inject.Named;

import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.ext.web.client.WebClient;

@Named
@ApplicationScoped
public class LiveReloadEndpoint {

@Inject
HttpBuildTimeConfig httpConfig;

void addConfigRoute(@Observes Router router) {
router.route("/test")
.produces("text/plain")
.handler(rc -> rc.response().end(WebClient.class.hashCode() + "-" + HttpRequest.class.hashCode()));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.quarkus.vertx.http.devmode;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusDevModeTest;
import io.restassured.RestAssured;

/**
* tests the parent first artifacts option
*/
public class ParentFirstArtifactTest {

@RegisterExtension
static final QuarkusDevModeTest test = new QuarkusDevModeTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addAsResource(new StringAsset("quarkus.class-loading.parent-first-artifacts=io.vertx:vertx-web-client\n"),
"application.properties")
.addClasses(ParentFirstEndpoint.class));

@Test
public void test() {
String firstClassToString = RestAssured.get("/test").then().statusCode(200).extract().body().asString();
Assertions.assertEquals("false", firstClassToString);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.quarkus.vertx.http.devmode;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import javax.inject.Named;

import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.client.WebClient;

@Named
@ApplicationScoped
public class ParentFirstEndpoint {

@Inject
HttpBuildTimeConfig httpConfig;

void addConfigRoute(@Observes Router router) {
router.route("/test")
.produces("text/plain")
.handler(rc -> rc.response()
.end(WebClient.class.getClassLoader() instanceof QuarkusClassLoader ? "true" : "false"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.quarkus.bootstrap.app;

/**
* Mirror of the logic in ProfileManager, but this is needed pre-bootstrap so there is nowhere to really share it.
*
* This is only used for reading the class loading config
*/
public class BootstrapProfile {

public static final String QUARKUS_PROFILE_ENV = "QUARKUS_PROFILE";
public static final String QUARKUS_PROFILE_PROP = "quarkus.profile";
public static final String QUARKUS_TEST_PROFILE_PROP = "quarkus.test.profile";
private static final String BACKWARD_COMPATIBLE_QUARKUS_PROFILE_PROP = "quarkus-profile";
public static final String DEV = "dev";
public static final String PROD = "prod";
public static final String TEST = "test";

private static String runtimeDefaultProfile = null;

public static void setRuntimeDefaultProfile(final String profile) {
runtimeDefaultProfile = profile;
}

public static String getActiveProfile(QuarkusBootstrap.Mode mode) {
if (mode == QuarkusBootstrap.Mode.TEST) {
String profile = System.getProperty(QUARKUS_TEST_PROFILE_PROP);
if (profile != null) {
return profile;
}
return "test";
}

String profile = System.getProperty(QUARKUS_PROFILE_PROP);
if (profile != null) {
return profile;
}

profile = System.getProperty(BACKWARD_COMPATIBLE_QUARKUS_PROFILE_PROP);
if (profile != null) {
return profile;
}

profile = System.getenv(QUARKUS_PROFILE_ENV);
if (profile != null) {
return profile;
}

profile = runtimeDefaultProfile;
if (profile != null) {
return profile;
}
switch (mode) {
case REMOTE_DEV_SERVER:
case DEV:
return DEV;
case REMOTE_DEV_CLIENT:
case PROD:
return PROD;
case TEST:
return TEST;
default:
throw new RuntimeException("unknown mode:" + mode);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.bootstrap.app;

import io.quarkus.bootstrap.model.AppArtifactKey;
import java.io.Serializable;
import java.util.Set;

public class ConfiguredClassLoading implements Serializable {

public final Set<AppArtifactKey> parentFirstArtifacts;
public final Set<AppArtifactKey> reloadableArtifacts;

public ConfiguredClassLoading(Set<AppArtifactKey> parentFirstArtifacts, Set<AppArtifactKey> reloadableArtifacts) {
this.parentFirstArtifacts = parentFirstArtifacts;
this.reloadableArtifacts = reloadableArtifacts;
}
}
Loading

0 comments on commit 3a6b561

Please sign in to comment.