Implemented factory for generating invokedynamic proxy classes (#9502)
This commit is contained in:
parent
324de7f913
commit
446d9a28ae
|
@ -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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>it has same superclass as the proxied class
|
||||
* <li>it implements all interfaces implemented by the proxied class
|
||||
* <li>for every public constructor of the proxied class, it defined a matching public constructor
|
||||
* which:
|
||||
* <ul>
|
||||
* <li>invokes the default constructor of the superclass
|
||||
* <li>invoked the corresponding constructor of the proxied class to generate the object to
|
||||
* which the proxy delegates
|
||||
* </ul>
|
||||
* <li>it "copies" every declared static and non-static public method, the implementation will
|
||||
* delegate to the corresponding method in the proxied class
|
||||
* <li>all annotations on the proxied class and on its methods are copied to the proxy
|
||||
* </ul>
|
||||
*
|
||||
* <p>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 <init> 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());
|
||||
}
|
||||
}
|
|
@ -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<JavaConstant> 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<String> {
|
||||
|
||||
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<String>) 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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
Loading…
Reference in New Issue