diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 1a0a5c3377..ba49c1e0b9 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -32,6 +32,7 @@ endif::[] [float] ===== Bug fixes +* Fix module loading errors on J9 JVM - {pull}2341[#2341] [[release-notes-1.x]] === Java Agent version 1.x diff --git a/apm-agent-bootstrap/pom.xml b/apm-agent-bootstrap/pom.xml new file mode 100644 index 0000000000..cf5893c2cb --- /dev/null +++ b/apm-agent-bootstrap/pom.xml @@ -0,0 +1,138 @@ + + + 4.0.0 + + + apm-agent-parent + co.elastic.apm + 1.28.2-SNAPSHOT + + + apm-agent-bootstrap + ${project.groupId}:${project.artifactId} + + + true + + ${project.basedir}/.. + + + true + + ${project.basedir}/target/tmp + + + + + + maven-compiler-plugin + + + compile-java7 + + compile + + + + bootstrap/dispatcher/**/*.java + + + bootstrap/modulesetter + + + + + compile-java9 + + compile + + + 9 + 9 + + bootstrap/modulesetter/**/*.java + + + bootstrap/dispatcher + + + + + + + maven-shade-plugin + + + shade-classes + package + + shade + + + + + + + + bootstrap.dispatcher + java.lang + + + + bootstrap.modulesetter + co.elastic.apm.agent.modulesetter + + + + + + maven-antrun-plugin + + + rename-classes + package + + run + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apm-agent-core/src/main/resources/bootstrap/IndyBootstrapDispatcher.java b/apm-agent-bootstrap/src/main/java/bootstrap/dispatcher/IndyBootstrapDispatcher.java similarity index 83% rename from apm-agent-core/src/main/resources/bootstrap/IndyBootstrapDispatcher.java rename to apm-agent-bootstrap/src/main/java/bootstrap/dispatcher/IndyBootstrapDispatcher.java index 317ff6637f..fdb50c59d4 100644 --- a/apm-agent-core/src/main/resources/bootstrap/IndyBootstrapDispatcher.java +++ b/apm-agent-bootstrap/src/main/java/bootstrap/dispatcher/IndyBootstrapDispatcher.java @@ -1,3 +1,4 @@ +package bootstrap.dispatcher; /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -16,7 +17,6 @@ * specific language governing permissions and limitations * under the License. */ -package java.lang; import java.lang.invoke.CallSite; import java.lang.invoke.ConstantCallSite; @@ -26,7 +26,18 @@ import java.lang.reflect.Array; import java.lang.reflect.Method; +/** + * Indy bootstrap dispatcher + *

+ * IMPORTANT: This class is relocated in a different package and stored as a classpath resource to be injected into bootstrap classloader. + * A copy of this resource is stored in 'src/main/resources' and should be updated by running 'mvn clean package' whenever + * this class is being modified. This has only an effect when running code/tests in the IDE as the resources are loaded + * from the project classpath and not the packaged artifact. + *

+ */ +@SuppressWarnings("unused") public class IndyBootstrapDispatcher { + public static Method bootstrap; private static final MethodHandle VOID_NOOP; diff --git a/apm-indy-bootstrap-module/src/main/java/co/elastic/apm/agent/bci/IndyBootstrapDispatcherModuleSetter.java b/apm-agent-bootstrap/src/main/java/bootstrap/modulesetter/ModuleSetter.java similarity index 69% rename from apm-indy-bootstrap-module/src/main/java/co/elastic/apm/agent/bci/IndyBootstrapDispatcherModuleSetter.java rename to apm-agent-bootstrap/src/main/java/bootstrap/modulesetter/ModuleSetter.java index f0a01e49a8..80573f0148 100644 --- a/apm-indy-bootstrap-module/src/main/java/co/elastic/apm/agent/bci/IndyBootstrapDispatcherModuleSetter.java +++ b/apm-agent-bootstrap/src/main/java/bootstrap/modulesetter/ModuleSetter.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.bci; +package bootstrap.modulesetter; import sun.misc.Unsafe; @@ -27,16 +27,25 @@ *

* NOTE: This class is compiled with Java 9 so it must only be loaded though reflection and only when running on Java 9. * In addition, since it relies on the {@link Unsafe} API, it must be loaded by the bootstrap or platform class loaders. + *

+ *

+ * IMPORTANT: This class is relocated in a different package and stored as a classpath resource to be injected into bootstrap classloader. + * A copy of this resource is stored in 'src/main/resources' and should be updated by running 'mvn clean package' whenever + * this class is being modified. This has only an effect when running code/tests in the IDE as the resources are loaded + * from the project classpath and not the packaged artifact. + *

*/ + @SuppressWarnings("unused") -public class IndyBootstrapDispatcherModuleSetter { +public class ModuleSetter { + + public static void setJavaBaseModule(Class targetClass) throws Exception { - public static void setJavaBaseModule(Class indyBootstrapDispatcherClass) throws Exception { Field moduleField = Class.class.getDeclaredField("module"); if (moduleField.getType() == Module.class) { Module javaBaseModule = Class.class.getModule(); Unsafe unsafe = Unsafe.getUnsafe(); - unsafe.putObject(indyBootstrapDispatcherClass, unsafe.objectFieldOffset(moduleField), javaBaseModule); + unsafe.putObject(targetClass, unsafe.objectFieldOffset(moduleField), javaBaseModule); } else { throw new IllegalStateException("Unexpected module field type: " + moduleField.getType().getName()); } diff --git a/apm-agent-bootstrap/src/main/resources/bootstrap/co/elastic/apm/agent/modulesetter/ModuleSetter.esclazz b/apm-agent-bootstrap/src/main/resources/bootstrap/co/elastic/apm/agent/modulesetter/ModuleSetter.esclazz new file mode 100644 index 0000000000..b73dbacd2f Binary files /dev/null and b/apm-agent-bootstrap/src/main/resources/bootstrap/co/elastic/apm/agent/modulesetter/ModuleSetter.esclazz differ diff --git a/apm-agent-bootstrap/src/main/resources/bootstrap/java/lang/IndyBootstrapDispatcher.esclazz b/apm-agent-bootstrap/src/main/resources/bootstrap/java/lang/IndyBootstrapDispatcher.esclazz new file mode 100644 index 0000000000..544cd281f8 Binary files /dev/null and b/apm-agent-bootstrap/src/main/resources/bootstrap/java/lang/IndyBootstrapDispatcher.esclazz differ diff --git a/apm-agent-core/pom.xml b/apm-agent-core/pom.xml index d8609e4e2c..ae19d57920 100644 --- a/apm-agent-core/pom.xml +++ b/apm-agent-core/pom.xml @@ -93,10 +93,11 @@ ${project.groupId} - apm-indy-bootstrap-module + apm-agent-bootstrap ${project.version} - test + runtime + com.squareup.okhttp3 okhttp diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/IndyBootstrap.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/IndyBootstrap.java index 5902a50ed0..efb2472fb2 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/IndyBootstrap.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/IndyBootstrap.java @@ -168,19 +168,28 @@ public class IndyBootstrap { /** - * Starts with {@code java.lang} so that OSGi class loaders don't restrict access to it + * Starts with {@code java.lang} so that OSGi class loaders don't restrict access to it. + * This also allows to load it in {@code java.base} module on Java9+ for Hotspot, Open J9 requires {@code ModuleSetter} */ private static final String INDY_BOOTSTRAP_CLASS_NAME = "java.lang.IndyBootstrapDispatcher"; + + /** + * The class file of {@code IndyBootstrapDispatcher}, loaded from classpath resource, {@code esclazz} extension avoids + * being loaded as a regular class. + */ + private static final String INDY_BOOTSTRAP_RESOURCE = "bootstrap/java/lang/IndyBootstrapDispatcher.esclazz"; + /** * Needs to be loaded from the bootstrap CL because it uses {@code sun.misc.Unsafe}. * In addition, needs to be loaded explicitly by name only when running on Java 9, because compiled with Java 9 */ - private static final String INDY_BOOTSTRAP_MODULE_SETTER_CLASS_NAME = "co.elastic.apm.agent.bci.IndyBootstrapDispatcherModuleSetter"; + private static final String INDY_BOOTSTRAP_MODULE_SETTER_CLASS_NAME = "co.elastic.apm.agent.modulesetter.ModuleSetter"; + /** - * The class file of {@code java.lang.IndyBootstrapDispatcher}. - * Ends with {@code clazz} because if it ended with {@code class}, it would be loaded like a regular class. + * The class file of {@code ModuleSetter}, loaded from classpath resource, {@code esclazz} extension avoids being + * loaded as a regular class. */ - private static final String INDY_BOOTSTRAP_RESOURCE = "bootstrap/IndyBootstrapDispatcher.clazz"; + private static final String INDY_BOOTSTRAP_MODULE_SETTER_RESOURCE = "bootstrap/co/elastic/apm/agent/modulesetter/ModuleSetter.esclazz"; /** * The name of the class we use as the lookup class during the invokedynamic bootstrap flow. The bytecode of this @@ -220,17 +229,7 @@ public static Method getIndyBootstrapMethod(final Logger logger) { * Injects the {@code java.lang.IndyBootstrapDispatcher} class into the bootstrap class loader if it wasn't already. */ private static Class initIndyBootstrap(final Logger logger) throws Exception { - Class indyBootstrapDispatcherClass; - try { - indyBootstrapDispatcherClass = Class.forName(INDY_BOOTSTRAP_CLASS_NAME, false, null); - } catch (ClassNotFoundException e) { - byte[] bootstrapClass = IOUtils.readToBytes(ElasticApmAgent.getAgentClassLoader().getResourceAsStream(INDY_BOOTSTRAP_RESOURCE)); - if (bootstrapClass == null || bootstrapClass.length == 0) { - throw new IllegalStateException("Could not locate " + INDY_BOOTSTRAP_RESOURCE); - } - ClassInjector.UsingUnsafe.ofBootLoader().injectRaw(Collections.singletonMap(INDY_BOOTSTRAP_CLASS_NAME, bootstrapClass)); - indyBootstrapDispatcherClass = Class.forName(INDY_BOOTSTRAP_CLASS_NAME, false, null); - } + Class indyBootstrapDispatcherClass = loadClassInBootstrap(INDY_BOOTSTRAP_CLASS_NAME, INDY_BOOTSTRAP_RESOURCE); if (JvmRuntimeInfo.ofCurrentVM().getMajorVersion() >= 9 && JvmRuntimeInfo.ofCurrentVM().isJ9VM()) { try { @@ -243,6 +242,42 @@ private static Class initIndyBootstrap(final Logger logger) throws Exception return indyBootstrapDispatcherClass; } + /** + * Loads a class from classpath resource in bootstrap classloader. + *

+ * Ensuring that classes loaded through this method can ONLY be loaded in the bootstrap CL requires the following: + *

+ * + * @param className class name + * @param resourceName class resource name + * @return class loaded in bootstrap classloader + * @throws IOException if unable to open provided resource + * @throws ClassNotFoundException if unable to load class in bootstrap CL + */ + private static Class loadClassInBootstrap(String className, String resourceName) throws IOException, ClassNotFoundException { + Class bootstrapClass; + try { + // Will return non-null value only if the class has already been loaded. + // Ensuring that a class can ONLY be loaded through this method and not from regular classloading relies + // on applying the listed instructions in method documentation + bootstrapClass = Class.forName(className, false, null); + } catch (ClassNotFoundException e) { + byte[] classBytes = IOUtils.readToBytes(ElasticApmAgent.getAgentClassLoader().getResourceAsStream(resourceName)); + if (classBytes == null || classBytes.length == 0) { + throw new IllegalStateException("Could not locate " + resourceName); + } + ClassInjector.UsingUnsafe.ofBootLoader().injectRaw(Collections.singletonMap(className, classBytes)); + bootstrapClass = Class.forName(className, false, null); + } + return bootstrapClass; + } + + /** * A package-private method for unit-testing of the module overriding functionality * @@ -252,7 +287,8 @@ private static Class initIndyBootstrap(final Logger logger) throws Exception static void setJavaBaseModule(Class targetClass) throws Throwable { // In order to override the original unnamed module assigned to the IndyBootstrapDispatcher, we rely on the // Unsafe API, which requires the caller to be loaded by the Bootstrap CL - Class moduleSetterClass = Class.forName(INDY_BOOTSTRAP_MODULE_SETTER_CLASS_NAME, false, null); + + Class moduleSetterClass = loadClassInBootstrap(INDY_BOOTSTRAP_MODULE_SETTER_CLASS_NAME, INDY_BOOTSTRAP_MODULE_SETTER_RESOURCE); MethodHandles.lookup() .findStatic(moduleSetterClass, "setJavaBaseModule", MethodType.methodType(void.class, Class.class)) .invoke(targetClass); diff --git a/apm-agent-core/src/main/resources/bootstrap/IndyBootstrapDispatcher.clazz b/apm-agent-core/src/main/resources/bootstrap/IndyBootstrapDispatcher.clazz deleted file mode 100644 index 4528ca7da9..0000000000 Binary files a/apm-agent-core/src/main/resources/bootstrap/IndyBootstrapDispatcher.clazz and /dev/null differ diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/IndyBootstrapTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/IndyBootstrapTest.java index d23d39741a..5139cf7014 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/IndyBootstrapTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/IndyBootstrapTest.java @@ -19,12 +19,7 @@ package co.elastic.apm.agent.bci; import co.elastic.apm.agent.AbstractInstrumentationTest; -import net.bytebuddy.dynamic.loading.ClassInjector; import org.junit.jupiter.api.Test; -import org.stagemonitor.util.IOUtils; - -import java.io.InputStream; -import java.util.Collections; import static org.assertj.core.api.Assertions.assertThat; @@ -35,12 +30,6 @@ void testSetJavaBaseModule() throws Throwable { Module javaBaseModule = Class.class.getModule(); assertThat(IndyBootstrapTest.class.getModule()).isNotEqualTo(javaBaseModule); - // In order to test this functionality, IndyBootstrapDispatcherModuleSetter needs to be loaded from the Boot CL. - // We don't mind loading it with the test's class loader as well only to get it's class file - InputStream classFileAsStream = IndyBootstrapDispatcherModuleSetter.class.getResourceAsStream("IndyBootstrapDispatcherModuleSetter.class"); - byte[] bootstrapClass = IOUtils.readToBytes(classFileAsStream); - ClassInjector.UsingUnsafe.ofBootLoader().injectRaw(Collections.singletonMap(IndyBootstrapDispatcherModuleSetter.class.getName(), bootstrapClass)); - IndyBootstrap.setJavaBaseModule(IndyBootstrapTest.class); assertThat(IndyBootstrapTest.class.getModule()).isEqualTo(javaBaseModule); } diff --git a/apm-agent/pom.xml b/apm-agent/pom.xml index 25d35b8025..76dfcd2f77 100644 --- a/apm-agent/pom.xml +++ b/apm-agent/pom.xml @@ -36,7 +36,7 @@
${project.groupId} - apm-indy-bootstrap-module + apm-agent-bootstrap ${project.version} diff --git a/apm-indy-bootstrap-module/pom.xml b/apm-indy-bootstrap-module/pom.xml deleted file mode 100644 index feefee45d3..0000000000 --- a/apm-indy-bootstrap-module/pom.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - 4.0.0 - - - apm-agent-parent - co.elastic.apm - 1.28.2-SNAPSHOT - - - apm-indy-bootstrap-module - ${project.groupId}:${project.artifactId} - - - true - - ${project.basedir}/.. - - 9 - - - ${maven.compiler.target} - - - true - - diff --git a/elastic-apm-agent/pom.xml b/elastic-apm-agent/pom.xml index 993e018fd1..0c6d6d6d51 100644 --- a/elastic-apm-agent/pom.xml +++ b/elastic-apm-agent/pom.xml @@ -106,7 +106,6 @@ maven-antrun-plugin - 3.0.0 shade-cached-lookup-key diff --git a/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/TomcatIT.java b/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/TomcatIT.java index c48a5ee0bf..7ff8a75947 100644 --- a/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/TomcatIT.java +++ b/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/TomcatIT.java @@ -48,9 +48,8 @@ public static Iterable data() { {"9-jre11-slim"}, {"9.0.39-jdk14-openjdk-oracle"}, {"jdk8-adoptopenjdk-openj9"}, - // TODO openj9 on JDK11 has an access problem from java.base - //{"jdk11-adoptopenjdk-openj9"}, - //{"9.0.50-jdk11-adoptopenjdk-openj9"} + {"jdk11-adoptopenjdk-openj9"}, + {"9.0.50-jdk11-adoptopenjdk-openj9"} }); } diff --git a/pom.xml b/pom.xml index da41fefc2a..117b4417a4 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,7 @@ integration-tests apm-agent-attach apm-agent-plugin-sdk - apm-indy-bootstrap-module + apm-agent-bootstrap apm-agent apm-agent-attach-cli apm-agent-common @@ -514,6 +514,10 @@ maven-site-plugin 3.8.2 + + maven-antrun-plugin + 3.0.0 +