Support URLClassLoader addURL
ADD
instrumentation/classloaders/javaagent-integration-tests/javaagent-integration-tests.gradle
+ext.skipPublish = true
+
+apply from: "$rootDir/gradle/instrumentation.gradle"
+
+dependencies {
+ testImplementation group: "org.apache.commons", name: "commons-lang3", version: "3.12.0"
+ testImplementation group: "commons-io", name: "commons-io", version: "2.8.0"
+}

ADD
instrumentation/classloaders/javaagent-integration-tests/src/main/java/instrumentation/TestInstrumentationModule.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package instrumentation;
+
+import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.ClassLoaderMatcher.hasClassesNamed;
+import static java.util.Collections.singletonList;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.javaagent.tooling.InstrumentationModule;
+import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+@AutoService(InstrumentationModule.class)
+public class TestInstrumentationModule extends InstrumentationModule {
+ public TestInstrumentationModule() {
+ super("test-instrumentation");
+ }
+
+ @Override
+ public List<TypeInstrumentation> typeInstrumentations() {
+ return singletonList(new TestTypeInstrumentation());
+ }
+
+ public static class TestTypeInstrumentation implements TypeInstrumentation {
+ @Override
+ public ElementMatcher<ClassLoader> classLoaderOptimization() {
+ return hasClassesNamed("org.apache.commons.lang3.SystemUtils");
+ }
+
+ @Override
+ public ElementMatcher<? super TypeDescription> typeMatcher() {
+ return named("org.apache.commons.lang3.SystemUtils");
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+ return Collections.singletonMap(named("getHostName"), MarkInstrumentedAdvice.class.getName());
+ }
+ }
+
+ public static class MarkInstrumentedAdvice {
+ @Advice.OnMethodExit
+ public static void methodExit(@Advice.Return(readOnly = false) String hostName) {
+ hostName = "not-the-host-name";
+ }
+ }
+}

ADD
instrumentation/classloaders/javaagent-integration-tests/src/test/groovy/AddUrlTest.groovy
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification
+import org.apache.commons.io.IOUtils
+import org.apache.commons.lang3.SystemUtils
+
+class AddUrlTest extends AgentInstrumentationSpecification {
+
+ def "should instrument class after it is loaded via addURL"() {
+ given:
+ TestURLClassLoader loader = new TestURLClassLoader()
+
+ when:
+ // this is just to verify the assumption that TestURLClassLoader is not finding SystemUtils via
+ // the test class path (in which case the verification below would not be very meaningful)
+ loader.loadClass(SystemUtils.getName())
+
+ then:
+ thrown ClassNotFoundException
+
+ when:
+ // loading a class in the URLClassLoader in order to trigger
+ // a negative cache hit on org.apache.commons.lang3.SystemUtils
+ loader.addURL(IOUtils.getProtectionDomain().getCodeSource().getLocation())
+ loader.loadClass(IOUtils.getName())
+
+ loader.addURL(SystemUtils.getProtectionDomain().getCodeSource().getLocation())
+ def clazz = loader.loadClass(SystemUtils.getName())
+
+ then:
+ clazz.getClassLoader() == loader
+ clazz.getMethod("getHostName").invoke(null) == "not-the-host-name"
+ }
+
+ static class TestURLClassLoader extends URLClassLoader {
+
+ TestURLClassLoader() {
+ super(new URL[0], (ClassLoader) null)
+ }
+ }
+}

