First pass at supporting annotations inheritance for JAX-RS
[skip ci]
MODIFY
dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ByteBuddyElementMatchers.java
dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ByteBuddyElementMatchers.java
package datadog.trace.agent.tooling;
+import static net.bytebuddy.matcher.ElementMatchers.hasSignature;
+
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
@@ -7,8 +9,10 @@ import java.util.List;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.build.HashCodeAndEqualsPlugin;
+import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDefinition;
import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.description.type.TypeList;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;
@@ -296,4 +300,85 @@ public class ByteBuddyElementMatchers {
}
}
}
+
+ // TODO: add javadoc
+ public static <T extends MethodDescription> ElementMatcher.Junction<T> hasSuperMethod(
+ final ElementMatcher<? super MethodDescription> matcher) {
+ return new HasSuperMethodMatcher<>(matcher);
+ }
+
+ // TODO: add javadoc
+ @HashCodeAndEqualsPlugin.Enhance
+ public static class HasSuperMethodMatcher<T extends MethodDescription>
+ extends ElementMatcher.Junction.AbstractBase<T> {
+
+ private final ElementMatcher<? super MethodDescription> matcher;
+
+ public HasSuperMethodMatcher(final ElementMatcher<? super MethodDescription> matcher) {
+ this.matcher = matcher;
+ }
+
+ @Override
+ public boolean matches(final MethodDescription target) {
+ if (target.isConstructor()) {
+ return false;
+ }
+ final Junction<MethodDescription> signatureMatcher = hasSignature(target.asSignatureToken());
+ TypeDefinition declaringType = target.getDeclaringType();
+ final Set<TypeDefinition> checkedInterfaces = new HashSet<>();
+
+ while (declaringType != null) {
+ for (final MethodDescription methodDescription :
+ declaringType.getDeclaredMethods().filter(signatureMatcher)) {
+ if (matcher.matches(methodDescription)) {
+ return true;
+ }
+ }
+ if (matchesInterface(declaringType.getInterfaces(), signatureMatcher, checkedInterfaces)) {
+ return true;
+ }
+ declaringType = safeGetSuperClass(declaringType);
+ }
+ return false;
+ }
+
+ private boolean matchesInterface(
+ final TypeList.Generic interfaces,
+ final Junction<MethodDescription> signatureMatcher,
+ final Set<TypeDefinition> checkedInterfaces) {
+ for (final TypeDefinition type : interfaces) {
+ if (!checkedInterfaces.contains(type)) {
+ checkedInterfaces.add(type);
+ for (final MethodDescription methodDescription :
+ type.getDeclaredMethods().filter(signatureMatcher)) {
+ if (matcher.matches(methodDescription)) {
+ return true;
+ }
+ }
+ if (matchesInterface(type.getInterfaces(), signatureMatcher, checkedInterfaces)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private TypeDefinition safeGetSuperClass(final TypeDefinition typeDefinition) {
+ try {
+ return typeDefinition.getSuperClass();
+ } catch (final Exception e) {
+ log.debug(
+ "{} trying to get super class for target {}: {}",
+ e.getClass().getSimpleName(),
+ safeTypeDefinitionName(typeDefinition),
+ e.getMessage());
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "hasSuperMethodMatcher(" + matcher + ")";
+ }
+ }
}

MODIFY
dd-java-agent/instrumentation/jax-rs-annotations-1/src/main/java/datadog/trace/instrumentation/jaxrs1/JaxRsAnnotationsDecorator.java
dd-java-agent/instrumentation/jax-rs-annotations-1/src/main/java/datadog/trace/instrumentation/jaxrs1/JaxRsAnnotationsDecorator.java
@@ -10,8 +10,6 @@ import datadog.trace.instrumentation.api.AgentSpan;
import datadog.trace.instrumentation.api.Tags;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.ws.rs.HttpMethod;
@@ -83,9 +81,22 @@ public class JaxRsAnnotationsDecorator extends BaseDecorator {
String resourceName = classMap.get(method);
if (resourceName == null) {
- final String httpMethod = locateHttpMethod(method);
- final List<Path> paths = gatherPaths(target, method);
- resourceName = buildResourceName(httpMethod, paths);
+ String httpMethod = null;
+ Path methodPath = null;
+ final Path classPath = findClassPath(target);
+ for (final Method current : new OverriddenMethodIterable(target, method)) {
+ if (httpMethod == null) {
+ httpMethod = locateHttpMethod(method);
+ }
+ if (methodPath == null) {
+ methodPath = findMethodPath(method);
+ }
+ // TODO figure out if these will ever be on different methods.
+ if (method != null && methodPath != null) {
+ break;
+ }
+ }
+ resourceName = buildResourceName(httpMethod, classPath, methodPath);
classMap.put(method, resourceName);
}
@@ -102,40 +113,51 @@ public class JaxRsAnnotationsDecorator extends BaseDecorator {
return httpMethod;
}
- private List<Path> gatherPaths(Class<Object> target, final Method method) {
- final List<Path> paths = new ArrayList();
+ private Path findMethodPath(final Method method) {
+ return method.getAnnotation(Path.class);
+ }
+
+ private Path findClassPath(Class<Object> target) {
while (target != null && target != Object.class) {
final Path annotation = target.getAnnotation(Path.class);
if (annotation != null) {
- paths.add(annotation);
- break; // Annotation overridden, no need to continue.
+ // Annotation overridden, no need to continue.
+ return annotation;
}
target = target.getSuperclass();
}
- final Path methodPath = method.getAnnotation(Path.class);
- if (methodPath != null) {
- paths.add(methodPath);
- }
- return paths;
+ return null;
}
- private String buildResourceName(final String httpMethod, final List<Path> paths) {
+ private String buildResourceName(
+ final String httpMethod, final Path classPath, final Path methodPath) {
final String resourceName;
final StringBuilder resourceNameBuilder = new StringBuilder();
if (httpMethod != null) {
resourceNameBuilder.append(httpMethod);
resourceNameBuilder.append(" ");
}
- Path last = null;
- for (final Path path : paths) {
- if (path.value().startsWith("/") || (last != null && last.value().endsWith("/"))) {
- resourceNameBuilder.append(path.value());
- } else {
+ boolean skipSlash = false;
+ if (classPath != null) {
+ if (!classPath.value().startsWith("/")) {
resourceNameBuilder.append("/");
- resourceNameBuilder.append(path.value());
}
- last = path;
+ resourceNameBuilder.append(classPath.value());
+ skipSlash = classPath.value().endsWith("/");
+ }
+
+ if (methodPath != null) {
+ String path = methodPath.value();
+ if (skipSlash) {
+ if (path.startsWith("/")) {
+ path = path.length() == 1 ? "" : path.substring(1);
}
+ } else if (!path.startsWith("/")) {
+ resourceNameBuilder.append("/");
+ }
+ resourceNameBuilder.append(path);
+ }
+
resourceName = resourceNameBuilder.toString().trim();
return resourceName;
}

MODIFY
dd-java-agent/instrumentation/jax-rs-annotations-1/src/main/java/datadog/trace/instrumentation/jaxrs1/JaxRsAnnotationsInstrumentation.java
dd-java-agent/instrumentation/jax-rs-annotations-1/src/main/java/datadog/trace/instrumentation/jaxrs1/JaxRsAnnotationsInstrumentation.java
package datadog.trace.instrumentation.jaxrs1;
+import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.hasSuperMethod;
import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
import static datadog.trace.agent.tooling.ClassLoaderMatcher.classLoaderHasClasses;
import static datadog.trace.instrumentation.api.AgentTracer.activateSpan;
@@ -9,6 +10,7 @@ import static datadog.trace.instrumentation.jaxrs1.JaxRsAnnotationsDecorator.DEC
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.declaresMethod;
import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
+import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
@@ -55,6 +57,9 @@ public final class JaxRsAnnotationsInstrumentation extends Instrumenter.Default
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
+ isMethod()
+ .and(
+ hasSuperMethod(
isAnnotatedWith(
named("javax.ws.rs.Path")
.or(named("javax.ws.rs.DELETE"))
@@ -62,7 +67,7 @@ public final class JaxRsAnnotationsInstrumentation extends Instrumenter.Default
.or(named("javax.ws.rs.HEAD"))
.or(named("javax.ws.rs.OPTIONS"))
.or(named("javax.ws.rs.POST"))
- .or(named("javax.ws.rs.PUT"))),
+ .or(named("javax.ws.rs.PUT"))))),
JaxRsAnnotationsAdvice.class.getName());
}

ADD
dd-java-agent/instrumentation/jax-rs-annotations-1/src/main/java/datadog/trace/instrumentation/jaxrs1/OverriddenMethodIterable.java
+package datadog.trace.instrumentation.jaxrs1;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Queue;
+
+public class OverriddenMethodIterable implements Iterable<Method> {
+ private final Class baseClass;
+ private final Method baseMethod;
+
+ public OverriddenMethodIterable(final Class target, final Method baseMethod) {
+ baseClass = target;
+ this.baseMethod = baseMethod;
+ }
+
+ @Override
+ public Iterator<Method> iterator() {
+ return new Iterator<Method>() {
+ private Method next = baseMethod;
+ private Class<?> currentClass = baseClass;
+ private boolean analyzeCurrentClass = !baseMethod.getDeclaringClass().equals(baseClass);
+ private final Queue<Class<?>> interfaces = new LinkedList<>();
+
+ @Override
+ public boolean hasNext() {
+ if (next == null) {
+ calculateNext();
+ }
+ return next == null;
+ }
+
+ @Override
+ public Method next() {
+ final Method next = this.next;
+ this.next = null;
+ return next;
+ }
+
+ @Override
+ public void remove() {
+ // Ignore
+ }
+
+ private void calculateNext() {
+ if (currentClass == null) {
+ return;
+ }
+ if (analyzeCurrentClass) {
+ next = findMatchingMethod(currentClass.getDeclaredMethods());
+ if (next != null) {
+ return;
+ }
+ interfaces.addAll(Arrays.asList(currentClass.getInterfaces()));
+ }
+ if (!interfaces.isEmpty()) {
+ final Class<?> nextInterface = interfaces.remove();
+ interfaces.addAll(Arrays.asList(nextInterface.getInterfaces()));
+ next = findMatchingMethod(nextInterface.getDeclaredMethods());
+ if (next != null) {
+ return;
+ }
+ } else {
+ currentClass = currentClass.getSuperclass();
+ analyzeCurrentClass = true;
+ }
+ }
+
+ private Method findMatchingMethod(final Method[] methods) {
+ nextMethod:
+ for (final Method method : methods) {
+ if (!baseMethod.getName().equals(method.getName())) {
+ continue;
+ }
+ if (!baseMethod.getReturnType().equals(method.getReturnType())) {
+ continue;
+ }
+ final Class<?>[] baseParameterTypes = baseMethod.getParameterTypes();
+ final Class<?>[] parameterTypes = method.getParameterTypes();
+ if (baseParameterTypes.length != parameterTypes.length) {
+ continue;
+ }
+ for (int i = 0; i < baseParameterTypes.length; i++) {
+ if (!baseParameterTypes[i].equals(parameterTypes[i])) {
+ continue nextMethod;
+ }
+ }
+ return method;
+ }
+ return null;
+ }
+ };
+ }
+}

MODIFY
dd-java-agent/instrumentation/jax-rs-annotations-1/src/test/java/Resource.java
dd-java-agent/instrumentation/jax-rs-annotations-1/src/test/java/Resource.java
@@ -9,13 +9,15 @@ public interface Resource {
String hello(final String name);
@Path("/test")
- interface SubResource {
+ interface SubResource extends Cloneable, Resource {
+ @Override
@POST
@Path("/hello/{name}")
String hello(@PathParam("name") final String name);
}
class Test1 implements SubResource {
+ @Override
public String hello(final String name) {
return "Test1 " + name + "!";
}
@@ -23,12 +25,14 @@ public interface Resource {
@Path("/test2")
class Test2 implements SubResource {
+ @Override
public String hello(final String name) {
return "Test2 " + name + "!";
}
}
class Test3 extends Test1 {
+ @Override
public String hello(final String name) {
return "Test3 " + name + "!";
}

First pass at supporting annotations inheritance for JAX-RS

[skip ci]