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: + * + * + * + *

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 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 bootstrapArgs = + bootstrapArgsProvider.getBootstrapArgsForMethod(classToProxy, method); + builder = createProxyConstructor(superClass, method, bootstrapArgs, builder); + } else if (method.isMethod()) { + List bootstrapArgs = + bootstrapArgsProvider.getBootstrapArgsForMethod(classToProxy, method); + builder = createProxyMethod(method, bootstrapArgs, builder); + } + } + } + return builder.make(); + } + + private DynamicType.Builder createProxyMethod( + MethodDescription.InDefinedShape proxiedMethod, + List 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 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(); +}