Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import io.opentelemetry.api.baggage.Baggage;
import io.opentelemetry.context.Scope;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldInstalledMarker;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
plugins {
id("otel.java-conventions")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.internal.classloader.stub;

import java.security.ProtectionDomain;

/**
* A placeholder for java.lang.ClassLoader to allow compilation of advice classes that invoke
* protected methods of ClassLoader (like defineClass and findLoadedClass). During the build we'll
* use shadow plugin to replace reference to this class with the real java.lang.ClassLoader.
*/
@SuppressWarnings("JavaLangClash")
public abstract class ClassLoader {
public abstract Class<?> findLoadedClass(String name);

public abstract Class<?> defineClass(
String name, byte[] b, int off, int len, ProtectionDomain protectionDomain);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ dependencies {
compileOnly("org.apache.commons:commons-lang3:3.12.0")
testImplementation("org.apache.commons:commons-lang3:3.12.0")

testInstrumentation(project(":instrumentation:internal:internal-class-loader:javaagent"))
testInstrumentation(project(":instrumentation:internal:internal-class-loader:javaagent", configuration = "shaded"))
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

plugins {
id("otel.javaagent-instrumentation")
}

dependencies {
compileOnly(project(":javaagent-bootstrap"))
compileOnly(project(":javaagent-tooling"))
compileOnly(project(":instrumentation:internal:internal-class-loader:compile-stub"))

testImplementation(project(":javaagent-bootstrap"))

Expand All @@ -21,3 +24,32 @@ dependencies {
testImplementation("org.eclipse.platform:org.eclipse.osgi:3.13.200")
testImplementation("org.apache.felix:org.apache.felix.framework:6.0.2")
}

val shadedJar by tasks.registering(ShadowJar::class) {
from(zipTree(tasks.jar.get().archiveFile))
archiveClassifier.set("shaded")
}

tasks {
withType(ShadowJar::class) {
relocate("io.opentelemetry.javaagent.instrumentation.internal.classloader.stub", "java.lang")
}

assemble {
dependsOn(shadedJar)
}
}

// Create a consumable configuration for the shaded jar. We can't use the "shadow" configuration
// because that is taken by the agent-testing.jar
configurations {
consumable("shaded") {
attributes {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("shaded"))
}
}
}

artifacts {
add("shaded", shadedJar)
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,37 @@ public void transform(TypeTransformer transformer) {
@SuppressWarnings("unused")
public static class LoadClassAdvice {

// Class loader stub is shaded back to the real class loader class. It is used to call protected
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried multiple approaches here (check the commits if interested). Firstly I tried creating a MethodHandles.Lookup in the advice and passing it along. This allowed getting access to MethodHandles for findLoadedClass and defineClass. Unfortunately it didn't work on jdk8, apparently ClassLoader is a restricted class there for which you can't create a Lookup for. Didn't like that a special case was needed for jdk8. Next I tried writing the advice with asm. Since we need to call multiple methods it was a sizable chunk of asm so instead I tried what you see here. We use a modified stub ClassLoader class that has findLoadedClass and defineClass as public so we could use them in the advice. During the build we shade the stub to java.lang.ClassLoader. The bytecode works because this advice is inlined into subclasses of ClassLoader that can call these protected methods. It isn't ideal either as it required some build script hacking to make the shading work.

// method from the advice that the compiler won't let us call directly. During runtime it is
// fine since this code is inlined into subclasses of ClassLoader that can call protected
// methods.
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
public static Class<?> onEnter(
@Advice.This ClassLoader classLoader, @Advice.Argument(0) String name) {
return InjectedClassHelper.loadHelperClass(classLoader, name);
@Advice.This java.lang.ClassLoader classLoader,
@Advice.This
io.opentelemetry.javaagent.instrumentation.internal.classloader.stub.ClassLoader
classLoaderStub,
@Advice.Argument(0) String name) {
InjectedClassHelper.HelperClassInfo helperClassInfo =
InjectedClassHelper.getHelperClassInfo(classLoader, name);
if (helperClassInfo != null) {
Class<?> clazz = classLoaderStub.findLoadedClass(name);
if (clazz != null) {
return clazz;
}
try {
byte[] bytes = helperClassInfo.getClassBytes();
return classLoaderStub.defineClass(
name, bytes, 0, bytes.length, helperClassInfo.getProtectionDomain());
} catch (LinkageError error) {
clazz = classLoaderStub.findLoadedClass(name);
if (clazz != null) {
return clazz;
}
throw error;
}
}
return null;
}

@AssignReturned.ToReturned
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import static org.assertj.core.api.Assertions.assertThat;

import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldInstalledMarker;
import org.junit.jupiter.api.Test;

class LambdaInstrumentationTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

import instrumentation.TestHelperClass;
import io.opentelemetry.javaagent.bootstrap.InstrumentationProxy;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldInstalledMarker;
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.lang.reflect.Field;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
package io.opentelemetry.javaagent.instrumentation.internal.reflection;

import io.opentelemetry.javaagent.bootstrap.InstrumentationProxy;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldDetector;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldDetector;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldInstalledMarker;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package io.opentelemetry.javaagent.bootstrap;

import java.security.ProtectionDomain;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;
Expand Down Expand Up @@ -37,21 +38,27 @@ public static boolean isHelperClass(ClassLoader classLoader, String className) {
return helperClassDetector.test(classLoader, className);
}

private static volatile BiFunction<ClassLoader, String, Class<?>> helperClassLoader;
private static volatile BiFunction<ClassLoader, String, HelperClassInfo> helperClassInfo;

public static void internalSetHelperClassLoader(
BiFunction<ClassLoader, String, Class<?>> helperClassLoader) {
if (InjectedClassHelper.helperClassLoader != null) {
public static void internalSetHelperClassInfo(
BiFunction<ClassLoader, String, HelperClassInfo> helperClassInfo) {
if (InjectedClassHelper.helperClassInfo != null) {
// Only possible by misuse of this API, just ignore.
return;
}
InjectedClassHelper.helperClassLoader = helperClassLoader;
InjectedClassHelper.helperClassInfo = helperClassInfo;
}

public static Class<?> loadHelperClass(ClassLoader classLoader, String className) {
if (helperClassLoader == null) {
public static HelperClassInfo getHelperClassInfo(ClassLoader classLoader, String className) {
if (helperClassInfo == null) {
return null;
}
return helperClassLoader.apply(classLoader, className);
return helperClassInfo.apply(classLoader, className);
}

public interface HelperClassInfo {
byte[] getClassBytes();

ProtectionDomain getProtectionDomain();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.bootstrap;
package io.opentelemetry.javaagent.bootstrap.field;

/** A marker interface implemented by virtual field accessor classes. */
public interface VirtualFieldAccessorMarker {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.bootstrap;
package io.opentelemetry.javaagent.bootstrap.field;

import io.opentelemetry.instrumentation.api.internal.cache.Cache;
import java.lang.invoke.MethodHandles;
import java.util.Collection;

/** Helper class for detecting whether given class has virtual fields. */
Expand Down Expand Up @@ -48,4 +49,8 @@ public static boolean hasVirtualField(Class<?> clazz, String virtualFieldInterfa
public static void markVirtualFields(Class<?> clazz, Collection<String> virtualFieldClassName) {
classesWithVirtualFields.put(clazz, virtualFieldClassName);
}

public static MethodHandles.Lookup lookup() {
return MethodHandles.lookup();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.bootstrap;
package io.opentelemetry.javaagent.bootstrap.field;

/**
* A marker interface that signifies that virtual fields have been installed into the class that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ private static void installBytebuddyAgent(
ThreadLocalRandom.current();

AgentBuilder agentBuilder =
new AgentBuilder.Default(
newAgentBuilder(
// default method graph compiler inspects the class hierarchy, we don't need it, so
// we use a simpler and faster strategy instead
new ByteBuddy()
Expand All @@ -147,6 +147,7 @@ private static void installBytebuddyAgent(
.with(AgentTooling.poolStrategy())
.with(AgentTooling.transformListener())
.with(AgentTooling.locationStrategy());

if (JavaModule.isSupported()) {
agentBuilder = agentBuilder.with(new ExposeAgentBootstrapListener(inst));
}
Expand Down Expand Up @@ -225,6 +226,29 @@ private static void installBytebuddyAgent(
runAfterAgentListeners(agentListeners, autoConfiguredSdk, sdkConfig);
}

private static AgentBuilder newAgentBuilder(ByteBuddy byteBuddy) {
// AgentBuilder.Default constructor triggers sun.misc.Unsafe::objectFieldOffset called warning
// AgentBuilder$Default.<init>
// -> NexusAccessor.<clinit>
// -> ClassInjector$UsingReflection.<clinit>
// -> ClassInjector$UsingUnsafe.<clinit>
// -> WARNING: sun.misc.Unsafe::objectFieldOffset called by
// net.bytebuddy.dynamic.loading.ClassInjector$UsingUnsafe$Dispatcher$CreationAction
// we don't use byte-buddy nexus so we disable it and later restore the original value for the
// system property
String originalNexusDisabled = System.setProperty("net.bytebuddy.nexus.disabled", "true");
try {
return new AgentBuilder.Default(byteBuddy);
} finally {
// restore the original value for the nexus disabled property
if (originalNexusDisabled != null) {
System.setProperty("net.bytebuddy.nexus.disabled", originalNexusDisabled);
} else {
System.clearProperty("net.bytebuddy.nexus.disabled");
}
}
}

private static void installEarlyInstrumentation(
AgentBuilder agentBuilder, Instrumentation instrumentation) {
// We are only going to install the virtual fields here. Installing virtual field changes class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import static io.opentelemetry.javaagent.tooling.field.GeneratedVirtualFieldNames.getRealGetterName;
import static io.opentelemetry.javaagent.tooling.field.GeneratedVirtualFieldNames.getRealSetterName;

import io.opentelemetry.javaagent.bootstrap.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.tooling.muzzle.VirtualFieldMappings;
import java.util.HashMap;
import java.util.Map;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
package io.opentelemetry.javaagent.tooling.field;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.tooling.muzzle.ClassFileLocatorProvider;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.SyntheticState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.opentelemetry.javaagent.bootstrap.InstrumentationHolder;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldDetector;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldDetector;
import io.opentelemetry.javaagent.tooling.HelperInjector;
import io.opentelemetry.javaagent.tooling.TransformSafeLogger;
import io.opentelemetry.javaagent.tooling.instrumentation.InstrumentationModuleInstaller;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

package io.opentelemetry.javaagent.tooling.field;

import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldAccessorMarker;

final class GeneratedVirtualFieldNames {

/**
Expand All @@ -13,7 +15,8 @@ final class GeneratedVirtualFieldNames {
* 'isolating' (or 'module') classloaders like jboss and osgi see injected classes. This works
* because we instrument those classloaders to load everything inside bootstrap packages.
*/
static final String DYNAMIC_CLASSES_PACKAGE = "io.opentelemetry.javaagent.bootstrap.field.";
static final String DYNAMIC_CLASSES_PACKAGE =
VirtualFieldAccessorMarker.class.getPackage().getName() + ".";

private GeneratedVirtualFieldNames() {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import static io.opentelemetry.javaagent.tooling.field.GeneratedVirtualFieldNames.getRealSetterName;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker;
import io.opentelemetry.javaagent.bootstrap.field.VirtualFieldInstalledMarker;
import io.opentelemetry.javaagent.extension.instrumentation.internal.AsmApi;
import io.opentelemetry.javaagent.tooling.Utils;
import java.util.Arrays;
Expand Down
Loading
Loading