diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/ClassLoadingStrategy.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/ClassLoadingStrategy.java new file mode 100644 index 000000000000..7daca65232fc --- /dev/null +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/ClassLoadingStrategy.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.instrumentation.internal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time.
+ * Explicitly defines the classloader that needs to be used to load classes. + * + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.PACKAGE}) +public @interface ClassLoadingStrategy { + + ClassLoadingTarget value() default ClassLoadingTarget.INSTRUMENTATION_ISOLATED; +} diff --git a/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/ClassLoadingTarget.java b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/ClassLoadingTarget.java new file mode 100644 index 000000000000..ed63a24b155e --- /dev/null +++ b/javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/ClassLoadingTarget.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.extension.instrumentation.internal; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time.
+ */ +public enum ClassLoadingTarget { + /** + * Class or package will be injected into the instrumented classloader, as if the class was + * present in the application classpath. When referenced directly from instrumentation advice or + * helper classes, it should never be loaded in the instrumentation module or agent classloader. + */ + INSTRUMENTATION_TARGET, + /** + * Class or package will be injected into an isolated instrumentation module classloader when + * using InvokeDynamic instrumentation. This is the default for most instrumentation classes as + * they need to be isolated from the instrumented application.
+ * When using inlined instrumentation, this is equivalent to {@link #INSTRUMENTATION_TARGET}. + */ + INSTRUMENTATION_ISOLATED, + /** + * Class or package will be injected into a shared instrumentation module classloader when using + * InvokeDynamic instrumentation. This should be used for shared libraries classes and classes + * that are used across multiple instrumentation modules.
+ * When using inlined instrumentation, this is equivalent to {@link #INSTRUMENTATION_TARGET}. + */ + INSTRUMENTATION_SHARED +} diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ClassLoadingTargetUtil.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ClassLoadingTargetUtil.java new file mode 100644 index 000000000000..6b818aa51f07 --- /dev/null +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ClassLoadingTargetUtil.java @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy; + +import io.opentelemetry.javaagent.extension.instrumentation.internal.ClassLoadingStrategy; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ClassLoadingTarget; +import java.io.IOException; +import java.io.InputStream; +import javax.annotation.Nullable; +import net.bytebuddy.utility.StreamDrainer; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; + +/** Utility to help with class-loading indy modules */ +public class ClassLoadingTargetUtil { + + private static final Type STRATEGY_ANNOTATION = Type.getType(ClassLoadingStrategy.class); + private static final Type TARGET_ENUM = Type.getType(ClassLoadingTarget.class); + + private ClassLoadingTargetUtil() {} + + /** + * Reads the class class-loading strategy from class (or package) bytecode + * + * @param bytecode class or package bytecode + * @return class loading strategy, defaults to {@link ClassLoadingTarget#INSTRUMENTATION_ISOLATED} + * if annotation is not present. + */ + @Nullable + private static ClassLoadingTarget getTarget(byte[] bytecode) { + ClassReader cr = new ClassReader(bytecode); + ClassNode classNode = new ClassNode(); + cr.accept(classNode, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG); + if (classNode.visibleAnnotations != null) { + for (AnnotationNode annotation : classNode.visibleAnnotations) { + if (Type.getType(annotation.desc).equals(STRATEGY_ANNOTATION)) { + for (Object value : annotation.values) { + if (value instanceof String[]) { + String[] array = (String[]) value; + if (array.length == 2 && Type.getType(array[0]).equals(TARGET_ENUM)) { + return ClassLoadingTarget.valueOf(array[1]); + } + } + } + } + } + } + return null; + } + + /** + * Get class target with fallback on package target. + * + * @param className class name + * @param classLoader class loader to load class/package bytecode + * @return class loading target, or {@literal null} of no annotation is present + */ + @Nullable + public static ClassLoadingTarget getClassTarget(String className, ClassLoader classLoader) { + String classPath = className.replace(".", "/") + ".class"; + byte[] byteCode = getByteCode(classPath, classLoader); + if (byteCode == null) { + // class is not present in this CL + return null; + } + ClassLoadingTarget classTarget = getTarget(byteCode); + if (null != classTarget) { + return classTarget; + } + String packageName = className.substring(0, className.lastIndexOf('.')); + ClassLoadingTarget packageTarget = packageTarget(packageName, classLoader); + if (packageTarget != null) { + return packageTarget; + } + return null; + } + + // package-private for testing + @Nullable + static ClassLoadingTarget packageTarget(String packageName, ClassLoader classLoader) { + String packagePath = packageName.replace(".", "/") + "/package-info.class"; + byte[] byteCode = getByteCode(packagePath, classLoader); + return byteCode == null ? null : getTarget(byteCode); + } + + // package-private for testing + @Nullable + static ClassLoadingTarget classTarget(String className, ClassLoader classLoader) { + String classPath = className.replace(".", "/") + ".class"; + byte[] byteCode = getByteCode(classPath, classLoader); + if (byteCode == null) { + return null; + } + return getTarget(byteCode); + } + + @Nullable + private static byte[] getByteCode(String resourcePath, ClassLoader classLoader) { + try (InputStream input = classLoader.getResourceAsStream(resourcePath)) { + if (input == null) { + return null; + } + return StreamDrainer.DEFAULT.drain(input); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ClassLoadingTargetUtilTest.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ClassLoadingTargetUtilTest.java new file mode 100644 index 000000000000..d3c7c7007028 --- /dev/null +++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/ClassLoadingTargetUtilTest.java @@ -0,0 +1,75 @@ +/* + * 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.extension.instrumentation.internal.ClassLoadingStrategy; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ClassLoadingTarget; +import io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies.targetcl.DummyInherit; +import io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies.targetcl.DummyOverride; +import org.junit.jupiter.api.Test; + +public class ClassLoadingTargetUtilTest { + + @Test + void checkTarget() { + // not defined at class nor package level + testStrategy(AClass.class, null); + + // explicitly set at class level + testExplicitAnnotation(BClass.class, ClassLoadingTarget.INSTRUMENTATION_ISOLATED); + testExplicitAnnotation(CClass.class, ClassLoadingTarget.INSTRUMENTATION_SHARED); + testExplicitAnnotation(DClass.class, ClassLoadingTarget.INSTRUMENTATION_TARGET); + + // explicitly set at package level + String packageName = "io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies.targetcl"; + assertThat(ClassLoadingTargetUtil.packageTarget(packageName, getClass().getClassLoader())) + .isEqualTo(ClassLoadingTarget.INSTRUMENTATION_SHARED); + + // package defined values inherited on class + assertThat(DummyInherit.class.getAnnotation(ClassLoadingStrategy.class)) + .describedAs("no annotation is present on class") + .isNull(); + assertThat( + ClassLoadingTargetUtil.getClassTarget( + DummyInherit.class.getName(), getClass().getClassLoader())) + .describedAs("package annotation is applied for class lookup") + .isEqualTo(ClassLoadingTarget.INSTRUMENTATION_SHARED); + + // class lookup has priority over package when defined on both + testExplicitAnnotation(DummyOverride.class, ClassLoadingTarget.INSTRUMENTATION_ISOLATED); + + // should defend against non-existing class + assertThat( + ClassLoadingTargetUtil.getClassTarget( + "this.class.does.not.Exists", getClass().getClassLoader())) + .isNull(); + } + + private void testStrategy(Class type, ClassLoadingTarget expected) { + assertThat(ClassLoadingTargetUtil.getClassTarget(type.getName(), getClass().getClassLoader())) + .isEqualTo(expected); + } + + private void testExplicitAnnotation(Class type, ClassLoadingTarget expected) { + ClassLoadingStrategy annotation = type.getAnnotation(ClassLoadingStrategy.class); + assertThat(annotation).isNotNull(); + assertThat(annotation.value()).isEqualTo(expected); + testStrategy(type, expected); + } + + private static class AClass {} + + @ClassLoadingStrategy(ClassLoadingTarget.INSTRUMENTATION_ISOLATED) + private static class BClass {} + + @ClassLoadingStrategy(ClassLoadingTarget.INSTRUMENTATION_SHARED) + private static class CClass {} + + @ClassLoadingStrategy(ClassLoadingTarget.INSTRUMENTATION_TARGET) + private static class DClass {} +} diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/targetcl/DummyInherit.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/targetcl/DummyInherit.java new file mode 100644 index 000000000000..0dcbfa8d6963 --- /dev/null +++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/targetcl/DummyInherit.java @@ -0,0 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies.targetcl; + +public class DummyInherit {} diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/targetcl/DummyOverride.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/targetcl/DummyOverride.java new file mode 100644 index 000000000000..dabf14e99e8c --- /dev/null +++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/targetcl/DummyOverride.java @@ -0,0 +1,12 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies.targetcl; + +import io.opentelemetry.javaagent.extension.instrumentation.internal.ClassLoadingStrategy; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ClassLoadingTarget; + +@ClassLoadingStrategy(ClassLoadingTarget.INSTRUMENTATION_ISOLATED) +public class DummyOverride {} diff --git a/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/targetcl/package-info.java b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/targetcl/package-info.java new file mode 100644 index 000000000000..502ff1ab6afe --- /dev/null +++ b/javaagent-tooling/src/test/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/dummies/targetcl/package-info.java @@ -0,0 +1,5 @@ +@ClassLoadingStrategy(ClassLoadingTarget.INSTRUMENTATION_SHARED) +package io.opentelemetry.javaagent.tooling.instrumentation.indy.dummies.targetcl; + +import io.opentelemetry.javaagent.extension.instrumentation.internal.ClassLoadingStrategy; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ClassLoadingTarget;