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.
+ *
+ *
+ * - when using inline instrumentation, this is ignored and behavior is equivalent to {@link
+ * ClassLoadingTarget#INSTRUMENTATION_TARGET} as classes are injected into the instrumented CL
+ *
- when using indy instrumentation and not explicitly set, the behavior is equivalent to an
+ * explicit {@link ClassLoadingTarget#INSTRUMENTATION_ISOLATED}
+ *
- when using indy and classes/packages need to be shared across multiple isolated
+ * classloaders, the {@link ClassLoadingTarget#INSTRUMENTATION_SHARED} should be used
+ *
+ */
+@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;