Implemented InstrumentationModuleClassLoader for invokedynamic Advice dispatching (#9177)
Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>
This commit is contained in:
parent
dd2bccdd3e
commit
5abba34ade
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.tooling.instrumentation.indy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import net.bytebuddy.utility.StreamDrainer;
|
||||
|
||||
/**
|
||||
* Provides the bytecode and the original resource URL for loaded and not-yet loaded classes. The
|
||||
* implementation is based on {@link net.bytebuddy.dynamic.ClassFileLocator.ForClassLoader}, with
|
||||
* the difference that it preserves the original classfile resource URL.
|
||||
*/
|
||||
abstract class ClassCopySource {
|
||||
|
||||
private ClassCopySource() {}
|
||||
|
||||
/**
|
||||
* Provides a URL pointing to the specific classfile.
|
||||
*
|
||||
* @return the URL
|
||||
*/
|
||||
public abstract URL getUrl();
|
||||
|
||||
/**
|
||||
* Provides the bytecode of the class. The result is the same as calling {@link URL#openStream()}
|
||||
* on {@link #getUrl()} and draining that stream.
|
||||
*
|
||||
* @return the bytecode of the class.
|
||||
*/
|
||||
public abstract byte[] getBytecode();
|
||||
|
||||
/**
|
||||
* Creates a cached copy of this {@link ClassCopySource}. The cached copy eagerly loads the
|
||||
* bytecode, so that {@link #getBytecode()} is guaranteed to not cause any IO. This comes at the
|
||||
* cost of a higher heap consumption, as the bytecode is kept in memory.
|
||||
*
|
||||
* @return an ClassFileSource implementing the described caching behaviour.
|
||||
*/
|
||||
public abstract ClassCopySource cached();
|
||||
|
||||
/**
|
||||
* Creates a {@link ClassCopySource} for the class with the provided fully qualified name. The
|
||||
* .class file for the provided classname must be available as a resource in the provided
|
||||
* classloader. The class is guaranteed to not be loaded during this process.
|
||||
*
|
||||
* @param className the fully qualified name of the class to copy
|
||||
* @param classLoader the classloader
|
||||
* @return the ClassCopySource which can be used to copy the provided class to other classloaders.
|
||||
*/
|
||||
public static ClassCopySource create(String className, ClassLoader classLoader) {
|
||||
if (classLoader == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Copying classes from the bootstrap classloader is not supported!");
|
||||
}
|
||||
String classFileName = className.replace('.', '/') + ".class";
|
||||
return new Lazy(classLoader, classFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #create(String, ClassLoader)}, but easier to use for already loaded classes.
|
||||
*
|
||||
* @param loadedClass the class to copy
|
||||
* @return the ClassCopySource which can be used to copy the provided class to other classloaders.
|
||||
*/
|
||||
public static ClassCopySource create(Class<?> loadedClass) {
|
||||
return create(loadedClass.getName(), loadedClass.getClassLoader());
|
||||
}
|
||||
|
||||
private static class Lazy extends ClassCopySource {
|
||||
|
||||
private final ClassLoader classLoader;
|
||||
private final String resourceName;
|
||||
|
||||
private Lazy(ClassLoader classLoader, String resourceName) {
|
||||
this.classLoader = classLoader;
|
||||
this.resourceName = resourceName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getUrl() {
|
||||
URL url = classLoader.getResource(resourceName);
|
||||
if (url == null) {
|
||||
throw new IllegalStateException(
|
||||
"Classfile " + resourceName + " does not exist in the provided classloader!");
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBytecode() {
|
||||
try (InputStream bytecodeStream = getUrl().openStream()) {
|
||||
return StreamDrainer.DEFAULT.drain(bytecodeStream);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Failed to read classfile URL", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClassCopySource cached() {
|
||||
return new Cached(this);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Cached extends ClassCopySource {
|
||||
|
||||
private final URL classFileUrl;
|
||||
|
||||
private final byte[] cachedByteCode;
|
||||
|
||||
private Cached(ClassCopySource.Lazy from) {
|
||||
classFileUrl = from.getUrl();
|
||||
cachedByteCode = from.getBytecode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getUrl() {
|
||||
return classFileUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBytecode() {
|
||||
return cachedByteCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClassCopySource cached() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,255 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.tooling.instrumentation.indy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.net.URL;
|
||||
import java.security.AccessController;
|
||||
import java.security.PrivilegedAction;
|
||||
import java.security.ProtectionDomain;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Classloader used to load the helper classes from {@link
|
||||
* io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule}s, so that those
|
||||
* classes have access to both the agent/extension classes and the instrumented application classes.
|
||||
*
|
||||
* <p>This classloader implements the following classloading delegation strategy:
|
||||
*
|
||||
* <ul>
|
||||
* <li>First, injected classes are considered (usually the helper classes from the
|
||||
* InstrumentationModule)
|
||||
* <li>Next, the classloader looks in the agent or extension classloader, depending on where the
|
||||
* InstrumentationModule comes from
|
||||
* <li>Finally, the instrumented application classloader is checked for the class
|
||||
* </ul>
|
||||
*
|
||||
* <p>In addition, this classloader ensures that the lookup of corresponding .class resources follow
|
||||
* the same delegation strategy, so that bytecode inspection tools work correctly.
|
||||
*/
|
||||
class InstrumentationModuleClassLoader extends ClassLoader {
|
||||
|
||||
static {
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
|
||||
private static final Map<String, ClassCopySource> ALWAYS_INJECTED_CLASSES =
|
||||
Collections.singletonMap(
|
||||
LookupExposer.class.getName(), ClassCopySource.create(LookupExposer.class).cached());
|
||||
private static final ProtectionDomain PROTECTION_DOMAIN = getProtectionDomain();
|
||||
private static final MethodHandle FIND_PACKAGE_METHOD = getFindPackageMethod();
|
||||
|
||||
private final Map<String, ClassCopySource> additionalInjectedClasses;
|
||||
private final ClassLoader agentOrExtensionCl;
|
||||
private final ClassLoader instrumentedCl;
|
||||
private volatile MethodHandles.Lookup cachedLookup;
|
||||
|
||||
public InstrumentationModuleClassLoader(
|
||||
ClassLoader instrumentedCl,
|
||||
ClassLoader agentOrExtensionCl,
|
||||
Map<String, ClassCopySource> injectedClasses) {
|
||||
// agent/extension-classloader is "main"-parent, but class lookup is overridden
|
||||
super(agentOrExtensionCl);
|
||||
additionalInjectedClasses = injectedClasses;
|
||||
this.agentOrExtensionCl = agentOrExtensionCl;
|
||||
this.instrumentedCl = instrumentedCl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a Lookup within this classloader. See {@link LookupExposer} for the details.
|
||||
*
|
||||
* @return a lookup capable of accessing public types in this classloader
|
||||
*/
|
||||
public MethodHandles.Lookup getLookup() {
|
||||
if (cachedLookup == null) {
|
||||
// Load the injected copy of LookupExposer and invoke it
|
||||
try {
|
||||
// we don't mind the race condition causing the initialization to run multiple times here
|
||||
Class<?> lookupExposer = loadClass(LookupExposer.class.getName());
|
||||
cachedLookup = (MethodHandles.Lookup) lookupExposer.getMethod("getLookup").invoke(null);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
return cachedLookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
synchronized (getClassLoadingLock(name)) {
|
||||
Class<?> result = findLoadedClass(name);
|
||||
|
||||
// This CL is self-first: Injected class are loaded BEFORE a parent lookup
|
||||
if (result == null) {
|
||||
ClassCopySource injected = getInjectedClass(name);
|
||||
if (injected != null) {
|
||||
byte[] bytecode = injected.getBytecode();
|
||||
if (System.getSecurityManager() == null) {
|
||||
result = defineClassWithPackage(name, bytecode);
|
||||
} else {
|
||||
result =
|
||||
AccessController.doPrivileged(
|
||||
(PrivilegedAction<Class<?>>) () -> defineClassWithPackage(name, bytecode));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result == null) {
|
||||
result = tryLoad(agentOrExtensionCl, name);
|
||||
}
|
||||
if (result == null) {
|
||||
result = tryLoad(instrumentedCl, name);
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
if (resolve) {
|
||||
resolveClass(result);
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Class<?> tryLoad(ClassLoader cl, String name) {
|
||||
try {
|
||||
return cl.loadClass(name);
|
||||
} catch (ClassNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getResource(String resourceName) {
|
||||
String className = resourceToClassName(resourceName);
|
||||
if (className == null) {
|
||||
// delegate to just the default parent (the agent classloader)
|
||||
return super.getResource(resourceName);
|
||||
}
|
||||
// for classes use the same precedence as in loadClass
|
||||
ClassCopySource injected = getInjectedClass(className);
|
||||
if (injected != null) {
|
||||
return injected.getUrl();
|
||||
}
|
||||
URL fromAgentCl = agentOrExtensionCl.getResource(resourceName);
|
||||
if (fromAgentCl != null) {
|
||||
return fromAgentCl;
|
||||
}
|
||||
return instrumentedCl.getResource(resourceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<URL> getResources(String resourceName) throws IOException {
|
||||
String className = resourceToClassName(resourceName);
|
||||
if (className == null) {
|
||||
return super.getResources(resourceName);
|
||||
}
|
||||
URL resource = getResource(resourceName);
|
||||
List<URL> result =
|
||||
resource != null ? Collections.singletonList(resource) : Collections.emptyList();
|
||||
return Collections.enumeration(result);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String resourceToClassName(String resourceName) {
|
||||
if (!resourceName.endsWith(".class")) {
|
||||
return null;
|
||||
}
|
||||
String className = resourceName;
|
||||
if (className.startsWith("/")) {
|
||||
className = className.substring(1);
|
||||
}
|
||||
className = className.replace('/', '.');
|
||||
className = className.substring(0, className.length() - ".class".length());
|
||||
return className;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ClassCopySource getInjectedClass(String name) {
|
||||
ClassCopySource alwaysInjected = ALWAYS_INJECTED_CLASSES.get(name);
|
||||
if (alwaysInjected != null) {
|
||||
return alwaysInjected;
|
||||
}
|
||||
return additionalInjectedClasses.get(name);
|
||||
}
|
||||
|
||||
private Class<?> defineClassWithPackage(String name, byte[] bytecode) {
|
||||
int lastDotIndex = name.lastIndexOf('.');
|
||||
if (lastDotIndex != -1) {
|
||||
String packageName = name.substring(0, lastDotIndex);
|
||||
safeDefinePackage(packageName);
|
||||
}
|
||||
return defineClass(name, bytecode, 0, bytecode.length, PROTECTION_DOMAIN);
|
||||
}
|
||||
|
||||
private void safeDefinePackage(String packageName) {
|
||||
if (findPackage(packageName) == null) {
|
||||
try {
|
||||
definePackage(packageName, null, null, null, null, null, null, null);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Can happen if two classes from the same package are loaded concurrently
|
||||
if (findPackage(packageName) == null) {
|
||||
// package still doesn't exist, the IllegalArgumentException must be for a different
|
||||
// reason than a race condition
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@link #getPackage(String)} for Java 8 and {@link #getDefinedPackage(String)} for Java
|
||||
* 9+.
|
||||
*
|
||||
* <p>Package-private for testing.
|
||||
*
|
||||
* @param name the name of the package find
|
||||
* @return the found package or null if it was not found.
|
||||
*/
|
||||
@SuppressWarnings({"deprecation", "InvalidLink"})
|
||||
Package findPackage(String name) {
|
||||
try {
|
||||
return (Package) FIND_PACKAGE_METHOD.invoke(this, name);
|
||||
} catch (Throwable t) {
|
||||
throw new IllegalStateException(t);
|
||||
}
|
||||
}
|
||||
|
||||
private static ProtectionDomain getProtectionDomain() {
|
||||
if (System.getSecurityManager() == null) {
|
||||
return InstrumentationModuleClassLoader.class.getProtectionDomain();
|
||||
}
|
||||
return AccessController.doPrivileged(
|
||||
(PrivilegedAction<ProtectionDomain>)
|
||||
((Class<?>) InstrumentationModuleClassLoader.class)::getProtectionDomain);
|
||||
}
|
||||
|
||||
private static MethodHandle getFindPackageMethod() {
|
||||
MethodType methodType = MethodType.methodType(Package.class, String.class);
|
||||
MethodHandles.Lookup lookup = MethodHandles.lookup();
|
||||
try {
|
||||
try {
|
||||
return lookup.findVirtual(ClassLoader.class, "getDefinedPackage", methodType);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// Java 8 case
|
||||
try {
|
||||
return lookup.findVirtual(ClassLoader.class, "getPackage", methodType);
|
||||
} catch (NoSuchMethodException ex) {
|
||||
throw new IllegalStateException("expected method to always exist!", ex);
|
||||
}
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new IllegalStateException("Method should be accessible from here", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.tooling.instrumentation.indy;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
|
||||
/**
|
||||
* This class is injected into every {@link InstrumentationModuleClassLoader} so that the bootstrap
|
||||
* can use a {@link MethodHandles.Lookup} with a lookup class from within the {@link
|
||||
* InstrumentationModuleClassLoader}, instead of calling {@link MethodHandles#lookup()} which uses
|
||||
* the caller class as the lookup class.
|
||||
*
|
||||
* <p>This circumvents a nasty JVM bug that's described <a
|
||||
* href="https://github.com/elastic/apm-agent-java/issues/1450">here</a>. The error is reproduced in
|
||||
* {@code InstrumentationModuleClassLoaderTest}
|
||||
*/
|
||||
public class LookupExposer {
|
||||
|
||||
private LookupExposer() {}
|
||||
|
||||
public static MethodHandles.Lookup getLookup() {
|
||||
return MethodHandles.lookup();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.tooling.instrumentation.indy;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies.Bar;
|
||||
import io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies.Foo;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import net.bytebuddy.ByteBuddy;
|
||||
import net.bytebuddy.dynamic.ClassFileLocator;
|
||||
import net.bytebuddy.implementation.FixedValue;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
@SuppressWarnings("ClassNamedLikeTypeParameter")
|
||||
class InstrumentationModuleClassLoaderTest {
|
||||
|
||||
@Test
|
||||
void checkLookup() throws Throwable {
|
||||
Map<String, ClassCopySource> toInject = new HashMap<>();
|
||||
toInject.put(Foo.class.getName(), ClassCopySource.create(Foo.class));
|
||||
toInject.put(Bar.class.getName(), ClassCopySource.create(Bar.class));
|
||||
|
||||
ClassLoader dummyParent = new URLClassLoader(new URL[] {}, null);
|
||||
|
||||
InstrumentationModuleClassLoader m1 =
|
||||
new InstrumentationModuleClassLoader(dummyParent, dummyParent, toInject);
|
||||
InstrumentationModuleClassLoader m2 =
|
||||
new InstrumentationModuleClassLoader(dummyParent, dummyParent, toInject);
|
||||
|
||||
// MethodHandles.publicLookup() always succeeds on the first invocation
|
||||
lookupAndInvokeFoo(m1);
|
||||
// MethodHandles.publicLookup() fails on the second invocation,
|
||||
// even though the classes are loaded from an isolated class loader hierarchy
|
||||
lookupAndInvokeFoo(m2);
|
||||
}
|
||||
|
||||
private static void lookupAndInvokeFoo(InstrumentationModuleClassLoader classLoader)
|
||||
throws Throwable {
|
||||
Class<?> fooClass = classLoader.loadClass(Foo.class.getName());
|
||||
MethodHandles.Lookup lookup;
|
||||
// using public lookup fails with LinkageError on second invocation - is this a (known) JVM bug?
|
||||
// The failure only occurs on certain JVM versions, e.g. Java 8 and 11
|
||||
// lookup = MethodHandles.publicLookup();
|
||||
lookup = classLoader.getLookup();
|
||||
MethodHandle methodHandle =
|
||||
lookup.findStatic(
|
||||
fooClass,
|
||||
"foo",
|
||||
MethodType.methodType(String.class, classLoader.loadClass(Bar.class.getName())));
|
||||
assertThat(methodHandle.invoke((Bar) null)).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkInjectedClassesHavePackage() throws Throwable {
|
||||
Map<String, ClassCopySource> toInject = new HashMap<>();
|
||||
toInject.put(A.class.getName(), ClassCopySource.create(A.class));
|
||||
toInject.put(B.class.getName(), ClassCopySource.create(B.class));
|
||||
String packageName = A.class.getName().substring(0, A.class.getName().lastIndexOf('.'));
|
||||
|
||||
ClassLoader dummyParent = new URLClassLoader(new URL[] {}, null);
|
||||
InstrumentationModuleClassLoader m1 =
|
||||
new InstrumentationModuleClassLoader(dummyParent, dummyParent, toInject);
|
||||
|
||||
Class<?> injected = Class.forName(A.class.getName(), true, m1);
|
||||
// inject two classes from the same package to trigger errors if we try to redefine the package
|
||||
Class.forName(B.class.getName(), true, m1);
|
||||
|
||||
assertThat(injected.getClassLoader()).isSameAs(m1);
|
||||
Package clPackage = m1.findPackage(packageName);
|
||||
Package classPackage = injected.getPackage();
|
||||
|
||||
assertThat(classPackage).isNotNull();
|
||||
assertThat(clPackage).isNotNull();
|
||||
assertThat(classPackage).isSameAs(clPackage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkClassLookupPrecedence(@TempDir Path tempDir) throws Exception {
|
||||
|
||||
Map<String, byte[]> appClasses = copyClassesWithMarker("app-cl", A.class, B.class, C.class);
|
||||
Map<String, byte[]> agentClasses = copyClassesWithMarker("agent-cl", B.class, C.class);
|
||||
Map<String, byte[]> moduleClasses =
|
||||
copyClassesWithMarker("module-cl", A.class, B.class, C.class, D.class);
|
||||
|
||||
Path appJar = tempDir.resolve("dummy-app.jar");
|
||||
createJar(appClasses, appJar);
|
||||
|
||||
Path agentJar = tempDir.resolve("dummy-agent.jar");
|
||||
createJar(agentClasses, agentJar);
|
||||
|
||||
Path moduleJar = tempDir.resolve("instrumentation-module.jar");
|
||||
createJar(moduleClasses, moduleJar);
|
||||
|
||||
URLClassLoader appCl = new URLClassLoader(new URL[] {appJar.toUri().toURL()}, null);
|
||||
URLClassLoader agentCl = new URLClassLoader(new URL[] {agentJar.toUri().toURL()}, null);
|
||||
URLClassLoader moduleSourceCl = new URLClassLoader(new URL[] {moduleJar.toUri().toURL()}, null);
|
||||
|
||||
try {
|
||||
Map<String, ClassCopySource> toInject = new HashMap<>();
|
||||
toInject.put(C.class.getName(), ClassCopySource.create(C.class.getName(), moduleSourceCl));
|
||||
|
||||
InstrumentationModuleClassLoader moduleCl =
|
||||
new InstrumentationModuleClassLoader(appCl, agentCl, toInject);
|
||||
|
||||
// Verify precedence for classloading
|
||||
Class<?> clA = moduleCl.loadClass(A.class.getName());
|
||||
assertThat(getMarkerValue(clA)).isEqualTo("app-cl");
|
||||
assertThat(clA.getClassLoader()).isSameAs(appCl);
|
||||
|
||||
Class<?> clB = moduleCl.loadClass(B.class.getName());
|
||||
assertThat(getMarkerValue(clB)).isEqualTo("agent-cl");
|
||||
assertThat(clB.getClassLoader()).isSameAs(agentCl);
|
||||
|
||||
Class<?> clC = moduleCl.loadClass(C.class.getName());
|
||||
assertThat(getMarkerValue(clC)).isEqualTo("module-cl");
|
||||
assertThat(clC.getClassLoader())
|
||||
.isSameAs(moduleCl); // class must be copied, therefore moduleCL
|
||||
|
||||
assertThatThrownBy(() -> moduleCl.loadClass(D.class.getName()))
|
||||
.isInstanceOf(ClassNotFoundException.class);
|
||||
|
||||
// Verify precedence for looking up .class resources
|
||||
URL resourceA = moduleCl.getResource(getClassFile(A.class));
|
||||
assertThat(resourceA.toString()).startsWith("jar:file:" + appJar);
|
||||
assertThat(Collections.list(moduleCl.getResources(getClassFile(A.class))))
|
||||
.containsExactly(resourceA);
|
||||
assertThat(moduleCl.getResourceAsStream(getClassFile(A.class)))
|
||||
.hasBinaryContent(appClasses.get(A.class.getName()));
|
||||
|
||||
URL resourceB = moduleCl.getResource(getClassFile(B.class));
|
||||
assertThat(resourceB.toString()).startsWith("jar:file:" + agentJar);
|
||||
assertThat(Collections.list(moduleCl.getResources(getClassFile(B.class))))
|
||||
.containsExactly(resourceB);
|
||||
assertThat(moduleCl.getResourceAsStream(getClassFile(B.class)))
|
||||
.hasBinaryContent(agentClasses.get(B.class.getName()));
|
||||
|
||||
URL resourceC = moduleCl.getResource(getClassFile(C.class));
|
||||
assertThat(resourceC.toString()).startsWith("jar:file:" + moduleJar);
|
||||
assertThat(Collections.list(moduleCl.getResources(getClassFile(C.class))))
|
||||
.containsExactly(resourceC);
|
||||
assertThat(moduleCl.getResourceAsStream(getClassFile(C.class)))
|
||||
.hasBinaryContent(moduleClasses.get(C.class.getName()));
|
||||
assertThat(moduleCl.getResource("/" + getClassFile(C.class))).isEqualTo(resourceC);
|
||||
|
||||
assertThat(moduleCl.getResource(D.class.getName())).isNull();
|
||||
assertThat(moduleCl.getResourceAsStream(D.class.getName())).isNull();
|
||||
assertThat(Collections.list(moduleCl.getResources(D.class.getName()))).isEmpty();
|
||||
|
||||
// And finally verify that our resource handling does what it is supposed to do:
|
||||
// Provide the correct bytecode sources when looking up bytecode with bytebuddy (or similar
|
||||
// tools)
|
||||
|
||||
assertThat(ClassFileLocator.ForClassLoader.read(clA))
|
||||
.isEqualTo(appClasses.get(A.class.getName()));
|
||||
assertThat(ClassFileLocator.ForClassLoader.read(clB))
|
||||
.isEqualTo(agentClasses.get(B.class.getName()));
|
||||
assertThat(ClassFileLocator.ForClassLoader.read(clC))
|
||||
.isEqualTo(moduleClasses.get(C.class.getName()));
|
||||
|
||||
} finally {
|
||||
appCl.close();
|
||||
agentCl.close();
|
||||
moduleSourceCl.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static String getClassFile(Class<?> cl) {
|
||||
return cl.getName().replace('.', '/') + ".class";
|
||||
}
|
||||
|
||||
private static Map<String, byte[]> copyClassesWithMarker(String marker, Class<?>... toCopy) {
|
||||
|
||||
Map<String, byte[]> classes = new HashMap<>();
|
||||
for (Class<?> clazz : toCopy) {
|
||||
classes.put(clazz.getName(), copyClassWithMarker(clazz, marker));
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
|
||||
private static byte[] copyClassWithMarker(Class<?> original, String markerValue) {
|
||||
return new ByteBuddy()
|
||||
.redefine(original)
|
||||
.defineMethod("marker", String.class, Modifier.PUBLIC | Modifier.STATIC)
|
||||
.intercept(FixedValue.value(markerValue))
|
||||
.make()
|
||||
.getBytes();
|
||||
}
|
||||
|
||||
private static String getMarkerValue(Class<?> clazz) {
|
||||
try {
|
||||
return (String) clazz.getMethod("marker").invoke(null);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createJar(Map<String, byte[]> classNameToBytecode, Path jarFilePath) {
|
||||
try (OutputStream fileOut = Files.newOutputStream(jarFilePath)) {
|
||||
try (JarOutputStream jarOut = new JarOutputStream(fileOut)) {
|
||||
for (String clName : classNameToBytecode.keySet()) {
|
||||
String classFile = clName.replace('.', '/') + ".class";
|
||||
jarOut.putNextEntry(new JarEntry(classFile));
|
||||
jarOut.write(classNameToBytecode.get(clName));
|
||||
jarOut.closeEntry();
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class A {}
|
||||
|
||||
public static class B {}
|
||||
|
||||
public static class C {}
|
||||
|
||||
public static class D {}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies;
|
||||
|
||||
public class Bar {}
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies;
|
||||
|
||||
public class Foo {
|
||||
private Foo() {}
|
||||
|
||||
public static String foo(Bar bar) {
|
||||
return "foo";
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue