Avoid proxy IllegalArgumentException: repeated interface
ADD
instrumentation/classloaders/javaagent-unittests/classloaders-javaagent-unittests.gradle
+apply from: "$rootDir/gradle/java.gradle"
+
+dependencies {
+ testImplementation project(':instrumentation:classloaders:javaagent')
+ testImplementation project(':javaagent-bootstrap')
+}

ADD
instrumentation/classloaders/javaagent-unittests/src/test/groovy/ProxyHelperTest.groovy
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import io.opentelemetry.javaagent.bootstrap.FieldBackedContextStoreAppliedMarker
+import io.opentelemetry.javaagent.instrumentation.javaclassloader.ProxyHelper
+import java.util.concurrent.Callable
+import spock.lang.Specification
+
+class ProxyHelperTest extends Specification {
+
+ def "should filter #interfaces"() {
+ expect:
+ ProxyHelper.filtered(interfaces.toArray() as Class<?>[]) == filtered.toArray()
+
+ where:
+ interfaces | filtered
+ [] | []
+ [Runnable] | [Runnable]
+ [Runnable, Callable] | [Runnable, Callable]
+ [Runnable, FieldBackedContextStoreAppliedMarker] | [Runnable, FieldBackedContextStoreAppliedMarker]
+ [Runnable, FieldBackedContextStoreAppliedMarker, FieldBackedContextStoreAppliedMarker] | [Runnable, FieldBackedContextStoreAppliedMarker]
+ [FieldBackedContextStoreAppliedMarker, Runnable] | [FieldBackedContextStoreAppliedMarker, Runnable]
+ [FieldBackedContextStoreAppliedMarker, Runnable, FieldBackedContextStoreAppliedMarker] | [FieldBackedContextStoreAppliedMarker, Runnable]
+ [FieldBackedContextStoreAppliedMarker, FieldBackedContextStoreAppliedMarker, Runnable] | [FieldBackedContextStoreAppliedMarker, Runnable]
+ }
+}

MODIFY
instrumentation/classloaders/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaclassloader/ClassLoaderInstrumentationModule.java
instrumentation/classloaders/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaclassloader/ClassLoaderInstrumentationModule.java
@@ -25,6 +25,9 @@ public class ClassLoaderInstrumentationModule extends InstrumentationModule {
@Override
public List<TypeInstrumentation> typeInstrumentations() {
- return asList(new ClassLoaderInstrumentation(), new ResourceInjectionInstrumentation());
+ return asList(
+ new ClassLoaderInstrumentation(),
+ new ProxyInstrumentation(),
+ new ResourceInjectionInstrumentation());
}
}

ADD
instrumentation/classloaders/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaclassloader/ProxyHelper.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.javaclassloader;
+
+import io.opentelemetry.javaagent.bootstrap.FieldBackedContextStoreAppliedMarker;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+
+public class ProxyHelper {
+
+ private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
+
+ public static Class<?>[] filtered(Class<?>[] interfaces) {
+ int numMarkers = 0;
+ for (Class<?> iface : interfaces) {
+ if (iface == FieldBackedContextStoreAppliedMarker.class) {
+ numMarkers++;
+ }
+ }
+ if (numMarkers <= 1) {
+ // no duplicates, safe to use original interfaces
+ return interfaces;
+ }
+
+ // it's probably ok to remove them all(?)
+ // but just doing the minimum here and only removing the duplicates to prevent
+ // Proxy.newProxyInstance() from throwing IllegalArgumentException: repeated interface
+ return new LinkedHashSet<>(Arrays.asList(interfaces)).toArray(EMPTY_CLASS_ARRAY);
+ }
+}

ADD
instrumentation/classloaders/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/javaclassloader/ProxyInstrumentation.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.javaclassloader;
+
+import static net.bytebuddy.matcher.ElementMatchers.isMethod;
+import static net.bytebuddy.matcher.ElementMatchers.isPublic;
+import static net.bytebuddy.matcher.ElementMatchers.isStatic;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
+import java.lang.reflect.InvocationHandler;
+import java.util.HashMap;
+import java.util.Map;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class ProxyInstrumentation implements TypeInstrumentation {
+
+ @Override
+ public ElementMatcher<TypeDescription> typeMatcher() {
+ return named("java.lang.reflect.Proxy");
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+ Map<ElementMatcher<? super MethodDescription>, String> transformers = new HashMap<>();
+ transformers.put(
+ isMethod()
+ .and(named("newProxyInstance"))
+ .and(takesArguments(3))
+ .and(takesArgument(0, ClassLoader.class))
+ .and(takesArgument(1, Class[].class))
+ .and(takesArgument(2, InvocationHandler.class))
+ .and(isPublic())
+ .and(isStatic()),
+ ProxyInstrumentation.class.getName() + "$FilterDuplicateMarkerInterfaces");
+ transformers.put(
+ isMethod()
+ .and(named("getProxyClass"))
+ .and(takesArguments(2))
+ .and(takesArgument(0, ClassLoader.class))
+ .and(takesArgument(1, Class[].class))
+ .and(isPublic())
+ .and(isStatic()),
+ ProxyInstrumentation.class.getName() + "$FilterDuplicateMarkerInterfaces");
+ return transformers;
+ }
+
+ public static class FilterDuplicateMarkerInterfaces {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void onEnter(
+ @Advice.Argument(value = 1, readOnly = false) Class<?>[] interfaces) {
+ interfaces = ProxyHelper.filtered(interfaces);
+ }
+ }
+}

ADD
instrumentation/classloaders/javaagent/src/test/groovy/NewProxyInstanceTest.groovy
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification
+import io.opentelemetry.javaagent.bootstrap.FieldBackedContextStoreAppliedMarker
+import java.lang.reflect.InvocationHandler
+import java.lang.reflect.Method
+import java.lang.reflect.Proxy
+
+class NewProxyInstanceTest extends AgentInstrumentationSpecification {
+ def "should filter out duplicate FieldBackedContextStoreAppliedMarker interfaces from newProxyInstance"() {
+ setup:
+ Class[] interfaces = new Class[3]
+ interfaces[0] = Runnable
+ interfaces[1] = FieldBackedContextStoreAppliedMarker
+ interfaces[2] = FieldBackedContextStoreAppliedMarker
+
+ expect:
+ Runnable proxy = Proxy.newProxyInstance(NewProxyInstanceTest.getClassLoader(), interfaces, new MyHandler()) as Runnable
+ proxy.run()
+
+ // should not throw IllegalArgumentException:
+ // repeated interface: io.opentelemetry.javaagent.bootstrap.FieldBackedContextStoreAppliedMarker
+ }
+
+ def "should filter out duplicate FieldBackedContextStoreAppliedMarker interfaces from getProxyClass"() {
+ setup:
+ Class[] interfaces = new Class[3]
+ interfaces[0] = Runnable
+ interfaces[1] = FieldBackedContextStoreAppliedMarker
+ interfaces[2] = FieldBackedContextStoreAppliedMarker
+
+ expect:
+ Class<?> proxyClass = Proxy.getProxyClass(NewProxyInstanceTest.getClassLoader(), interfaces)
+ def proxy = proxyClass.newInstance(new MyHandler()) as Runnable
+ proxy.run()
+
+ // should not throw IllegalArgumentException:
+ // repeated interface: io.opentelemetry.javaagent.bootstrap.FieldBackedContextStoreAppliedMarker
+ }
+
+ static class MyHandler implements InvocationHandler {
+
+ @Override
+ Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ return null
+ }
+ }
+}

MODIFY
javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/matcher/GlobalIgnoresMatcher.java
javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/matcher/GlobalIgnoresMatcher.java
@@ -124,6 +124,9 @@ public class GlobalIgnoresMatcher<T extends TypeDescription>
if (name.startsWith("java.rmi.") || name.startsWith("java.util.concurrent.")) {
return false;
}
+ if (name.equals("java.lang.reflect.Proxy")) {
+ return false;
+ }
if (name.equals("java.lang.ClassLoader")) {
return false;
}

MODIFY
settings.gradle
settings.gradle
@@ -86,6 +86,7 @@ include ':instrumentation:cassandra:cassandra-3.0:javaagent'
include ':instrumentation:cassandra:cassandra-4.0:javaagent'
include ':instrumentation:cdi-testing'
include ':instrumentation:classloaders:javaagent'
+include ':instrumentation:classloaders:javaagent-unittests'
include ':instrumentation:couchbase:couchbase-2.0:javaagent'
include ':instrumentation:couchbase:couchbase-2.0:javaagent-unittests'
include ':instrumentation:couchbase:couchbase-2.6:javaagent'

Avoid proxy IllegalArgumentException: repeated interface (#2773)