Support URLClassLoader addURL (#2772)

This commit is contained in:
Trask Stalnaker 2021-04-12 15:53:44 -07:00 committed by GitHub
parent 0f60b44b7c
commit 117d38a01b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 207 additions and 1 deletions

View File

@ -0,0 +1,8 @@
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"
}

View File

@ -0,0 +1,57 @@
/*
* 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";
}
}
}

View File

@ -0,0 +1,44 @@
/*
* 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)
}
}
}

View File

@ -27,6 +27,7 @@ public class ClassLoaderInstrumentationModule extends InstrumentationModule {
public List<TypeInstrumentation> typeInstrumentations() {
return asList(
new ClassLoaderInstrumentation(),
new UrlClassLoaderInstrumentation(),
new ProxyInstrumentation(),
new ResourceInjectionInstrumentation());
}

View File

@ -0,0 +1,52 @@
/*
* 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);
}
}
}

View File

@ -0,0 +1,39 @@
/*
* 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);
}
}
}
}

View File

@ -6,6 +6,7 @@
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) {

View File

@ -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.")) {

View File

@ -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'