MODIFY
instrumentation/classloaders/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaclassloader/ClassLoaderInstrumentationModule.java
instrumentation/classloaders/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaclassloader/ClassLoaderInstrumentationModule.java
@@ -27,6 +27,7 @@ public class ClassLoaderInstrumentationModule extends InstrumentationModule {
public List<TypeInstrumentation> typeInstrumentations() {
return asList(
new ClassLoaderInstrumentation(),
+ new UrlClassLoaderInstrumentation(),
new ProxyInstrumentation(),
new ResourceInjectionInstrumentation());
}

ADD
instrumentation/classloaders/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaclassloader/UrlClassLoaderInstrumentation.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.javaclassloader;
+
+import static java.util.Collections.singletonMap;
+import static net.bytebuddy.matcher.ElementMatchers.isMethod;
+import static net.bytebuddy.matcher.ElementMatchers.isProtected;
+import static net.bytebuddy.matcher.ElementMatchers.isStatic;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.not;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import io.opentelemetry.javaagent.bootstrap.ClassLoaderMatcherCacheHolder;
+import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Map;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class UrlClassLoaderInstrumentation implements TypeInstrumentation {
+
+ @Override
+ public ElementMatcher<TypeDescription> typeMatcher() {
+ return named("java.net.URLClassLoader");
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+ return singletonMap(
+ isMethod()
+ .and(named("addURL"))
+ .and(takesArguments(1))
+ .and(takesArgument(0, URL.class))
+ .and(isProtected())
+ .and(not(isStatic())),
+ UrlClassLoaderInstrumentation.class.getName() + "$InvalidateClassLoaderMatcher");
+ }
+
+ public static class InvalidateClassLoaderMatcher {
+ @Advice.OnMethodExit(suppress = Throwable.class)
+ public static void onExit(@Advice.This URLClassLoader loader) {
+ ClassLoaderMatcherCacheHolder.invalidateAllCachesForClassLoader(loader);
+ }
+ }
+}

ADD
javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/ClassLoaderMatcherCacheHolder.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.bootstrap;
+
+import io.opentelemetry.instrumentation.api.caching.Cache;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import org.checkerframework.checker.lock.qual.GuardedBy;
+
+/**
+ * A holder of all ClassLoaderMatcher caches. We store them in the bootstrap classloader so that
+ * instrumentation can invalidate the ClassLoaderMatcher for a particular ClassLoader, e.g. when
+ * {@link java.net.URLClassLoader#addURL(URL)} is called.
+ */
+public class ClassLoaderMatcherCacheHolder {
+
+ @GuardedBy("allCaches")
+ private static final List<Cache<ClassLoader, Boolean>> allCaches = new ArrayList<>();
+
+ private ClassLoaderMatcherCacheHolder() {}
+
+ public static void addCache(Cache<ClassLoader, Boolean> cache) {
+ synchronized (allCaches) {
+ allCaches.add(cache);
+ }
+ }
+
+ public static void invalidateAllCachesForClassLoader(ClassLoader loader) {
+ synchronized (allCaches) {
+ for (Cache<ClassLoader, Boolean> cache : allCaches) {
+ cache.remove(loader);
+ }
+ }
+ }
+}

MODIFY
javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/ClassLoaderMatcher.java
javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/bytebuddy/matcher/ClassLoaderMatcher.java
package io.opentelemetry.javaagent.tooling.bytebuddy.matcher;
import io.opentelemetry.instrumentation.api.caching.Cache;
+import io.opentelemetry.javaagent.bootstrap.ClassLoaderMatcherCacheHolder;
import io.opentelemetry.javaagent.instrumentation.api.internal.InClassLoaderMatcher;
import net.bytebuddy.matcher.ElementMatcher;
@@ -43,6 +44,7 @@ public final class ClassLoaderMatcher {
for (int i = 0; i < resources.length; i++) {
resources[i] = resources[i].replace(".", "/") + ".class";
}
+ ClassLoaderMatcherCacheHolder.addCache(cache);
}
private boolean hasResources(ClassLoader cl) {

MODIFY
javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/matcher/GlobalIgnoresMatcher.java
javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/matcher/GlobalIgnoresMatcher.java
@@ -118,7 +118,9 @@ public class GlobalIgnoresMatcher<T extends TypeDescription>
}
if (name.startsWith("java.")) {
- if (name.equals("java.net.URL") || name.equals("java.net.HttpURLConnection")) {
+ if (name.equals("java.net.URL")
+ || name.equals("java.net.HttpURLConnection")
+ || name.equals("java.net.URLClassLoader")) {
return false;
}
if (name.startsWith("java.rmi.") || name.startsWith("java.util.concurrent.")) {

MODIFY
settings.gradle
settings.gradle
@@ -86,6 +86,7 @@ include ':instrumentation:cassandra:cassandra-3.0:javaagent'
include ':instrumentation:cassandra:cassandra-4.0:javaagent'
include ':instrumentation:cdi-testing'
include ':instrumentation:classloaders:javaagent'
+include ':instrumentation:classloaders:javaagent-integration-tests'
include ':instrumentation:classloaders:javaagent-unittests'
include ':instrumentation:couchbase:couchbase-2.0:javaagent'
include ':instrumentation:couchbase:couchbase-2.0:javaagent-unittests'

Support URLClassLoader addURL (#2772)