Skip to content

Commit 8865ab6

Browse files
authored
Avoid looking up annotation types during type matching (#5906)
1 parent aac502c commit 8865ab6

File tree

3 files changed

+256
-7
lines changed

3 files changed

+256
-7
lines changed

muzzle/src/main/java/io/opentelemetry/javaagent/tooling/muzzle/AgentCachingPoolStrategy.java

+177-7
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@
1515
import java.util.ArrayList;
1616
import java.util.Collections;
1717
import java.util.List;
18+
import javax.annotation.Nonnull;
1819
import javax.annotation.Nullable;
1920
import net.bytebuddy.agent.builder.AgentBuilder;
2021
import net.bytebuddy.description.annotation.AnnotationList;
22+
import net.bytebuddy.description.annotation.AnnotationValue;
2123
import net.bytebuddy.description.method.MethodDescription;
2224
import net.bytebuddy.description.method.MethodList;
25+
import net.bytebuddy.description.method.ParameterDescription;
26+
import net.bytebuddy.description.method.ParameterList;
2327
import net.bytebuddy.description.type.TypeDescription;
2428
import net.bytebuddy.description.type.TypeList;
2529
import net.bytebuddy.dynamic.ClassFileLocator;
@@ -294,6 +298,8 @@ public void clear() {
294298

295299
/** Based on TypePool.Default.WithLazyResolution */
296300
private class AgentTypePool extends TypePool.Default {
301+
// ThreadLocal used for detecting loading of annotation types
302+
private final ThreadLocal<Boolean> loadingAnnotations = new ThreadLocal<>();
297303
private final WeakReference<ClassLoader> classLoaderRef;
298304

299305
public AgentTypePool(
@@ -331,6 +337,18 @@ protected TypePool.Resolution doResolve(String name) {
331337
return resolution;
332338
}
333339

340+
void enterLoadAnnotations() {
341+
loadingAnnotations.set(Boolean.TRUE);
342+
}
343+
344+
void exitLoadAnnotations() {
345+
loadingAnnotations.set(null);
346+
}
347+
348+
boolean isLoadingAnnotations() {
349+
return loadingAnnotations.get() != null;
350+
}
351+
334352
/** Based on TypePool.Default.WithLazyResolution.LazyResolution */
335353
private class LazyResolution implements TypePool.Resolution {
336354
private final WeakReference<ClassLoader> classLoaderRef;
@@ -343,6 +361,19 @@ private class LazyResolution implements TypePool.Resolution {
343361

344362
@Override
345363
public boolean isResolved() {
364+
// Like jdk, byte-buddy getDeclaredAnnotations does not report annotations whose class is
365+
// missing. To do this it needs to locate the bytes for annotation types used in class.
366+
// Which means that if we have a matcher that matches methods annotated with @Foo byte-buddy
367+
// will end up location bytes for all annotations used on any method in the classes that
368+
// this matcher is applied to. From our perspective this is unreasonable, we just want to
369+
// match based on annotation name with as little overhead as possible. As we match only
370+
// based on annotation name we never need to locate the bytes for the annotation type.
371+
// See TypePool.Default.LazyTypeDescription.LazyAnnotationDescription.asList()
372+
// When isResolved() is called during loading of annotations delay resolving to avoid
373+
// looking up the class bytes.
374+
if (isLoadingAnnotations()) {
375+
return true;
376+
}
346377
return doResolve(name).isResolved();
347378
}
348379

@@ -354,6 +385,11 @@ public TypeDescription resolve() {
354385
// super class and interfaces multiple times
355386
if (cached == null) {
356387
cached = new AgentTypePool.LazyTypeDescription(classLoaderRef, name);
388+
// if we know that an annotation is being loaded wrap the result so that we wouldn't
389+
// need to resolve the class bytes to tell whether it is an annotation
390+
if (isLoadingAnnotations()) {
391+
cached = new AnnotationTypeDescription(cached);
392+
}
357393
}
358394
return cached;
359395
}
@@ -376,7 +412,15 @@ protected TypeDescription delegate() {
376412
@Override
377413
public AnnotationList getDeclaredAnnotations() {
378414
if (annotations == null) {
379-
annotations = delegate().getDeclaredAnnotations();
415+
TypeDescription delegate = delegate();
416+
// Run getDeclaredAnnotations with ThreadLocal. ThreadLocal helps us detect types looked
417+
// up by getDeclaredAnnotations and treat them specially.
418+
enterLoadAnnotations();
419+
try {
420+
annotations = delegate.getDeclaredAnnotations();
421+
} finally {
422+
exitLoadAnnotations();
423+
}
380424
}
381425
return annotations;
382426
}
@@ -413,12 +457,12 @@ public String getName() {
413457
return name;
414458
}
415459

416-
private volatile Generic cachedSuperClass;
460+
private volatile TypeDescription.Generic cachedSuperClass;
417461

418462
@Override
419-
public Generic getSuperClass() {
463+
public TypeDescription.Generic getSuperClass() {
420464
if (cachedSuperClass == null) {
421-
Generic superClassDescription = delegate().getSuperClass();
465+
TypeDescription.Generic superClassDescription = delegate().getSuperClass();
422466
ClassLoader classLoader = classLoaderRef.get();
423467
if (canUseFindLoadedClass() && classLoader != null && superClassDescription != null) {
424468
String superName = superClassDescription.getTypeName();
@@ -445,7 +489,7 @@ public TypeList.Generic getInterfaces() {
445489
// here we use raw types and loose generic info
446490
// we don't expect to have matchers that would use the generic info
447491
List<TypeDescription> result = new ArrayList<>();
448-
for (Generic interfaceDescription : interfaces) {
492+
for (TypeDescription.Generic interfaceDescription : interfaces) {
449493
String interfaceName = interfaceDescription.getTypeName();
450494
Class<?> interfaceClass = findLoadedClass(classLoader, interfaceName);
451495
if (interfaceClass != null) {
@@ -461,6 +505,45 @@ public TypeList.Generic getInterfaces() {
461505
}
462506
return cachedInterfaces;
463507
}
508+
509+
private class LazyAnnotationMethodDescription extends DelegatingMethodDescription {
510+
511+
LazyAnnotationMethodDescription(MethodDescription.InDefinedShape method) {
512+
super(method);
513+
}
514+
515+
@Override
516+
public AnnotationList getDeclaredAnnotations() {
517+
// Run getDeclaredAnnotations with ThreadLocal. ThreadLocal helps us detect types looked
518+
// up by getDeclaredAnnotations and treat them specially.
519+
enterLoadAnnotations();
520+
try {
521+
return method.getDeclaredAnnotations();
522+
} finally {
523+
exitLoadAnnotations();
524+
}
525+
}
526+
}
527+
528+
@Override
529+
public MethodList<MethodDescription.InDefinedShape> getDeclaredMethods() {
530+
MethodList<MethodDescription.InDefinedShape> methods = super.getDeclaredMethods();
531+
532+
class MethodListWrapper extends MethodList.AbstractBase<MethodDescription.InDefinedShape> {
533+
534+
@Override
535+
public MethodDescription.InDefinedShape get(int index) {
536+
return new LazyAnnotationMethodDescription(methods.get(index));
537+
}
538+
539+
@Override
540+
public int size() {
541+
return methods.size();
542+
}
543+
}
544+
545+
return new MethodListWrapper();
546+
}
464547
}
465548

466549
private AgentTypePool.LazyTypeDescriptionWithClass newTypeDescription(Class<?> clazz) {
@@ -493,10 +576,10 @@ public String getName() {
493576
return name;
494577
}
495578

496-
private volatile Generic cachedSuperClass;
579+
private volatile TypeDescription.Generic cachedSuperClass;
497580

498581
@Override
499-
public Generic getSuperClass() {
582+
public TypeDescription.Generic getSuperClass() {
500583
if (cachedSuperClass == null) {
501584
Class<?> clazz = classRef.get();
502585
if (clazz == null) {
@@ -556,4 +639,91 @@ private static AgentTypePool.LazyTypeDescriptionWithClass newLazyTypeDescription
556639
}
557640
return pool.new LazyTypeDescriptionWithClass(clazz);
558641
}
642+
643+
/**
644+
* Class descriptor that claims to represent an annotation without checking whether the underlying
645+
* type really is an annotation.
646+
*/
647+
private static class AnnotationTypeDescription
648+
extends TypeDescription.AbstractBase.OfSimpleType.WithDelegation {
649+
private final TypeDescription delegate;
650+
651+
AnnotationTypeDescription(TypeDescription delegate) {
652+
this.delegate = delegate;
653+
}
654+
655+
@Override
656+
protected TypeDescription delegate() {
657+
return delegate;
658+
}
659+
660+
@Override
661+
public String getName() {
662+
return delegate.getName();
663+
}
664+
665+
@Override
666+
public boolean isAnnotation() {
667+
// by default byte-buddy checks whether class modifiers have annotation bit set
668+
// as we wish to avoid looking up the class bytes we assume that every that was expected
669+
// to be an annotation really is an annotation and return true here
670+
// See TypePool.Default.LazyTypeDescription.LazyAnnotationDescription.asList()
671+
return true;
672+
}
673+
}
674+
675+
private static class DelegatingMethodDescription
676+
extends MethodDescription.InDefinedShape.AbstractBase {
677+
protected final MethodDescription.InDefinedShape method;
678+
679+
DelegatingMethodDescription(MethodDescription.InDefinedShape method) {
680+
this.method = method;
681+
}
682+
683+
@Nonnull
684+
@Override
685+
public TypeDescription getDeclaringType() {
686+
return method.getDeclaringType();
687+
}
688+
689+
@Override
690+
public TypeDescription.Generic getReturnType() {
691+
return method.getReturnType();
692+
}
693+
694+
@Override
695+
public ParameterList<ParameterDescription.InDefinedShape> getParameters() {
696+
return method.getParameters();
697+
}
698+
699+
@Override
700+
public TypeList.Generic getExceptionTypes() {
701+
return method.getExceptionTypes();
702+
}
703+
704+
@Override
705+
public AnnotationValue<?, ?> getDefaultValue() {
706+
return method.getDefaultValue();
707+
}
708+
709+
@Override
710+
public String getInternalName() {
711+
return method.getInternalName();
712+
}
713+
714+
@Override
715+
public TypeList.Generic getTypeVariables() {
716+
return method.getTypeVariables();
717+
}
718+
719+
@Override
720+
public int getModifiers() {
721+
return method.getModifiers();
722+
}
723+
724+
@Override
725+
public AnnotationList getDeclaredAnnotations() {
726+
return method.getDeclaredAnnotations();
727+
}
728+
}
559729
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.tooling.muzzle;
7+
8+
import static net.bytebuddy.matcher.ElementMatchers.declaresMethod;
9+
import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
10+
import static org.junit.jupiter.api.Assertions.assertTrue;
11+
12+
import io.opentelemetry.test.AnnotatedTestClass;
13+
import java.io.IOException;
14+
import java.io.InputStream;
15+
import java.net.URL;
16+
import java.util.Enumeration;
17+
import net.bytebuddy.description.type.TypeDescription;
18+
import net.bytebuddy.dynamic.ClassFileLocator;
19+
import net.bytebuddy.pool.TypePool;
20+
import org.junit.jupiter.api.Test;
21+
22+
class AgentCachingPoolStrategyTest {
23+
24+
@Test
25+
void testSkipResourceLookupForAnnotations() {
26+
ClassLoader classLoader =
27+
new ClassLoader(AgentCachingPoolStrategyTest.class.getClassLoader()) {
28+
29+
private void checkResource(String name) {
30+
if (name.contains("TestAnnotation")) {
31+
throw new IllegalStateException("Unexpected resource lookup for " + name);
32+
}
33+
}
34+
35+
@Override
36+
public URL getResource(String name) {
37+
checkResource(name);
38+
return super.getResource(name);
39+
}
40+
41+
@Override
42+
public InputStream getResourceAsStream(String name) {
43+
checkResource(name);
44+
return super.getResourceAsStream(name);
45+
}
46+
47+
@Override
48+
public Enumeration<URL> getResources(String name) throws IOException {
49+
checkResource(name);
50+
return super.getResources(name);
51+
}
52+
};
53+
54+
ClassFileLocator locator = ClassFileLocator.ForClassLoader.of(classLoader);
55+
TypePool pool = AgentTooling.poolStrategy().typePool(locator, classLoader);
56+
TypePool.Resolution resolution = pool.describe(AnnotatedTestClass.class.getName());
57+
TypeDescription typeDescription = resolution.resolve();
58+
59+
assertTrue(isAnnotatedWith(AnnotatedTestClass.TestAnnotation.class).matches(typeDescription));
60+
assertTrue(
61+
declaresMethod(isAnnotatedWith(AnnotatedTestClass.TestAnnotation.class))
62+
.matches(typeDescription));
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.test;
7+
8+
@AnnotatedTestClass.TestAnnotation
9+
public class AnnotatedTestClass {
10+
11+
@AnnotatedTestClass.TestAnnotation
12+
void testMethod() {}
13+
14+
public @interface TestAnnotation {}
15+
}

0 commit comments

Comments
 (0)