diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyProxyFactory.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyProxyFactory.java
new file mode 100644
index 000000000000..0a1b774ba60f
--- /dev/null
+++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyProxyFactory.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.tooling.instrumentation.indy;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.List;
+import net.bytebuddy.ByteBuddy;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.method.ParameterDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.dynamic.DynamicType;
+import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy;
+import net.bytebuddy.implementation.FieldAccessor;
+import net.bytebuddy.implementation.Implementation;
+import net.bytebuddy.implementation.InvokeDynamic;
+import net.bytebuddy.implementation.MethodCall;
+import net.bytebuddy.implementation.bytecode.StackManipulation;
+import net.bytebuddy.implementation.bytecode.member.MethodInvocation;
+import net.bytebuddy.implementation.bytecode.member.MethodVariableAccess;
+import net.bytebuddy.utility.JavaConstant;
+
+/**
+ * Factory for generating proxies which invoke their target via {@code INVOKEDYNAMIC}. Generated
+ * proxy classes have the following properties: The generated proxies have the following basic
+ * structure:
+ *
+ *
+ * - it has same superclass as the proxied class
+ *
- it implements all interfaces implemented by the proxied class
+ *
- for every public constructor of the proxied class, it defined a matching public constructor
+ * which:
+ *
+ * - invokes the default constructor of the superclass
+ *
- invoked the corresponding constructor of the proxied class to generate the object to
+ * which the proxy delegates
+ *
+ * - it "copies" every declared static and non-static public method, the implementation will
+ * delegate to the corresponding method in the proxied class
+ *
- all annotations on the proxied class and on its methods are copied to the proxy
+ *
+ *
+ * Note that only the public methods declared by the proxied class are actually proxied.
+ * Inherited methods are not automatically proxied. If you want those to be proxied, you'll need to
+ * explicitly override them in the proxied class.
+ */
+public class IndyProxyFactory {
+
+ @FunctionalInterface
+ public interface BootstrapArgsProvider {
+
+ /**
+ * Defines the additional arguments to pass to the invokedynamic bootstrap method for a given
+ * proxied method. The arguments have to be storable in the constant pool.
+ *
+ * @param classBeingProxied the type for which {@link
+ * IndyProxyFactory#generateProxy(TypeDescription, String)} was invoked
+ * @param proxiedMethodOrCtor the method or constructor from the proxied class for which the
+ * arguments are requested
+ * @return the arguments to pass to the bootstrap method
+ */
+ List extends JavaConstant> getBootstrapArgsForMethod(
+ TypeDescription classBeingProxied, MethodDescription.InDefinedShape proxiedMethodOrCtor);
+ }
+
+ private static final String DELEGATE_FIELD_NAME = "delegate";
+
+ private final MethodDescription.InDefinedShape indyBootstrapMethod;
+
+ private final BootstrapArgsProvider bootstrapArgsProvider;
+
+ public IndyProxyFactory(Method bootstrapMethod, BootstrapArgsProvider bootstrapArgsProvider) {
+ this.indyBootstrapMethod = new MethodDescription.ForLoadedMethod(bootstrapMethod);
+ this.bootstrapArgsProvider = bootstrapArgsProvider;
+ }
+
+ /**
+ * Generates a proxy.
+ *
+ * @param classToProxy the class for which a proxy will be generated
+ * @param proxyClassName the desired fully qualified name for the proxy class
+ * @return the generated proxy class
+ */
+ public DynamicType.Unloaded> generateProxy(
+ TypeDescription classToProxy, String proxyClassName) {
+ TypeDescription.Generic superClass = classToProxy.getSuperClass();
+ DynamicType.Builder> builder =
+ new ByteBuddy()
+ .subclass(superClass, ConstructorStrategy.Default.NO_CONSTRUCTORS)
+ .implement(classToProxy.getInterfaces())
+ .name(proxyClassName)
+ .annotateType(classToProxy.getDeclaredAnnotations())
+ .defineField(DELEGATE_FIELD_NAME, Object.class, Modifier.PRIVATE | Modifier.FINAL);
+
+ for (MethodDescription.InDefinedShape method : classToProxy.getDeclaredMethods()) {
+ if (method.isPublic()) {
+ if (method.isConstructor()) {
+ List extends JavaConstant> bootstrapArgs =
+ bootstrapArgsProvider.getBootstrapArgsForMethod(classToProxy, method);
+ builder = createProxyConstructor(superClass, method, bootstrapArgs, builder);
+ } else if (method.isMethod()) {
+ List extends JavaConstant> bootstrapArgs =
+ bootstrapArgsProvider.getBootstrapArgsForMethod(classToProxy, method);
+ builder = createProxyMethod(method, bootstrapArgs, builder);
+ }
+ }
+ }
+ return builder.make();
+ }
+
+ private DynamicType.Builder> createProxyMethod(
+ MethodDescription.InDefinedShape proxiedMethod,
+ List extends JavaConstant> bootstrapArgs,
+ DynamicType.Builder> builder) {
+ InvokeDynamic body = InvokeDynamic.bootstrap(indyBootstrapMethod, bootstrapArgs);
+ if (!proxiedMethod.isStatic()) {
+ body = body.withField(DELEGATE_FIELD_NAME);
+ }
+ body = body.withMethodArguments();
+ int modifiers = Modifier.PUBLIC | (proxiedMethod.isStatic() ? Modifier.STATIC : 0);
+ return createProxyMethodOrConstructor(
+ proxiedMethod,
+ builder.defineMethod(proxiedMethod.getName(), proxiedMethod.getReturnType(), modifiers),
+ body);
+ }
+
+ private DynamicType.Builder> createProxyConstructor(
+ TypeDescription.Generic superClass,
+ MethodDescription.InDefinedShape proxiedConstructor,
+ List extends JavaConstant> bootstrapArgs,
+ DynamicType.Builder> builder) {
+ MethodDescription defaultSuperCtor = findDefaultConstructor(superClass);
+
+ Implementation.Composable fieldAssignment =
+ FieldAccessor.ofField(DELEGATE_FIELD_NAME)
+ .setsValue(
+ new StackManipulation.Compound(
+ MethodVariableAccess.allArgumentsOf(proxiedConstructor),
+ MethodInvocation.invoke(indyBootstrapMethod)
+ .dynamic(
+ "ctor", // the actual method name is not allowed by the verifier
+ TypeDescription.ForLoadedType.of(Object.class),
+ proxiedConstructor.getParameters().asTypeList().asErasures(),
+ bootstrapArgs)),
+ Object.class);
+ Implementation.Composable ctorBody =
+ MethodCall.invoke(defaultSuperCtor).andThen(fieldAssignment);
+ return createProxyMethodOrConstructor(
+ proxiedConstructor, builder.defineConstructor(Modifier.PUBLIC), ctorBody);
+ }
+
+ private static MethodDescription findDefaultConstructor(TypeDescription.Generic superClass) {
+ return superClass.getDeclaredMethods().stream()
+ .filter(MethodDescription::isConstructor)
+ .filter(constructor -> constructor.getParameters().isEmpty())
+ .findFirst()
+ .orElseThrow(
+ () ->
+ new IllegalArgumentException(
+ "Superclass of provided type does not define a default constructor"));
+ }
+
+ private static DynamicType.Builder> createProxyMethodOrConstructor(
+ MethodDescription.InDefinedShape method,
+ DynamicType.Builder.MethodDefinition.ParameterDefinition> methodDef,
+ Implementation methodBody) {
+ for (ParameterDescription param : method.getParameters()) {
+ methodDef =
+ methodDef
+ .withParameter(param.getType(), param.getName(), param.getModifiers())
+ .annotateParameter(param.getDeclaredAnnotations());
+ }
+ return methodDef
+ .throwing(method.getExceptionTypes())
+ .intercept(methodBody)
+ .annotateMethod(method.getDeclaredAnnotations());
+ }
+}
diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyProxyFactoryTest.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyProxyFactoryTest.java
new file mode 100644
index 000000000000..c5d1c287013b
--- /dev/null
+++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyProxyFactoryTest.java
@@ -0,0 +1,330 @@
+/*
+ * 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 io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies.DummyAnnotation;
+import java.lang.invoke.CallSite;
+import java.lang.invoke.ConstantCallSite;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.dynamic.DynamicType;
+import net.bytebuddy.utility.JavaConstant;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class IndyProxyFactoryTest {
+
+ private static IndyProxyFactory proxyFactory;
+
+ @BeforeAll
+ public static void init() throws Exception {
+ Method bootstrap =
+ IndyProxyFactoryTest.class.getMethod(
+ "indyBootstrap",
+ MethodHandles.Lookup.class,
+ String.class,
+ MethodType.class,
+ Object[].class);
+ proxyFactory = new IndyProxyFactory(bootstrap, IndyProxyFactoryTest::bootstrapArgsGenerator);
+ }
+
+ public static CallSite indyBootstrap(
+ MethodHandles.Lookup lookup, String methodName, MethodType methodType, Object... args) {
+
+ try {
+ String delegateClassName = (String) args[0];
+ String kind = (String) args[1];
+
+ Class> proxiedClass = Class.forName(delegateClassName);
+
+ MethodHandle target;
+
+ switch (kind) {
+ case "static":
+ target = MethodHandles.publicLookup().findStatic(proxiedClass, methodName, methodType);
+ break;
+ case "constructor":
+ target =
+ MethodHandles.publicLookup()
+ .findConstructor(proxiedClass, methodType.changeReturnType(void.class))
+ .asType(methodType);
+ break;
+ case "virtual":
+ target =
+ MethodHandles.publicLookup()
+ .findVirtual(proxiedClass, methodName, methodType.dropParameterTypes(0, 1))
+ .asType(methodType);
+ break;
+ default:
+ throw new IllegalStateException("unknown kind");
+ }
+ return new ConstantCallSite(target);
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private static List bootstrapArgsGenerator(
+ TypeDescription proxiedType, MethodDescription.InDefinedShape proxiedMethod) {
+ String kind = "virtual";
+ if (proxiedMethod.isConstructor()) {
+ kind = "constructor";
+ } else if (proxiedMethod.isStatic()) {
+ kind = "static";
+ }
+ return Arrays.asList(
+ JavaConstant.Simple.ofLoaded(proxiedType.getName()), JavaConstant.Simple.ofLoaded(kind));
+ }
+
+ public static class StatefulObj {
+
+ static StatefulObj lastCreatedInstance;
+
+ int counter = 0;
+
+ public StatefulObj() {
+ lastCreatedInstance = this;
+ }
+
+ public void increaseCounter() {
+ counter++;
+ }
+ }
+
+ @Test
+ void verifyDelegateInstantiation() throws Exception {
+ Class> proxy = generateProxy(StatefulObj.class);
+ Constructor> ctor = proxy.getConstructor();
+ Method increaseCounter = proxy.getMethod("increaseCounter");
+
+ Object proxyA = ctor.newInstance();
+ StatefulObj delegateA = StatefulObj.lastCreatedInstance;
+
+ Object proxyB = ctor.newInstance();
+ StatefulObj delegateB = StatefulObj.lastCreatedInstance;
+
+ assertThat(delegateA).isNotNull();
+ assertThat(delegateB).isNotNull();
+ assertThat(delegateA).isNotSameAs(delegateB);
+
+ increaseCounter.invoke(proxyA);
+ assertThat(delegateA.counter).isEqualTo(1);
+ assertThat(delegateB.counter).isEqualTo(0);
+
+ increaseCounter.invoke(proxyB);
+ increaseCounter.invoke(proxyB);
+ assertThat(delegateA.counter).isEqualTo(1);
+ assertThat(delegateB.counter).isEqualTo(2);
+ }
+
+ public static class UtilityWithPrivateCtor {
+
+ private UtilityWithPrivateCtor() {}
+
+ public static String utilityMethod() {
+ return "util";
+ }
+ }
+
+ @Test
+ void proxyClassWithoutConstructor() throws Exception {
+ Class> proxy = generateProxy(UtilityWithPrivateCtor.class);
+
+ // Not legal in Java code but legal in JVM bytecode
+ assertThat(proxy.getConstructors()).isEmpty();
+
+ assertThat(proxy.getMethod("utilityMethod").invoke(null)).isEqualTo("util");
+ }
+
+ @DummyAnnotation("type")
+ public static class AnnotationRetention {
+
+ @DummyAnnotation("constructor")
+ public AnnotationRetention(@DummyAnnotation("constructor_param") String someValue) {}
+
+ @DummyAnnotation("virtual")
+ public void virtualMethod(@DummyAnnotation("virtual_param") String someValue) {}
+
+ @DummyAnnotation("static")
+ public static void staticMethod(@DummyAnnotation("static_param") String someValue) {}
+ }
+
+ @Test
+ void verifyAnnotationsRetained() throws Exception {
+
+ Class> proxy = generateProxy(AnnotationRetention.class);
+
+ assertThat(proxy.getAnnotation(DummyAnnotation.class))
+ .isNotNull()
+ .extracting(DummyAnnotation::value)
+ .isEqualTo("type");
+
+ Constructor> ctor = proxy.getConstructor(String.class);
+ assertThat(ctor.getAnnotation(DummyAnnotation.class))
+ .isNotNull()
+ .extracting(DummyAnnotation::value)
+ .isEqualTo("constructor");
+ assertThat(ctor.getParameters()[0].getAnnotation(DummyAnnotation.class))
+ .isNotNull()
+ .extracting(DummyAnnotation::value)
+ .isEqualTo("constructor_param");
+
+ Method virtualMethod = proxy.getMethod("virtualMethod", String.class);
+ assertThat(virtualMethod.getAnnotation(DummyAnnotation.class))
+ .isNotNull()
+ .extracting(DummyAnnotation::value)
+ .isEqualTo("virtual");
+ assertThat(virtualMethod.getParameters()[0].getAnnotation(DummyAnnotation.class))
+ .isNotNull()
+ .extracting(DummyAnnotation::value)
+ .isEqualTo("virtual_param");
+
+ Method staticMethod = proxy.getMethod("staticMethod", String.class);
+ assertThat(staticMethod.getAnnotation(DummyAnnotation.class))
+ .isNotNull()
+ .extracting(DummyAnnotation::value)
+ .isEqualTo("static");
+ assertThat(staticMethod.getParameters()[0].getAnnotation(DummyAnnotation.class))
+ .isNotNull()
+ .extracting(DummyAnnotation::value)
+ .isEqualTo("static_param");
+
+ staticMethod.invoke(null, "blub");
+ virtualMethod.invoke(ctor.newInstance("bla"), "blub");
+ }
+
+ public static class CustomSuperClass {
+
+ int inheritedFromSuperclassCount = 0;
+
+ protected void overrideMe() {}
+
+ public void inheritedFromSuperclass() {
+ inheritedFromSuperclassCount++;
+ }
+ }
+
+ public static interface CustomSuperInterface extends Runnable {
+
+ default void inheritedDefault() {
+ if (this instanceof WithSuperTypes) {
+ ((WithSuperTypes) this).inheritedDefaultCount++;
+ }
+ }
+ }
+
+ public static class WithSuperTypes extends CustomSuperClass
+ implements CustomSuperInterface, Callable {
+
+ static WithSuperTypes lastCreatedInstance;
+
+ public WithSuperTypes() {
+ lastCreatedInstance = this;
+ }
+
+ int runInvocCount = 0;
+ int callInvocCount = 0;
+ int overrideMeInvocCount = 0;
+
+ int inheritedDefaultCount = 0;
+
+ @Override
+ public void run() {
+ runInvocCount++;
+ }
+
+ @Override
+ public String call() throws Exception {
+ callInvocCount++;
+ return "foo";
+ }
+
+ @Override
+ public void overrideMe() {
+ overrideMeInvocCount++;
+ }
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ void verifySuperTypes() throws Exception {
+ Object proxy = generateProxy(WithSuperTypes.class).getConstructor().newInstance();
+ WithSuperTypes proxied = WithSuperTypes.lastCreatedInstance;
+
+ ((Runnable) proxy).run();
+ assertThat(proxied.runInvocCount).isEqualTo(1);
+
+ ((Callable) proxy).call();
+ assertThat(proxied.callInvocCount).isEqualTo(1);
+
+ ((CustomSuperClass) proxy).overrideMe();
+ assertThat(proxied.overrideMeInvocCount).isEqualTo(1);
+
+ // Non-overidden, inherited methods are not proxied
+ ((CustomSuperClass) proxy).inheritedFromSuperclass();
+ assertThat(proxied.inheritedFromSuperclassCount).isEqualTo(0);
+ ((CustomSuperInterface) proxy).inheritedDefault();
+ assertThat(proxied.inheritedDefaultCount).isEqualTo(0);
+ }
+
+ @SuppressWarnings({"unused", "MethodCanBeStatic"})
+ public static class IgnoreNonPublicMethods {
+
+ public IgnoreNonPublicMethods() {}
+
+ protected IgnoreNonPublicMethods(int arg) {}
+
+ IgnoreNonPublicMethods(int arg1, int arg2) {}
+
+ private IgnoreNonPublicMethods(int arg1, int arg2, int arg3) {}
+
+ public void publicMethod() {}
+
+ public static void publicStaticMethod() {}
+
+ protected void protectedMethod() {}
+
+ protected static void protectedStaticMethod() {}
+
+ void packageMethod() {}
+
+ static void packageStaticMethod() {}
+
+ private void privateMethod() {}
+
+ private static void privateStaticMethod() {}
+ }
+
+ @Test
+ void verifyNonPublicMembersIgnored() throws Exception {
+ Class> proxy = generateProxy(IgnoreNonPublicMethods.class);
+
+ assertThat(proxy.getConstructors()).hasSize(1);
+ assertThat(proxy.getDeclaredMethods())
+ .hasSize(2)
+ .anySatisfy(method -> assertThat(method.getName()).isEqualTo("publicMethod"))
+ .anySatisfy(method -> assertThat(method.getName()).isEqualTo("publicStaticMethod"));
+ }
+
+ private static Class> generateProxy(Class> clazz) {
+ DynamicType.Unloaded> unloaded =
+ proxyFactory.generateProxy(
+ TypeDescription.ForLoadedType.of(clazz), clazz.getName() + "Proxy");
+ // Uncomment the following line to view the generated bytecode if needed
+ // unloaded.saveIn(new File("generated_proxies"));
+ return unloaded.load(clazz.getClassLoader()).getLoaded();
+ }
+}
diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/DummyAnnotation.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/DummyAnnotation.java
new file mode 100644
index 000000000000..8ce3e917040b
--- /dev/null
+++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/DummyAnnotation.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+public @interface DummyAnnotation {
+ String value();
+}