diff --git a/apm-agent-benchmarks/src/main/java/co/elastic/apm/agent/benchmark/MockHttpServletRequest.java b/apm-agent-benchmarks/src/main/java/co/elastic/apm/agent/benchmark/MockHttpServletRequest.java index d471cd311f..90a7be2447 100644 --- a/apm-agent-benchmarks/src/main/java/co/elastic/apm/agent/benchmark/MockHttpServletRequest.java +++ b/apm-agent-benchmarks/src/main/java/co/elastic/apm/agent/benchmark/MockHttpServletRequest.java @@ -11,9 +11,9 @@ * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY diff --git a/apm-agent-benchmarks/src/main/java/co/elastic/apm/agent/benchmark/MockHttpServletResponse.java b/apm-agent-benchmarks/src/main/java/co/elastic/apm/agent/benchmark/MockHttpServletResponse.java index 9ebbdc565e..973f58ab58 100644 --- a/apm-agent-benchmarks/src/main/java/co/elastic/apm/agent/benchmark/MockHttpServletResponse.java +++ b/apm-agent-benchmarks/src/main/java/co/elastic/apm/agent/benchmark/MockHttpServletResponse.java @@ -11,9 +11,9 @@ * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmAgent.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmAgent.java index 1d40a9c57e..b90c63d576 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmAgent.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmAgent.java @@ -389,6 +389,7 @@ public static synchronized void reset() { transformer.reset(instrumentation, RedefinitionStrategy.RETRANSFORMATION); } dynamicClassFileTransformers.clear(); + HelperClassManager.ForDispatcher.clear(); instrumentation = null; } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmInstrumentation.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmInstrumentation.java index c32f812da0..025d600c07 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmInstrumentation.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmInstrumentation.java @@ -11,9 +11,9 @@ * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @@ -37,6 +37,8 @@ import javax.annotation.Nullable; import java.security.ProtectionDomain; import java.util.Collection; +import java.util.Collections; +import java.util.List; import static net.bytebuddy.matcher.ElementMatchers.any; @@ -169,6 +171,14 @@ public Advice.OffsetMapping.Factory getOffsetMapping() { return null; } - public void onTypeMatch(TypeDescription typeDescription, ClassLoader classLoader, ProtectionDomain protectionDomain, @Nullable Class classBeingRedefined) { + public List helpers() throws Exception { + return Collections.emptyList(); + } + + public void onTypeMatch(TypeDescription typeDescription, ClassLoader classLoader, ProtectionDomain protectionDomain, @Nullable Class classBeingRedefined) throws Exception { + List helpers = helpers(); + if (!helpers.isEmpty()) { + HelperClassManager.ForDispatcher.inject(classLoader, protectionDomain, helpers); + } } } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/HelperClassManager.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/HelperClassManager.java index d06a6ac705..1fd8981b3e 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/HelperClassManager.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/HelperClassManager.java @@ -24,27 +24,46 @@ */ package co.elastic.apm.agent.bci; +import co.elastic.apm.agent.bootstrap.MethodHandleDispatcher; import co.elastic.apm.agent.impl.ElasticApmTracer; import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.method.MethodList; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.ClassFileLocator; import net.bytebuddy.dynamic.loading.ByteArrayClassLoader; import net.bytebuddy.dynamic.loading.ClassInjector; +import net.bytebuddy.dynamic.loading.MultipleParentClassLoader; import net.bytebuddy.dynamic.loading.PackageDefinitionStrategy; +import net.bytebuddy.pool.TypePool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; import java.lang.ref.WeakReference; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentMap; + +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; /** * This class helps to overcome the fact that the agent classes can't access the classes they want to instrument. @@ -142,7 +161,9 @@ public T getForClassLoaderOfClass(Class classOfTargetClassLoader) { *

* * @param the type of the helper class interface + * @deprecated In favor of {@link ForDispatcher} */ + @Deprecated public static class ForSingleClassLoader extends HelperClassManager { @Nullable @@ -199,7 +220,9 @@ public T doGetForClassLoaderOfClass(Class classOfTargetClassLoader) { * startup time and when new applications are deployed). These maps shouldn't grow big as they have an entry per class loader. * * @param + * @deprecated In favor of {@link ForDispatcher} */ + @Deprecated public static class ForAnyClassLoader extends HelperClassManager { // doesn't need to be concurrent - invoked only from a synchronized context @@ -271,7 +294,154 @@ private synchronized T loadAndReferenceHelper(Class classOfTargetClassLoader) } } - static Class injectClass(@Nullable ClassLoader targetClassLoader, @Nullable ProtectionDomain pd, String className, boolean isBootstrapClass) throws IOException, ClassNotFoundException { + public static class ForDispatcher { + + private static final Map>> alreadyInjected = new WeakHashMap>>(); + + /** + * Creates an isolated CL that has two parents: the target class loader and the agent CL. + * The agent class loader is currently the bootstrap CL but in the future it will be an isolated CL that is a child of the bootstrap CL. + *

+ * After the helper CL is created, it registers {@link RegisterMethodHandle}-annotated methods in {@link MethodHandleDispatcher}. + * These method handles are then called from within advices ({@link net.bytebuddy.asm.Advice.OnMethodEnter}/{@link net.bytebuddy.asm.Advice.OnMethodExit}). + * This lets them call the helpers that are loaded from the helper CL. + * The helpers have full access to both the agent API and the types visible to the target CL (such as the servlet API). + *

+ *

+ * See {@link MethodHandleDispatcher} for a diagram. + *

+ */ + public synchronized static void inject(@Nullable ClassLoader targetClassLoader, @Nullable ProtectionDomain protectionDomain, List classesToInject) throws Exception { + classesToInject = new ArrayList<>(classesToInject); + classesToInject.add(MethodHandleRegisterer.class.getName()); + + Set> injectedClasses = getOrCreateInjectedClasses(targetClassLoader); + if (injectedClasses.contains(classesToInject)) { + return; + } + injectedClasses.add(classesToInject); + logger.debug("Creating helper class loader for {} containing {}", targetClassLoader, classesToInject); + + ClassLoader parent = getHelperClassLoaderParent(targetClassLoader); + Map typeDefinitions = getTypeDefinitions(classesToInject); + // child first semantics are important here as the helper CL contains classes that are also present in the agent CL + ClassLoader helperCL = new ByteArrayClassLoader.ChildFirst(parent, true, typeDefinitions); + + registerMethodHandles(classesToInject, helperCL, targetClassLoader, protectionDomain); + } + + private static Set> getOrCreateInjectedClasses(@Nullable ClassLoader targetClassLoader) { + Set> injectedClasses = alreadyInjected.get(targetClassLoader); + if (injectedClasses == null) { + injectedClasses = new HashSet<>(); + alreadyInjected.put(targetClassLoader, injectedClasses); + } + return injectedClasses; + } + + private static ConcurrentMap ensureDispatcherForClassLoaderCreated(@Nullable ClassLoader targetClassLoader, @Nullable ProtectionDomain protectionDomain) throws IOException, ClassNotFoundException, IllegalAccessException, NoSuchFieldException { + ConcurrentMap dispatcherForClassLoader = MethodHandleDispatcher.getDispatcherForClassLoader(targetClassLoader); + ConcurrentMap reflectionDispatcherForClassLoader = MethodHandleDispatcher.getReflectionDispatcherForClassLoader(targetClassLoader); + if (dispatcherForClassLoader == null) { + // there's always a dispatcher for the bootstrap CL + assert targetClassLoader != null; + Class dispatcher = injectClass(targetClassLoader, protectionDomain, MethodHandleDispatcherHolder.class.getName(), true); + dispatcherForClassLoader = (ConcurrentMap) dispatcher.getField("registry").get(null); + reflectionDispatcherForClassLoader = (ConcurrentMap) dispatcher.getField("reflectionRegistry").get(null); + MethodHandleDispatcher.setDispatcherForClassLoader(targetClassLoader, dispatcherForClassLoader, reflectionDispatcherForClassLoader); + } + return dispatcherForClassLoader; + } + + @Nullable + private static ClassLoader getHelperClassLoaderParent(@Nullable ClassLoader targetClassLoader) { + ClassLoader agentClassLoader = HelperClassManager.class.getClassLoader(); + if (agentClassLoader != null && agentClassLoader != ClassLoader.getSystemClassLoader()) { + // future world: when the agent is loaded from an isolated class loader + // the helper class loader has both, the agent class loader and the target class loader as the parent + return new MultipleParentClassLoader(Arrays.asList(agentClassLoader, targetClassLoader)); + } + return targetClassLoader; + } + + /** + * Gets all {@link RegisterMethodHandle} annotated methods and registers them in {@link MethodHandleDispatcher} + */ + private static void registerMethodHandles(List classesToInject, ClassLoader helperCL, @Nullable ClassLoader targetClassLoader, @Nullable ProtectionDomain protectionDomain) throws Exception { + ensureDispatcherForClassLoaderCreated(targetClassLoader, protectionDomain); + ConcurrentMap dispatcherForClassLoader = MethodHandleDispatcher.getDispatcherForClassLoader(targetClassLoader); + ConcurrentMap reflectionDispatcherForClassLoader = MethodHandleDispatcher.getReflectionDispatcherForClassLoader(targetClassLoader); + + Class methodHandleRegistererClass = (Class) Class.forName(MethodHandleRegisterer.class.getName(), true, helperCL); + TypePool typePool = TypePool.Default.of(getAgentClassFileLocator()); + for (String name : classesToInject) { + // check with Byte Buddy matchers, acting on the byte code as opposed to reflection first + // to avoid NoClassDefFoundErrors when the classes refer to optional types + MethodList helperMethods = typePool.describe(name) + .resolve() + .getDeclaredMethods() + .filter(isAnnotatedWith(named(RegisterMethodHandle.class.getName()))); + if (!helperMethods.isEmpty()) { + assertPublic(helperMethods); + + Class clazz = Class.forName(name, true, helperCL); + if (clazz.getClassLoader() != helperCL) { + throw new IllegalStateException("Helper classes not loaded from helper class loader, instead loaded from " + clazz.getClassLoader()); + } + try { + // call in from the context of the created helper class loader so that helperClass.getMethods() doesn't throw NoClassDefFoundError + Method registerStaticMethodHandles = methodHandleRegistererClass.getMethod("registerStaticMethodHandles", ConcurrentMap.class, ConcurrentMap.class, Class.class); + registerStaticMethodHandles.invoke(null, dispatcherForClassLoader, reflectionDispatcherForClassLoader, clazz); + } catch (Exception e) { + logger.error("Exception while trying to register method handles for {}", name); + if (e instanceof InvocationTargetException) { + Throwable targetException = ((InvocationTargetException) e).getTargetException(); + if (targetException instanceof Exception) { + throw (Exception) targetException; + } + } + throw e; + } + } + } + } + + private static void assertPublic(MethodList methods) throws IllegalAccessException { + for (MethodDescription.InDefinedShape privateHelperMethod : methods.filter(not(isPublic()))) { + throw new IllegalAccessException("Helper method is not public: " + privateHelperMethod); + } + } + + /** + * This class is loaded form the helper class loader so that {@link Class#getDeclaredMethods()} doesn't throw a + * {@link NoClassDefFoundError}. + * This would otherwise happen as the methods may reference types not visible to the agent class loader (such as the servlet API). + */ + public static class MethodHandleRegisterer { + + public static void registerStaticMethodHandles(ConcurrentMap dispatcher, ConcurrentMap reflectionDispatcher, Class helperClass) throws ReflectiveOperationException { + for (Method method : helperClass.getMethods()) { + if (Modifier.isStatic(method.getModifiers()) && method.getAnnotation(RegisterMethodHandle.class) != null) { + MethodHandle methodHandle = MethodHandles.lookup().unreflect(method); + String key = helperClass.getName() + "#" + method.getName(); + // intern() to speed up map lookups (short-circuits String::equals via reference equality check) + MethodHandle previousValue = dispatcher.put(key.intern(), methodHandle); + reflectionDispatcher.put(key.intern(), method); + if (previousValue != null) { + throw new IllegalArgumentException("There is already a mapping for '" + key + "'"); + } + } + } + } + } + + public synchronized static void clear() { + alreadyInjected.clear(); + MethodHandleDispatcher.clear(); + } + } + + static Class injectClass(@Nullable ClassLoader targetClassLoader, @Nullable ProtectionDomain pd, String className, boolean isBootstrapClass) throws IOException, ClassNotFoundException { if (targetClassLoader == null) { if (isBootstrapClass) { return Class.forName(className, false, null); @@ -335,15 +505,23 @@ private static Class loadHelperClass(@Nullable ClassLoader targetClassLoa private static Map getTypeDefinitions(List helperClassNames) throws IOException { Map typeDefinitions = new HashMap<>(); + ClassFileLocator agentClassFileLocator = getAgentClassFileLocator(); for (final String helperName : helperClassNames) { - final byte[] classBytes = getAgentClassBytes(helperName); + final byte[] classBytes = agentClassFileLocator.locate(helperName).resolve(); typeDefinitions.put(helperName, classBytes); } return typeDefinitions; } private static byte[] getAgentClassBytes(String className) throws IOException { - final ClassFileLocator locator = ClassFileLocator.ForClassLoader.of(ClassLoader.getSystemClassLoader()); - return locator.locate(className).resolve(); + return getAgentClassFileLocator().locate(className).resolve(); + } + + private static ClassFileLocator getAgentClassFileLocator() { + ClassLoader agentClassLoader = HelperClassManager.class.getClassLoader(); + if (agentClassLoader == null) { + agentClassLoader = ClassLoader.getSystemClassLoader(); + } + return ClassFileLocator.ForClassLoader.of(agentClassLoader); } } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/MethodHandleDispatcherHolder.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/MethodHandleDispatcherHolder.java new file mode 100644 index 0000000000..405c438d96 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/MethodHandleDispatcherHolder.java @@ -0,0 +1,48 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 - 2020 Elastic and contributors + * %% + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * #L% + */ +package co.elastic.apm.agent.bci; + +import co.elastic.apm.agent.bootstrap.MethodHandleDispatcher; + +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Method; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * A class that holds a static reference to method handler dispatcher for a specific class loader. + * See also {@link MethodHandleDispatcher#dispatcherByClassLoader} + * Used to be loaded by the parent of the class loader that loads the helper + * itself, thus making the helper instance non-GC-eligible as long as the parent class loader is alive. + * NOTE: THIS CLASS SHOULD NEVER BE INSTANTIATED NOR REFERENCED EXPLICITLY, IT SHOULD ONLY BE USED THROUGH REFLECTION + */ +public class MethodHandleDispatcherHolder { + public static final ConcurrentMap registry = new ConcurrentHashMap(); + public static final ConcurrentMap reflectionRegistry = new ConcurrentHashMap(); + + // should never be instanciated + private MethodHandleDispatcherHolder() { + } +} diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/InputStreamFactoryHelperImpl.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/RegisterMethodHandle.java similarity index 60% rename from apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/InputStreamFactoryHelperImpl.java rename to apm-agent-core/src/main/java/co/elastic/apm/agent/bci/RegisterMethodHandle.java index 843a6fb126..cf7c0dbc53 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/InputStreamFactoryHelperImpl.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/RegisterMethodHandle.java @@ -22,16 +22,18 @@ * under the License. * #L% */ -package co.elastic.apm.agent.servlet.helper; +package co.elastic.apm.agent.bci; -import co.elastic.apm.agent.impl.context.Request; -import co.elastic.apm.agent.servlet.RequestStreamRecordingInstrumentation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; -import javax.servlet.ServletInputStream; - -public class InputStreamFactoryHelperImpl implements RequestStreamRecordingInstrumentation.InputStreamWrapperFactory { - @Override - public ServletInputStream wrap(Request request, ServletInputStream servletInputStream) { - return new RecordingServletInputStreamWrapper(request, servletInputStream); - } +/** + * Static methods annotated with this will be registered in {@link co.elastic.apm.agent.bootstrap.MethodHandleDispatcher}. + * This enables advices to call them by looking them up via {@link co.elastic.apm.agent.bootstrap.MethodHandleDispatcher#getMethodHandle(Class, String)} + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface RegisterMethodHandle { } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bootstrap/MethodHandleDispatcher.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bootstrap/MethodHandleDispatcher.java new file mode 100644 index 0000000000..9608b17cbb --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bootstrap/MethodHandleDispatcher.java @@ -0,0 +1,190 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 - 2020 Elastic and contributors + * %% + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * #L% + */ +package co.elastic.apm.agent.bootstrap; + +import co.elastic.apm.agent.annotation.NonnullApi; +import co.elastic.apm.agent.bci.MethodHandleDispatcherHolder; +import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap; + +import javax.annotation.Nullable; +import java.lang.invoke.MethodHandle; +import java.lang.ref.WeakReference; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * This class lives in the bootstrap classloader so it's visible to all classes. + * It is used to register method handles in order to call methods of classes that live in another classloader hierarchy. + * This is the only class that needs + */ +@NonnullApi +public class MethodHandleDispatcher { + + /** + * Java 7 has poor support for method handles. + * There are bugs in Java 7 that lead to segfaults after a method handle has been compiled by C2. + * See also https://github.com/elastic/apm-agent-java/issues/458 + */ + public static final boolean USE_REFLECTION = System.getProperty("java.version").startsWith("1.7."); + /** + * If the value was not weakly referenced, + * this would keep the {@link MethodHandle}s alive, + * which would keep the corresponding helper class alive, + * which would keep the helper classloader alive, + * which would keep the application class loader alive. + * This would lead to a class loader leak if the web application is undeployed (all classes of the web app can't be unloaded). + *
+     *   Bootstrap CL ←─────────────────────────────────── Agent CL
+     *       ↑ └{@link MethodHandleDispatcher}              ↑
+     *       │   └ WeakConcurrentMap>>
+     *     Ext/Platform CL           ╷                      │                          ╷
+     *       ↑                       ╷                      │                          ╷
+     *     System CL                 ╷                      │                          ╷
+     *       ↑                       ╷                      │                          ╷
+     *     Common                    ╷                      │                          ╷
+     *     ↑    ↑                    ╷                      │                          ╷
+     * WebApp1  WebApp2 ←╶╶╶╶╶╶╶╶╶╶╶╶┘                      │                          ╷
+     *          ↑ - {@link MethodHandleDispatcherHolder}    │                          ╷
+     *          │   - ConcurrentMap   │                          ╷
+     *          │          ┌────────────────────┘           │                          ╷
+     *          Helper CL ─┼────────────────────────────────┘                          ╷
+     *           │         ↓                                                           ╷
+     *           └ HelperClass ←╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶╶┘
+     * Legend:
+     *  ╶╶ weak reference
+     *  ── strong reference
+     * 
+ */ + private static WeakConcurrentMap>> dispatcherByClassLoader = new WeakConcurrentMap.WithInlinedExpunction<>(); + private static WeakConcurrentMap>> reflectionDispatcherByClassLoader = new WeakConcurrentMap.WithInlinedExpunction<>(); + private static ConcurrentMap bootstrapDispatcher = new ConcurrentHashMap<>(); + private static ConcurrentMap reflectionBootstrapDispatcher = new ConcurrentHashMap<>(); + + /** + * Calls {@link ConcurrentMap#clear()} on all injected {@link MethodHandleDispatcherHolder}s. + *

+ * This should make all helper class loaders eligible for GC. + * That is because the {@link java.lang.invoke.MethodHandle}s registered in the {@link MethodHandleDispatcherHolder} should be the only + * references to the classes loaded by the helper class loader. + *

+ *

+ * As the helper class loaders are (ideally) the only references that keep the Agent CL alive, + * this also makes the Agent CL eligible for CL. + *

+ */ + public synchronized static void clear() { + for (WeakConcurrentMap>> map : Arrays.asList(dispatcherByClassLoader, reflectionDispatcherByClassLoader)) { + for (Map.Entry>> entry : map) { + ConcurrentMap dispatcher = entry.getValue().get(); + if (dispatcher != null) { + dispatcher.clear(); + } + } + map.clear(); + } + bootstrapDispatcher.clear(); + reflectionBootstrapDispatcher.clear(); + } + + /** + * Gets a {@link MethodHandle}, mostly referring to a static helper method called from an advice method. + *

+ * An advice method is a static method that is annotated with {@link net.bytebuddy.asm.Advice.OnMethodEnter} + * or {@link net.bytebuddy.asm.Advice.OnMethodExit}. + *

+ *

+ * Helper methods are classloader specific so that they are able to reference types specific to that class loader. + * An example would be a callback interface like {@code okhttp3.Callback}. + * When there are multiple web applications deployed to the same servlet container, they might all use different versions of OkHttp. + *

+ * + * @param classOfTargetClassLoader + * @param methodHandleName + * @return + */ + public static MethodHandle getMethodHandle(Class classOfTargetClassLoader, String methodHandleName) { + return getMethodHandle(classOfTargetClassLoader.getClassLoader(), methodHandleName); + } + + public static Method getMethod(Class classOfTargetClassLoader, String methodHandleName) { + return getMethod(classOfTargetClassLoader.getClassLoader(), methodHandleName); + } + + public static MethodHandle getMethodHandle(ClassLoader targetClassLoader, String methodHandleName) { + ConcurrentMap dispatcherForClassLoader = getDispatcherForClassLoader(targetClassLoader); + if (dispatcherForClassLoader != null) { + MethodHandle methodHandle = dispatcherForClassLoader.get(methodHandleName); + if (methodHandle != null) { + return methodHandle; + } + } + throw new IllegalArgumentException("No method handle found for " + methodHandleName); + } + + + public static Method getMethod(ClassLoader targetClassLoader, String methodHandleName) { + ConcurrentMap dispatcherForClassLoader = getReflectionDispatcherForClassLoader(targetClassLoader); + if (dispatcherForClassLoader != null) { + Method methodHandle = dispatcherForClassLoader.get(methodHandleName); + if (methodHandle != null) { + return methodHandle; + } + } + throw new IllegalArgumentException("No method found for " + methodHandleName); + } + + public synchronized static void setDispatcherForClassLoader(ClassLoader classLoader, ConcurrentMap dispatcherMap, ConcurrentMap reflectionDispatcherMap) { + dispatcherByClassLoader.put(classLoader, new WeakReference<>(dispatcherMap)); + reflectionDispatcherByClassLoader.put(classLoader, new WeakReference<>(reflectionDispatcherMap)); + } + + @Nullable + public static ConcurrentMap getDispatcherForClassLoader(@Nullable ClassLoader classLoader) { + if (classLoader == null) { + return bootstrapDispatcher; + } + WeakReference> reference = dispatcherByClassLoader.get(classLoader); + if (reference != null) { + return reference.get(); + } + return null; + } + + @Nullable + public static ConcurrentMap getReflectionDispatcherForClassLoader(@Nullable ClassLoader classLoader) { + if (classLoader == null) { + return reflectionBootstrapDispatcher; + } + WeakReference> reference = reflectionDispatcherByClassLoader.get(classLoader); + if (reference != null) { + return reference.get(); + } + return null; + } + +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/util/CallDepth.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/util/CallDepth.java new file mode 100644 index 0000000000..139b8f3e8d --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/util/CallDepth.java @@ -0,0 +1,72 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 - 2020 Elastic and contributors + * %% + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * #L% + */ +package co.elastic.apm.agent.util; + +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A utility that makes it easy to detect nested method calls. + */ +public class CallDepth { + private static final ThreadLocal, AtomicInteger>> callDepthPerThread = new ThreadLocal, AtomicInteger>>(); + + /** + * Gets and increments the call depth counter. + * Returns {@code 0} if this is the outer-most (non-nested) invocation. + * + * @param clazz the class for which the call depth should be counted. + * Used as a key to distinguish multiple counters for a thread. + * @return the call depth before it has been incremented + */ + public static int increment(Class clazz) { + Map, AtomicInteger> callDepthForCurrentThread = callDepthPerThread.get(); + if (callDepthForCurrentThread == null) { + callDepthForCurrentThread = new WeakHashMap, AtomicInteger>(); + callDepthPerThread.set(callDepthForCurrentThread); + } + AtomicInteger depth = callDepthForCurrentThread.get(clazz); + if (depth == null) { + depth = new AtomicInteger(); + callDepthForCurrentThread.put(clazz, depth); + } + return depth.getAndIncrement(); + } + + /** + * Decrements and gets the call depth counter. + * Returns {@code 0} if this is the outer-most (non-nested) invocation. + * + * @param clazz the class for which the call depth should be counted. + * Used as a key to distinguish multiple counters for a thread. + * @return the call depth after it has been incremented + */ + public static int decrement(Class clazz) { + int depth = callDepthPerThread.get().get(clazz).decrementAndGet(); + assert depth >= 0; + return depth; + } +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/util/PackageScanner.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/util/PackageScanner.java new file mode 100644 index 0000000000..2752c84155 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/util/PackageScanner.java @@ -0,0 +1,101 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 - 2020 Elastic and contributors + * %% + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * #L% + */ +package co.elastic.apm.agent.util; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +public class PackageScanner { + + /** + * Returns all class names within a package and sub-packages + * + * @param basePackage the package to scan + * @return all class names within a package and sub-packages + * @throws IOException + * @throws URISyntaxException + */ + public static List getClassNames(final String basePackage) throws IOException, URISyntaxException { + String baseFolderResource = basePackage.replace('.', '/'); + final List classNames = new ArrayList<>(); + Enumeration resources = getResourcesFromAgentClassLoader(baseFolderResource); + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + URI uri = resource.toURI(); + if (uri.getScheme().equals("jar")) { + // avoids FileSystemAlreadyExistsException + synchronized (PackageScanner.class) { + try (FileSystem fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap())) { + final Path basePath = fileSystem.getPath(baseFolderResource).toAbsolutePath(); + classNames.addAll(listClassNames(basePackage, basePath)); + } + } + } else { + final Path basePath = Paths.get(uri); + if (basePath.toString().contains("test-classes")) { + continue; + } + classNames.addAll(listClassNames(basePackage, basePath)); + } + } + return classNames; + } + + private static List listClassNames(final String basePackage, final Path basePath) throws IOException { + final List classNames = new ArrayList<>(); + Files.walkFileTree(basePath, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (file.toString().endsWith(".class")) { + classNames.add(basePackage + "." + basePath.relativize(file).toString().replace('/', '.').replace(".class", "")); + } + return FileVisitResult.CONTINUE; + } + }); + return classNames; + } + + private static Enumeration getResourcesFromAgentClassLoader(String baseFolderResource) throws IOException { + ClassLoader agentCL = PackageScanner.class.getClassLoader(); + if (agentCL == null) { + agentCL = ClassLoader.getSystemClassLoader(); + } + return agentCL.getResources(baseFolderResource); + } +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/HelperClassManagerTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/HelperClassManagerTest.java index 876f6c72c1..4a58c75a78 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/HelperClassManagerTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/bci/HelperClassManagerTest.java @@ -11,9 +11,9 @@ * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @@ -24,17 +24,23 @@ */ package co.elastic.apm.agent.bci; +import co.elastic.apm.agent.bootstrap.MethodHandleDispatcher; import co.elastic.apm.agent.impl.ElasticApmTracer; import org.assertj.core.api.ThrowableAssert; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import java.lang.invoke.WrongMethodTypeException; import java.lang.ref.WeakReference; import java.net.URL; import java.net.URLClassLoader; +import java.util.Collections; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.mock; class HelperClassManagerTest { @@ -47,6 +53,11 @@ class HelperClassManagerTest { } } + @AfterEach + void tearDown() { + HelperClassManager.ForDispatcher.clear(); + } + @Test void testLoadHelperSingleClassLoader() { new InnerTestClass().testLoadHelperSingleClassLoader(); @@ -124,6 +135,134 @@ void testCaching() throws ClassNotFoundException, InterruptedException { assertThat(HelperClassManager.ForAnyClassLoader.clId2helperImplListMap.get(targetClassLoader3)).isNotNull(); } + @Test + void testForDispatcher() throws Throwable { + injectForDispatcher(TestHelper.class); + assertThat(MethodHandleDispatcher + .getMethodHandle(HelperClassManagerTest.class, "co.elastic.apm.agent.bci.HelperClassManagerTest$TestHelper#getParentClassLoader") + .invoke()) + .isEqualTo(HelperClassManagerTest.class.getClassLoader()); + assertThat(MethodHandleDispatcher + .getMethod(HelperClassManagerTest.class, "co.elastic.apm.agent.bci.HelperClassManagerTest$TestHelper#getParentClassLoader") + .invoke(null)) + .isEqualTo(HelperClassManagerTest.class.getClassLoader()); + } + + @Test + void testHelperClassLoaderUnloading() throws Throwable { + injectForDispatcher(TestHelper.class); + WeakReference helperClassLoader = new WeakReference<>((ClassLoader) MethodHandleDispatcher + .getMethodHandle(HelperClassManagerTest.class, "co.elastic.apm.agent.bci.HelperClassManagerTest$TestHelper#getClassLoader") + .invoke()); + + MethodHandleDispatcher.clear(); + System.gc(); + + await().untilAsserted(() -> assertThat(helperClassLoader.get()).isNull()); + } + + public static class TestHelper { + @RegisterMethodHandle + public static ClassLoader getClassLoader() { + return TestHelper.class.getClassLoader(); + } + @RegisterMethodHandle + public static ClassLoader getParentClassLoader() { + return TestHelper.class.getClassLoader().getParent(); + } + } + + @Test + void testHelperClassLoaderThreadLocalLeak() throws Throwable { + HelperClassManager.ForDispatcher.inject(HelperClassManagerTest.class.getClassLoader(), HelperClassManagerTest.class.getProtectionDomain(), + List.of("co.elastic.apm.agent.bci.HelperClassManagerTest$TestHelperThreadLocalLeak", + "co.elastic.apm.agent.bci.HelperClassManagerTest$TestHelperThreadLocalLeak$1")); + WeakReference helperClassLoader = new WeakReference<>((ClassLoader) MethodHandleDispatcher + .getMethodHandle(HelperClassManagerTest.class, "co.elastic.apm.agent.bci.HelperClassManagerTest$TestHelperThreadLocalLeak#getClassLoader") + .invoke()); + + MethodHandleDispatcher.clear(); + System.gc(); + Thread.sleep(100); + assertThat(helperClassLoader.get()).isNotNull(); + } + + public static class TestHelperThreadLocalLeak { + private static final ThreadLocal threadLocal = new ThreadLocal() { + // This creates a class loader leak as java.lang.Thread.threadLocals (loaded from bootstrap CL) + // now references a class from the helper CL (this anonymous inner class) + @Override + protected Boolean initialValue() { + return Boolean.TRUE; + } + }; + + @RegisterMethodHandle + public static ClassLoader getClassLoader() { + assertThat(threadLocal.get()).isTrue(); + return TestHelper.class.getClassLoader(); + } + } + + @Test + void testWrongArguments() throws Throwable { + injectForDispatcher(TestHelper.class); + assertThatThrownBy(() -> MethodHandleDispatcher + .getMethodHandle(HelperClassManagerTest.class, "co.elastic.apm.agent.bci.HelperClassManagerTest$TestHelper#getParentClassLoader") + .invoke(boolean.class)) + .isInstanceOf(WrongMethodTypeException.class); + } + + private void injectForDispatcher(Class helper) throws Exception { + HelperClassManager.ForDispatcher.inject(HelperClassManagerTest.class.getClassLoader(), HelperClassManagerTest.class.getProtectionDomain(), Collections.singletonList(helper.getName())); + } + + @Test + void testForDispatcherOverloadedMethod() { + assertThatThrownBy(() -> injectForDispatcher(OverloadedMethodHelper.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("There is already a mapping for"); + } + + public static class OverloadedMethodHelper { + @RegisterMethodHandle + public static void foo() { + } + @RegisterMethodHandle + public static void foo(boolean bar) { + } + } + + @Test + void testForDispatcherPrivateMethod() { + assertThatThrownBy(() -> injectForDispatcher(HelperWithPrivateMethod.class)) + .isInstanceOf(IllegalAccessException.class); + } + + public static class HelperWithPrivateMethod { + @RegisterMethodHandle + private static void foo() { + } + } + + @Test + void testMultipleCL() throws Throwable { + ClassLoader targetClassLoader1 = new URLClassLoader(new URL[]{}, getClass().getClassLoader()); + HelperClassManager.ForDispatcher.inject(targetClassLoader1, getClass().getProtectionDomain(), Collections.singletonList(TestHelper.class.getName())); + assertThat(MethodHandleDispatcher + .getMethodHandle(targetClassLoader1, "co.elastic.apm.agent.bci.HelperClassManagerTest$TestHelper#getParentClassLoader") + .invoke()) + .isEqualTo(targetClassLoader1); + + ClassLoader targetClassLoader2 = new URLClassLoader(new URL[]{}, getClass().getClassLoader()); + HelperClassManager.ForDispatcher.inject(targetClassLoader2, getClass().getProtectionDomain(), Collections.singletonList(TestHelper.class.getName())); + assertThat(MethodHandleDispatcher + .getMethodHandle(targetClassLoader2, "co.elastic.apm.agent.bci.HelperClassManagerTest$TestHelper#getParentClassLoader") + .invoke()) + .isEqualTo(targetClassLoader2); + } + + private void assertFailLoadingOnlyOnce(HelperClassManager helperClassManager, Class libClass1) { ThrowableAssert.ThrowingCallable throwingCallable = () -> helperClassManager.doGetForClassLoaderOfClass(libClass1); assertThatThrownBy(throwingCallable).isInstanceOf(RuntimeException.class); diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/util/PackageScannerTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/util/PackageScannerTest.java new file mode 100644 index 0000000000..9286c38d4f --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/util/PackageScannerTest.java @@ -0,0 +1,59 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 - 2020 Elastic and contributors + * %% + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * #L% + */ +package co.elastic.apm.agent.util; + +import net.bytebuddy.ByteBuddy; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PackageScannerTest { + + @Test + public void getClassNames() throws Exception { + assertThat(PackageScanner.getClassNames(getClass().getPackageName())) + .contains(PackageScanner.class.getName()); + } + + @Test + public void getDoesNotContainTestClasses() throws Exception { + assertThat(PackageScanner.getClassNames(getClass().getPackageName())) + .doesNotContain(getClass().getName()); + } + + @Test + public void testScanJar() throws Exception { + assertThat(PackageScanner.getClassNames(ByteBuddy.class.getPackageName())) + .contains(ByteBuddy.class.getName()); + // scan again to see verify there's no FileSystemAlreadyExistsException + assertThat(PackageScanner.getClassNames(ByteBuddy.class.getPackageName())) + .contains(ByteBuddy.class.getName()); + } + + @Test + public void getClassNamesOfNonExistentPackage() throws Exception { + assertThat(PackageScanner.getClassNames("foo.bar")).isEmpty(); + } +} diff --git a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/ConnectionInstrumentation.java b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/ConnectionInstrumentation.java index 244d0b71dc..ff801ecdfb 100644 --- a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/ConnectionInstrumentation.java +++ b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/ConnectionInstrumentation.java @@ -24,9 +24,7 @@ */ package co.elastic.apm.agent.jdbc; -import co.elastic.apm.agent.bci.VisibleForAdvice; -import co.elastic.apm.agent.impl.ElasticApmTracer; -import co.elastic.apm.agent.jdbc.helper.JdbcHelper; +import co.elastic.apm.agent.bootstrap.MethodHandleDispatcher; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.NamedElement; import net.bytebuddy.description.method.MethodDescription; @@ -52,24 +50,19 @@ */ public class ConnectionInstrumentation extends JdbcInstrumentation { - public ConnectionInstrumentation(ElasticApmTracer tracer) { - super(tracer); - } - - @VisibleForAdvice @Advice.OnMethodExit(suppress = Throwable.class) - public static void storeSql(@Advice.Return PreparedStatement statement, - @Advice.Argument(0) String sql) { - - if (jdbcHelperManager == null) { - return; + private static void storeSql(@Advice.Origin Class clazz, + @Advice.Return PreparedStatement statement, + @Advice.Argument(0) String sql) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#mapStatementToSql") + .invoke(null, statement, sql); + } else { + MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#mapStatementToSql") + .invoke(statement, sql); } - - JdbcHelper jdbcHelper = jdbcHelperManager.getForClassLoaderOfClass(PreparedStatement.class); - if (jdbcHelper != null) { - jdbcHelper.mapStatementToSql(statement, sql); - } - } @Override diff --git a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/JdbcInstrumentation.java b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/JdbcInstrumentation.java index 87ad638219..82944fc406 100644 --- a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/JdbcInstrumentation.java +++ b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/JdbcInstrumentation.java @@ -25,31 +25,19 @@ package co.elastic.apm.agent.jdbc; import co.elastic.apm.agent.bci.ElasticApmInstrumentation; -import co.elastic.apm.agent.bci.HelperClassManager; -import co.elastic.apm.agent.bci.VisibleForAdvice; -import co.elastic.apm.agent.impl.ElasticApmTracer; -import co.elastic.apm.agent.jdbc.helper.JdbcHelper; +import co.elastic.apm.agent.util.PackageScanner; -import javax.annotation.Nullable; import java.util.Collection; import java.util.Collections; +import java.util.List; public abstract class JdbcInstrumentation extends ElasticApmInstrumentation { private static final Collection JDBC_GROUPS = Collections.singleton("jdbc"); - @VisibleForAdvice - @Nullable - public static HelperClassManager jdbcHelperManager = null; - - public JdbcInstrumentation(ElasticApmTracer tracer) { - synchronized (JdbcInstrumentation.class) { - if (jdbcHelperManager == null) { - jdbcHelperManager = HelperClassManager.ForSingleClassLoader.of(tracer, - "co.elastic.apm.agent.jdbc.helper.JdbcHelperImpl", - "co.elastic.apm.agent.jdbc.helper.JdbcHelperImpl$1"); - } - } + @Override + public List helpers() throws Exception { + return PackageScanner.getClassNames(getClass().getPackage().getName()); } @Override diff --git a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/StatementInstrumentation.java b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/StatementInstrumentation.java index 25665ffa6e..b7d0803429 100644 --- a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/StatementInstrumentation.java +++ b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/StatementInstrumentation.java @@ -25,9 +25,8 @@ package co.elastic.apm.agent.jdbc; import co.elastic.apm.agent.bci.VisibleForAdvice; -import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.bootstrap.MethodHandleDispatcher; import co.elastic.apm.agent.impl.transaction.Span; -import co.elastic.apm.agent.jdbc.helper.JdbcHelper; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.NamedElement; import net.bytebuddy.description.method.MethodDescription; @@ -55,8 +54,7 @@ public abstract class StatementInstrumentation extends JdbcInstrumentation { private final ElementMatcher methodMatcher; - StatementInstrumentation(ElasticApmTracer tracer, ElementMatcher methodMatcher) { - super(tracer); + StatementInstrumentation(ElementMatcher methodMatcher) { this.methodMatcher = methodMatcher; } @@ -90,8 +88,8 @@ public ElementMatcher getMethodMatcher() { @SuppressWarnings("DuplicatedCode") public static class ExecuteWithQueryInstrumentation extends StatementInstrumentation { - public ExecuteWithQueryInstrumentation(ElasticApmTracer tracer) { - super(tracer, + public ExecuteWithQueryInstrumentation() { + super( named("execute").or(named("executeQuery")) .and(takesArgument(0, String.class)) .and(isPublic()) @@ -101,48 +99,37 @@ public ExecuteWithQueryInstrumentation(ElasticApmTracer tracer) { @Nullable @VisibleForAdvice @Advice.OnMethodEnter(suppress = Throwable.class) - public static Span onBeforeExecute(@Advice.This Statement statement, - @Advice.Argument(0) String sql) { - - if (tracer == null || jdbcHelperManager == null) { - return null; - } - - JdbcHelper helperImpl = jdbcHelperManager.getForClassLoaderOfClass(Statement.class); - if (helperImpl == null) { - return null; + private static Span onBeforeExecute(@Advice.Origin Class clazz, + @Advice.This Statement statement, + @Advice.Argument(0) String sql) throws Throwable { + + if (MethodHandleDispatcher.USE_REFLECTION) { + return (Span) MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#createJdbcSpan") + .invoke(null, sql, statement, false); + } else { + return (Span) MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#createJdbcSpan") + .invoke(sql, statement, false); } - - return helperImpl.createJdbcSpan(sql, statement, tracer.getActive(), false); } @VisibleForAdvice @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) - public static void onAfterExecute(@Advice.This Statement statement, - @Advice.Enter @Nullable Span span, - @Advice.Thrown @Nullable Throwable t) { - if (span == null) { - return; + private static void onAfterExecute(@Advice.Origin Class clazz, + @Advice.This Statement statement, + @Advice.Enter @Nullable Span span, + @Advice.Thrown @Nullable Throwable t) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#onAfterExecuteQuery") + .invoke(null, statement, span, t); + } else { + MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#onAfterExecuteQuery") + .invoke(statement, span, t); } - - if (t == null && jdbcHelperManager != null) { - JdbcHelper helper = jdbcHelperManager.getForClassLoaderOfClass(Statement.class); - if (helper != null) { - long count = helper.safeGetUpdateCount(statement); - if (count != Long.MIN_VALUE) { - span.getContext() - .getDb() - .withAffectedRowsCount(count); - } - } - - } - - span.captureException(t) - .deactivate() - .end(); - } } @@ -161,8 +148,8 @@ public static void onAfterExecute(@Advice.This Statement statement, */ public static class ExecuteUpdateWithQueryInstrumentation extends StatementInstrumentation { - public ExecuteUpdateWithQueryInstrumentation(ElasticApmTracer tracer) { - super(tracer, + public ExecuteUpdateWithQueryInstrumentation() { + super( named("executeUpdate").or(named("executeLargeUpdate")) .and(takesArgument(0, String.class)) .and(isPublic()) @@ -172,39 +159,37 @@ public ExecuteUpdateWithQueryInstrumentation(ElasticApmTracer tracer) { @Nullable @VisibleForAdvice @Advice.OnMethodEnter(suppress = Throwable.class) - public static Span onBeforeExecute(@Advice.This Statement statement, - @Advice.Argument(0) String sql) { - if (tracer == null || jdbcHelperManager == null) { - return null; - } - - JdbcHelper helperImpl = jdbcHelperManager.getForClassLoaderOfClass(Statement.class); - if (helperImpl == null) { - return null; + private static Span onBeforeExecute(@Advice.Origin Class clazz, + @Advice.This Statement statement, + @Advice.Argument(0) String sql) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + return (Span) MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#createJdbcSpan") + .invoke(null, sql, statement, false); + } else { + return (Span) MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#createJdbcSpan") + .invoke(sql, statement, false); } - - return helperImpl.createJdbcSpan(sql, statement, tracer.getActive(), false); } @VisibleForAdvice @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) - public static void onAfterExecute(@Advice.Enter @Nullable Span span, - @Advice.Thrown @Nullable Throwable t, - @Advice.Return long returnValue /* bytebuddy converts int to long for us here ! */) { - if (span == null) { - return; - } - - if (t == null) { - span.getContext() - .getDb() - .withAffectedRowsCount(returnValue); + private static void onAfterExecute(@Advice.Origin Class clazz, + @Advice.Enter @Nullable Span span, + @Advice.Thrown @Nullable Throwable t, + @Advice.Return long returnValue /* bytebuddy converts int to long for us here ! */) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#onAfter") + .invoke(null, span, t, returnValue); + } else { + MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#onAfter") + .invoke(span, t, returnValue); } - - span.captureException(t) - .deactivate() - .end(); } + } /** @@ -212,8 +197,8 @@ public static void onAfterExecute(@Advice.Enter @Nullable Span span, */ public static class AddBatchInstrumentation extends StatementInstrumentation { - public AddBatchInstrumentation(ElasticApmTracer tracer) { - super(tracer, + public AddBatchInstrumentation() { + super( nameStartsWith("addBatch") .and(takesArgument(0, String.class)) .and(isPublic()) @@ -221,14 +206,15 @@ public AddBatchInstrumentation(ElasticApmTracer tracer) { } @Advice.OnMethodEnter(suppress = Throwable.class) - public static void storeSql(@Advice.This Statement statement, @Advice.Argument(0) String sql) { - if (jdbcHelperManager == null) { - return; - } - - JdbcHelper helperImpl = jdbcHelperManager.getForClassLoaderOfClass(Statement.class); - if (helperImpl != null) { - helperImpl.mapStatementToSql(statement, sql); + private static void storeSql(@Advice.Origin Class clazz, @Advice.This Statement statement, @Advice.Argument(0) String sql) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#mapStatementToSql") + .invoke(null, statement, sql); + } else { + MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#mapStatementToSql") + .invoke(statement, sql); } } } @@ -237,14 +223,14 @@ public static void storeSql(@Advice.This Statement statement, @Advice.Argument(0 /** * Instruments: - *
    - *
  • {@link Statement#executeBatch()}
  • - *
  • {@link Statement#executeLargeBatch()} (java8)
  • - *
+ *
    + *
  • {@link Statement#executeBatch()}
  • + *
  • {@link Statement#executeLargeBatch()} (java8)
  • + *
*/ public static class ExecuteBatchInstrumentation extends StatementInstrumentation { - public ExecuteBatchInstrumentation(ElasticApmTracer tracer) { - super(tracer, + public ExecuteBatchInstrumentation() { + super( named("executeBatch").or(named("executeLargeBatch")) .and(takesArguments(0)) .and(isPublic()) @@ -254,66 +240,45 @@ public ExecuteBatchInstrumentation(ElasticApmTracer tracer) { @Nullable @Advice.OnMethodEnter(suppress = Throwable.class) - @SuppressWarnings("DuplicatedCode") - public static Span onBeforeExecute(@Advice.This Statement statement) { - if (tracer == null || jdbcHelperManager == null) { - return null; - } - JdbcHelper helper = jdbcHelperManager.getForClassLoaderOfClass(Statement.class); - if (helper == null) { - return null; + private static Span onBeforeExecute(@Advice.Origin Class clazz, @Advice.This Statement statement) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + return (Span) MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#createJdbcSpanLookupSql") + .invoke(null, statement, false); + } else { + return (Span) MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#createJdbcSpanLookupSql") + .invoke(statement, false); } - - String sql = helper.retrieveSqlForStatement(statement); - return helper.createJdbcSpan(sql, statement, tracer.getActive(), true); - } @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) - public static void onAfterExecute(@Advice.Enter @Nullable Span span, - @Advice.Thrown Throwable t, - @Advice.Return Object returnValue) { - if (span == null) { - return; - } - - // for 'executeBatch' and 'executeLargeBatch', we have to compute the sum as Statement.getUpdateCount() - // does not seem to return the sum of all elements. As we can use instanceof to check return type - // we do not need to use a separate advice. 'execute' return value is auto-boxed into a Boolean, - // but there is no extra allocation. - long affectedCount = 0; - if (returnValue instanceof int[]) { - int[] array = (int[]) returnValue; - for (int i = 0; i < array.length; i++) { - affectedCount += array[i]; - } - } else if (returnValue instanceof long[]) { - long[] array = (long[]) returnValue; - for (int i = 0; i < array.length; i++) { - affectedCount += array[i]; - } + private static void onAfterExecute(@Advice.Origin Class clazz, + @Advice.Enter @Nullable Span span, + @Advice.Thrown Throwable t, + @Advice.Return Object returnValue) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#onAfterExecuteBatch") + .invoke(null, span, t, returnValue); + } else { + MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#onAfterExecuteBatch") + .invoke(span, t, returnValue); } - span.getContext() - .getDb() - .withAffectedRowsCount(affectedCount); - - span.captureException(t) - .deactivate() - .end(); } - } /** * Instruments: - *
    - *
  • {@link PreparedStatement#executeUpdate()}
  • - *
  • {@link PreparedStatement#executeLargeUpdate()} ()} (java8)
  • - *
+ *
    + *
  • {@link PreparedStatement#executeUpdate()}
  • + *
  • {@link PreparedStatement#executeLargeUpdate()} ()} (java8)
  • + *
*/ public static class ExecuteUpdateNoQueryInstrumentation extends StatementInstrumentation { - public ExecuteUpdateNoQueryInstrumentation(ElasticApmTracer tracer) { - super(tracer, + public ExecuteUpdateNoQueryInstrumentation() { + super( named("executeUpdate").or(named("executeLargeUpdate")) .and(takesArguments(0)) .and(isPublic()) @@ -322,39 +287,34 @@ public ExecuteUpdateNoQueryInstrumentation(ElasticApmTracer tracer) { @Nullable @Advice.OnMethodEnter(suppress = Throwable.class) - @SuppressWarnings("DuplicatedCode") - public static Span onBeforeExecute(@Advice.This Statement statement) { - if (tracer == null || jdbcHelperManager == null) { - return null; + private static Span onBeforeExecute(@Advice.Origin Class clazz, @Advice.This Statement statement) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + return (Span) MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#createJdbcSpanLookupSql") + .invoke(null, statement, true); + } else { + return (Span) MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#createJdbcSpanLookupSql") + .invoke(statement, true); } - - JdbcHelper helperImpl = jdbcHelperManager.getForClassLoaderOfClass(Statement.class); - if (helperImpl == null) { - return null; - } - - String sql = helperImpl.retrieveSqlForStatement(statement); - return helperImpl.createJdbcSpan(sql, statement, tracer.getActive(), true); } @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) - public static void onAfterExecute(@Advice.Enter @Nullable Span span, - @Advice.Thrown @Nullable Throwable t, - @Advice.Return long returnValue /* bytebuddy converts int to long for us here ! */) { - if (span == null) { - return; + private static void onAfterExecute(@Advice.Origin Class clazz, + @Advice.Enter @Nullable Span span, + @Advice.Thrown @Nullable Throwable t, + @Advice.Return long returnValue /* bytebuddy converts int to long for us here ! */) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#onAfter") + .invoke(null, span, t, returnValue); + } else { + MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#onAfter") + .invoke(span, t, returnValue); } - - if (t == null) { - span.getContext() - .getDb() - .withAffectedRowsCount(returnValue); - } - - span.captureException(t) - .deactivate() - .end(); } + } /** @@ -365,8 +325,8 @@ public static void onAfterExecute(@Advice.Enter @Nullable Span span, * */ public static class ExecutePreparedStatementInstrumentation extends StatementInstrumentation { - public ExecutePreparedStatementInstrumentation(ElasticApmTracer tracer) { - super(tracer, + public ExecutePreparedStatementInstrumentation() { + super( named("execute").or(named("executeQuery")) .and(takesArguments(0)) .and(isPublic()) @@ -375,42 +335,33 @@ public ExecutePreparedStatementInstrumentation(ElasticApmTracer tracer) { @Nullable @Advice.OnMethodEnter(suppress = Throwable.class) - @SuppressWarnings("DuplicatedCode") - public static Span onBeforeExecute(@Advice.This Statement statement) { - if (tracer != null && jdbcHelperManager != null) { - JdbcHelper helperImpl = jdbcHelperManager.getForClassLoaderOfClass(Statement.class); - if (helperImpl != null) { - @Nullable String sql = helperImpl.retrieveSqlForStatement(statement); - return helperImpl.createJdbcSpan(sql, statement, tracer.getActive(), true); - } + private static Span onBeforeExecute(@Advice.Origin Class clazz, @Advice.This Statement statement) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + return (Span) MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#createJdbcSpanLookupSql") + .invoke(null, statement, true); + } else { + return (Span) MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#createJdbcSpanLookupSql") + .invoke(statement, true); } - return null; } @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) - public static void onAfterExecute(@Advice.This Statement statement, - @Advice.Enter @Nullable Span span, - @Advice.Thrown @Nullable Throwable t) { - - if (span == null) { - return; + private static void onAfterExecute(@Advice.Origin Class clazz, + @Advice.This Statement statement, + @Advice.Enter @Nullable Span span, + @Advice.Thrown @Nullable Throwable t) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#onAfterPreparedStatementExecuteQuery") + .invoke(null, statement, span, t); + } else { + MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#onAfterPreparedStatementExecuteQuery") + .invoke(statement, span, t); } - - if (t == null && jdbcHelperManager != null) { - JdbcHelper jdbcHelper = jdbcHelperManager.getForClassLoaderOfClass(Statement.class); - if (jdbcHelper != null) { - span.getContext() - .getDb() - // getUpdateCount javadoc indicates that this method should be called only once - // however in practice adding this extra call seem to not have noticeable side effects - .withAffectedRowsCount(jdbcHelper.safeGetUpdateCount(statement)); - } - } - - span.captureException(t) - .deactivate() - .end(); } - } + } diff --git a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/helper/AdviceHelperAdapter.java b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/helper/AdviceHelperAdapter.java new file mode 100644 index 0000000000..4e12746a5e --- /dev/null +++ b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/helper/AdviceHelperAdapter.java @@ -0,0 +1,79 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 - 2020 Elastic and contributors + * %% + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * #L% + */ +package co.elastic.apm.agent.jdbc.helper; + +import co.elastic.apm.agent.bci.ElasticApmInstrumentation; +import co.elastic.apm.agent.bci.RegisterMethodHandle; +import co.elastic.apm.agent.impl.transaction.Span; + +import javax.annotation.Nullable; +import java.sql.Statement; + +public class AdviceHelperAdapter { + + private static final JdbcHelper INSTANCE = new JdbcHelper(ElasticApmInstrumentation.tracer); + + @RegisterMethodHandle + public static void mapStatementToSql(Statement statement, String sql) { + INSTANCE.mapStatementToSql(statement, sql); + } + + @RegisterMethodHandle + public static void clearInternalStorage() { + INSTANCE.clearInternalStorage(); + } + + @Nullable + @RegisterMethodHandle + public static Span createJdbcSpanLookupSql(Statement statement, boolean preparedStatement) { + return INSTANCE.createJdbcSpanLookupSql(statement, preparedStatement); + } + + @Nullable + @RegisterMethodHandle + public static Span createJdbcSpan(@Nullable String sql, Statement statement, boolean preparedStatement) { + return INSTANCE.createJdbcSpan(sql, statement, preparedStatement); + } + + @RegisterMethodHandle + public static void onAfterExecuteQuery(Statement statement, @Nullable Span span, @Nullable Throwable t) { + INSTANCE.onAfterExecuteQuery(statement, span, t); + } + + @RegisterMethodHandle + public static void onAfterExecuteBatch(@Nullable Span span, Throwable t, @Nullable Object returnValue) { + INSTANCE.onAfterExecuteBatch(span, t, returnValue); + } + + @RegisterMethodHandle + public static void onAfterPreparedStatementExecuteQuery(Statement statement, @Nullable Span span, @Nullable Throwable t) { + INSTANCE.onAfterPreparedStatementExecuteQuery(statement, span, t); + } + + @RegisterMethodHandle + public static void onAfter(@Nullable Span span, @Nullable Throwable t, long affectedCount) { + INSTANCE.onAfter(span, t, affectedCount); + } +} diff --git a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/helper/JdbcHelper.java b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/helper/JdbcHelper.java index 42f798a965..8f37ed35f9 100644 --- a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/helper/JdbcHelper.java +++ b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/helper/JdbcHelper.java @@ -24,32 +24,75 @@ */ package co.elastic.apm.agent.jdbc.helper; +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.impl.context.Destination; +import co.elastic.apm.agent.impl.transaction.AbstractSpan; import co.elastic.apm.agent.impl.transaction.Span; import co.elastic.apm.agent.impl.transaction.TraceContextHolder; +import co.elastic.apm.agent.jdbc.signature.SignatureParser; import co.elastic.apm.agent.util.DataStructures; import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nullable; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.sql.Statement; -public abstract class JdbcHelper { - - private static final WeakConcurrentMap statementSqlMap = DataStructures.createWeakConcurrentMapWithCleanerThread(); +public class JdbcHelper { public static final String DB_SPAN_TYPE = "db"; public static final String DB_SPAN_ACTION = "query"; + private final ThreadLocal SIGNATURE_PARSER_THREAD_LOCAL = new ThreadLocal() { + @Override + protected SignatureParser initialValue() { + return new SignatureParser(); + } + }; + private final WeakConcurrentMap statementSqlMap = DataStructures.createWeakConcurrentMapWithCleanerThread(); + private final Logger logger = LoggerFactory.getLogger(JdbcHelper.class); + private final WeakConcurrentMap metaDataMap = DataStructures.createWeakConcurrentMapWithCleanerThread(); + private final WeakConcurrentMap, Boolean> updateCountSupported = new WeakConcurrentMap.WithInlinedExpunction, Boolean>(); + private final WeakConcurrentMap, Boolean> metadataSupported = new WeakConcurrentMap.WithInlinedExpunction, Boolean>(); + private final WeakConcurrentMap, Boolean> connectionSupported = new WeakConcurrentMap.WithInlinedExpunction, Boolean>(); + private final ElasticApmTracer tracer; + + public JdbcHelper(ElasticApmTracer tracer) { + this.tracer = tracer; + } + /** * Maps the provided sql to the provided Statement object * * @param statement javax.sql.Statement object * @param sql query string */ - public void mapStatementToSql(Object statement, String sql) { + public void mapStatementToSql(Statement statement, String sql) { statementSqlMap.putIfAbsent(statement, sql); } /** - * Returns the SQL statement belonging to provided Statement. + * Clears internal data storage, should only be used for testing + */ + public void clearInternalStorage() { + metaDataMap.clear(); + updateCountSupported.clear(); + metadataSupported.clear(); + connectionSupported.clear(); + } + + + @Nullable + public Span createJdbcSpanLookupSql(Statement statement, boolean preparedStatement) { + return createJdbcSpan(retrieveSqlForStatement(statement), statement, preparedStatement); + } + + + /** + * Looks up the SQL statement belonging to provided Statement. *

* Might return {@code null} when the provided Statement is a wrapper of the actual statement. *

@@ -57,17 +100,146 @@ public void mapStatementToSql(Object statement, String sql) { * @return the SQL statement belonging to provided Statement, or {@code null} */ @Nullable - public String retrieveSqlForStatement(Object statement) { + public String retrieveSqlForStatement(Statement statement) { return statementSqlMap.get(statement); } - /** - * Clears internal data storage, should only be used for testing + @Nullable + public Span createJdbcSpan(@Nullable String sql, Statement statement, boolean preparedStatement) { + TraceContextHolder parent = tracer.getActive(); + if (sql == null || isAlreadyMonitored(parent) || parent == null || !parent.isSampled()) { + return null; + } + + Span span = parent.createSpan().activate(); + if (sql.isEmpty()) { + span.withName("empty query"); + } else { + StringBuilder spanName = span.getAndOverrideName(AbstractSpan.PRIO_DEFAULT); + if (spanName != null) { + SIGNATURE_PARSER_THREAD_LOCAL.get().querySignature(sql, spanName, preparedStatement); + } + } + // setting the type here is important + // getting the meta data can result in another jdbc call + // if that is traced as well -> StackOverflowError + // to work around that, isAlreadyMonitored checks if the parent span is a db span and ignores them + span.withType(DB_SPAN_TYPE); + + // write fields that do not rely on metadata + span.getContext().getDb() + .withStatement(sql.isEmpty() ? "(empty query)" : sql) + .withType("sql"); + + Connection connection = safeGetConnection(statement); + ConnectionMetaData connectionMetaData = connection == null ? null : getConnectionMetaData(connection); + if (connectionMetaData != null) { + span.withSubtype(connectionMetaData.getDbVendor()) + .withAction(DB_SPAN_ACTION); + span.getContext().getDb() + .withUser(connectionMetaData.getUser()); + Destination destination = span.getContext().getDestination() + .withAddress(connectionMetaData.getHost()) + .withPort(connectionMetaData.getPort()); + destination.getService() + .withName(connectionMetaData.getDbVendor()) + .withResource(connectionMetaData.getDbVendor()) + .withType(DB_SPAN_TYPE); + } + + return span; + } + + public void onAfterExecuteQuery(Statement statement, @Nullable Span span, @Nullable Throwable t) { + if (span == null) { + return; + } + onAfter(span, t, t == null ? safeGetUpdateCount(statement) : Long.MIN_VALUE); + } + + public void onAfterExecuteBatch(@Nullable Span span, Throwable t, @Nullable Object returnValue) { + if (span == null) { + return; + } + onAfter(span, t, getAffectedCountForBatch(returnValue)); + } + + public void onAfterPreparedStatementExecuteQuery(Statement statement, @Nullable Span span, @Nullable Throwable t) { + if (span == null) { + return; + } + onAfter(span, t, t == null ? safeGetUpdateCount(statement) : Long.MIN_VALUE); + } + + public void onAfter(@Nullable Span span, @Nullable Throwable t, long affectedCount) { + if (span == null) { + return; + } + if (t == null && affectedCount != Long.MIN_VALUE) { + span.getContext() + .getDb() + .withAffectedRowsCount(affectedCount); + } + + span.captureException(t) + .deactivate() + .end(); + } + + private long getAffectedCountForBatch(@Nullable Object returnValue) { + // for 'executeBatch' and 'executeLargeBatch', we have to compute the sum as Statement.getUpdateCount() + // does not seem to return the sum of all elements. As we can use instanceof to check return type + // we do not need to use a separate advice. 'execute' return value is auto-boxed into a Boolean, + // but there is no extra allocation. + long affectedCount = 0; + if (returnValue instanceof int[]) { + int[] array = (int[]) returnValue; + for (int i = 0; i < array.length; i++) { + affectedCount += array[i]; + } + } else if (returnValue instanceof long[]) { + long[] array = (long[]) returnValue; + for (int i = 0; i < array.length; i++) { + affectedCount += array[i]; + } + } + return affectedCount; + } + + /* + * This makes sure that even when there are wrappers for the statement, + * we only record each JDBC call once. */ - public abstract void clearInternalStorage(); + private boolean isAlreadyMonitored(@Nullable TraceContextHolder parent) { + if (!(parent instanceof Span)) { + return false; + } + Span parentSpan = (Span) parent; + // a db span can't be the child of another db span + // this means the span has already been created for this db call + return parentSpan.getType() != null && parentSpan.getType().equals(DB_SPAN_TYPE); + } @Nullable - public abstract Span createJdbcSpan(@Nullable String sql, Object statement, @Nullable TraceContextHolder parent, boolean preparedStatement); + private Connection safeGetConnection(Statement statement) { + Connection connection = null; + Class type = statement.getClass(); + Boolean supported = isSupported(connectionSupported, type); + if (supported == Boolean.FALSE) { + return null; + } + + try { + connection = statement.getConnection(); + if (supported == null) { + markSupported(connectionSupported, type); + } + } catch (SQLException e) { + markNotSupported(connectionSupported, type, e); + } + + return connection; + } /** * Safely wraps calls to {@link java.sql.Statement#getUpdateCount()} @@ -75,7 +247,76 @@ public String retrieveSqlForStatement(Object statement) { * @param statement {@code java.sql.Statement} instance * @return {@link Long#MIN_VALUE} if statement does not support this feature, returned value otherwise */ - public abstract long safeGetUpdateCount(Object statement); + public long safeGetUpdateCount(Object statement) { + long result = Long.MIN_VALUE; + if (!(statement instanceof Statement)) { + return result; + } + + Class type = statement.getClass(); + Boolean supported = isSupported(updateCountSupported, type); + if (supported == Boolean.FALSE) { + return result; + } + + try { + // getUpdateCount javadoc indicates that this method should be called only once + // however in practice adding this extra call seem to not have noticeable side effects + result = ((Statement) statement).getUpdateCount(); + if (supported == null) { + markSupported(updateCountSupported, type); + } + } catch (SQLException e) { + markNotSupported(updateCountSupported, type, e); + } + + return result; + } + + @Nullable + private Boolean isSupported(WeakConcurrentMap, Boolean> featureMap, Class type) { + return featureMap.get(type); + } + + private void markSupported(WeakConcurrentMap, Boolean> map, Class type) { + map.put(type, Boolean.TRUE); + } + + private void markNotSupported(WeakConcurrentMap, Boolean> map, Class type, SQLException e) { + Boolean previous = map.put(type, Boolean.FALSE); + if (previous == null) { + logger.warn("JDBC feature not supported on class " + type, e); + } + } + + @Nullable + private ConnectionMetaData getConnectionMetaData(Connection connection) { + ConnectionMetaData connectionMetaData = metaDataMap.get(connection); + if (connectionMetaData != null) { + return connectionMetaData; + } + + Class type = connection.getClass(); + Boolean supported = isSupported(metadataSupported, type); + if (supported == Boolean.FALSE) { + return null; + } + + try { + DatabaseMetaData metaData = connection.getMetaData(); + connectionMetaData = ConnectionMetaData.create(metaData.getURL(), metaData.getUserName()); + if (supported == null) { + markSupported(metadataSupported, type); + } + } catch (SQLException e) { + markNotSupported(metadataSupported, type, e); + } + + if (connectionMetaData != null) { + metaDataMap.put(connection, connectionMetaData); + } + return connectionMetaData; + } } diff --git a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/helper/JdbcHelperImpl.java b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/helper/JdbcHelperImpl.java deleted file mode 100644 index 783d88c184..0000000000 --- a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/agent/jdbc/helper/JdbcHelperImpl.java +++ /dev/null @@ -1,228 +0,0 @@ -/*- - * #%L - * Elastic APM Java agent - * %% - * Copyright (C) 2018 - 2020 Elastic and contributors - * %% - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * #L% - */ -package co.elastic.apm.agent.jdbc.helper; - -import co.elastic.apm.agent.bci.VisibleForAdvice; -import co.elastic.apm.agent.impl.context.Destination; -import co.elastic.apm.agent.impl.transaction.AbstractSpan; -import co.elastic.apm.agent.impl.transaction.Span; -import co.elastic.apm.agent.impl.transaction.TraceContextHolder; -import co.elastic.apm.agent.jdbc.signature.SignatureParser; -import co.elastic.apm.agent.util.DataStructures; -import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.Nullable; -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.SQLException; -import java.sql.Statement; - -@SuppressWarnings("unused") // indirect access to this class provided through HelperClassManager -public class JdbcHelperImpl extends JdbcHelper { - - private static final Logger logger = LoggerFactory.getLogger(JdbcHelperImpl.class); - - // Important implementation note: - // - // because this class is potentially loaded from multiple classloaders, making those fields 'static' will not - // have the expected behavior, thus, any direct reference to `JdbcHelperImpl` should only be obtained from the - // HelperClassManager instance. - private final WeakConcurrentMap metaDataMap = DataStructures.createWeakConcurrentMapWithCleanerThread(); - private final WeakConcurrentMap, Boolean> updateCountSupported = new WeakConcurrentMap.WithInlinedExpunction, Boolean>(); - private final WeakConcurrentMap, Boolean> metadataSupported = new WeakConcurrentMap.WithInlinedExpunction, Boolean>(); - private final WeakConcurrentMap, Boolean> connectionSupported = new WeakConcurrentMap.WithInlinedExpunction, Boolean>(); - - @VisibleForAdvice - public final ThreadLocal SIGNATURE_PARSER_THREAD_LOCAL = new ThreadLocal() { - @Override - protected SignatureParser initialValue() { - return new SignatureParser(); - } - }; - - @Override - public void clearInternalStorage() { - metaDataMap.clear(); - updateCountSupported.clear(); - metadataSupported.clear(); - connectionSupported.clear(); - } - - @Override - @Nullable - public Span createJdbcSpan(@Nullable String sql, Object statement, @Nullable TraceContextHolder parent, boolean preparedStatement) { - if (!(statement instanceof Statement) || sql == null || isAlreadyMonitored(parent) || parent == null || !parent.isSampled()) { - return null; - } - - Span span = parent.createSpan().activate(); - if (sql.isEmpty()) { - span.withName("empty query"); - } else { - StringBuilder spanName = span.getAndOverrideName(AbstractSpan.PRIO_DEFAULT); - if (spanName != null) { - SIGNATURE_PARSER_THREAD_LOCAL.get().querySignature(sql, spanName, preparedStatement); - } - } - // setting the type here is important - // getting the meta data can result in another jdbc call - // if that is traced as well -> StackOverflowError - // to work around that, isAlreadyMonitored checks if the parent span is a db span and ignores them - span.withType(DB_SPAN_TYPE); - - // write fields that do not rely on metadata - span.getContext().getDb() - .withStatement(sql.isEmpty() ? "(empty query)" : sql) - .withType("sql"); - - Connection connection = safeGetConnection((Statement) statement); - ConnectionMetaData connectionMetaData = connection == null ? null : getConnectionMetaData(connection); - if (connectionMetaData != null) { - span.withSubtype(connectionMetaData.getDbVendor()) - .withAction(DB_SPAN_ACTION); - span.getContext().getDb() - .withUser(connectionMetaData.getUser()); - Destination destination = span.getContext().getDestination() - .withAddress(connectionMetaData.getHost()) - .withPort(connectionMetaData.getPort()); - destination.getService() - .withName(connectionMetaData.getDbVendor()) - .withResource(connectionMetaData.getDbVendor()) - .withType(DB_SPAN_TYPE); - } - - return span; - } - - /* - * This makes sure that even when there are wrappers for the statement, - * we only record each JDBC call once. - */ - private boolean isAlreadyMonitored(@Nullable TraceContextHolder parent) { - if (!(parent instanceof Span)) { - return false; - } - Span parentSpan = (Span) parent; - // a db span can't be the child of another db span - // this means the span has already been created for this db call - return parentSpan.getType() != null && parentSpan.getType().equals(DB_SPAN_TYPE); - } - - @Nullable - private ConnectionMetaData getConnectionMetaData(Connection connection) { - ConnectionMetaData connectionMetaData = metaDataMap.get(connection); - if (connectionMetaData != null) { - return connectionMetaData; - } - - Class type = connection.getClass(); - Boolean supported = isSupported(metadataSupported, type); - if (supported == Boolean.FALSE) { - return null; - } - - try { - DatabaseMetaData metaData = connection.getMetaData(); - connectionMetaData = ConnectionMetaData.create(metaData.getURL(), metaData.getUserName()); - if (supported == null) { - markSupported(metadataSupported, type); - } - } catch (SQLException e) { - markNotSupported(metadataSupported, type, e); - } - - if (connectionMetaData != null) { - metaDataMap.put(connection, connectionMetaData); - } - return connectionMetaData; - } - - @Nullable - private Connection safeGetConnection(Statement statement) { - Connection connection = null; - Class type = statement.getClass(); - Boolean supported = isSupported(connectionSupported, type); - if (supported == Boolean.FALSE) { - return null; - } - - try { - connection = statement.getConnection(); - if (supported == null) { - markSupported(connectionSupported, type); - } - } catch (SQLException e) { - markNotSupported(connectionSupported, type, e); - } - - return connection; - } - - - @Override - public long safeGetUpdateCount(Object statement) { - long result = Long.MIN_VALUE; - if (!(statement instanceof Statement)) { - return result; - } - - Class type = statement.getClass(); - Boolean supported = isSupported(updateCountSupported, type); - if (supported == Boolean.FALSE) { - return result; - } - - try { - result = ((Statement) statement).getUpdateCount(); - if (supported == null) { - markSupported(updateCountSupported, type); - } - } catch (SQLException e) { - markNotSupported(updateCountSupported, type, e); - } - - return result; - } - - @Nullable - private static Boolean isSupported(WeakConcurrentMap, Boolean> featureMap, Class type) { - return featureMap.get(type); - } - - private static void markSupported(WeakConcurrentMap, Boolean> map, Class type) { - map.put(type, Boolean.TRUE); - } - - private static void markNotSupported(WeakConcurrentMap, Boolean> map, Class type, SQLException e) { - Boolean previous = map.put(type, Boolean.FALSE); - if (previous == null) { - logger.warn("JDBC feature not supported on class " + type, e); - } - } - - -} diff --git a/apm-agent-plugins/apm-jdbc-plugin/src/test/java/co/elastic/apm/agent/jdbc/AbstractJdbcInstrumentationTest.java b/apm-agent-plugins/apm-jdbc-plugin/src/test/java/co/elastic/apm/agent/jdbc/AbstractJdbcInstrumentationTest.java index b3390ec7b9..06db2f42bc 100644 --- a/apm-agent-plugins/apm-jdbc-plugin/src/test/java/co/elastic/apm/agent/jdbc/AbstractJdbcInstrumentationTest.java +++ b/apm-agent-plugins/apm-jdbc-plugin/src/test/java/co/elastic/apm/agent/jdbc/AbstractJdbcInstrumentationTest.java @@ -25,11 +25,11 @@ package co.elastic.apm.agent.jdbc; import co.elastic.apm.agent.AbstractInstrumentationTest; +import co.elastic.apm.agent.bootstrap.MethodHandleDispatcher; import co.elastic.apm.agent.impl.context.Db; import co.elastic.apm.agent.impl.context.Destination; import co.elastic.apm.agent.impl.transaction.Span; import co.elastic.apm.agent.impl.transaction.Transaction; -import co.elastic.apm.agent.jdbc.helper.JdbcHelper; import co.elastic.apm.agent.jdbc.signature.SignatureParser; import org.junit.After; import org.junit.Before; @@ -161,11 +161,12 @@ private void executeTest(JdbcTask task) throws SQLException { // clear internal jdbc helper required due to metadata caching and global state about unsupported // JDBC driver features (based on classes instances) - if (JdbcInstrumentation.jdbcHelperManager != null) { - JdbcHelper jdbcHelper = JdbcInstrumentation.jdbcHelperManager.getForClassLoaderOfClass(Statement.class); - if (jdbcHelper != null) { - jdbcHelper.clearInternalStorage(); - } + try { + MethodHandleDispatcher + .getMethodHandle(getClass(), "co.elastic.apm.agent.jdbc.helper.AdviceHelperAdapter#clearInternalStorage") + .invoke(); + } catch (Throwable throwable) { + throwable.printStackTrace(); } } } diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/AbstractServletInstrumentation.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/AbstractServletInstrumentation.java index 01deca5a0c..301d7d3c29 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/AbstractServletInstrumentation.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/AbstractServletInstrumentation.java @@ -26,10 +26,12 @@ import co.elastic.apm.agent.bci.ElasticApmInstrumentation; import co.elastic.apm.agent.bci.bytebuddy.CustomElementMatchers; +import co.elastic.apm.agent.util.PackageScanner; import net.bytebuddy.matcher.ElementMatcher; import java.util.Collection; import java.util.Collections; +import java.util.List; import static co.elastic.apm.agent.servlet.ServletInstrumentation.SERVLET_API; @@ -40,6 +42,11 @@ public Collection getInstrumentationGroupNames() { return Collections.singleton(SERVLET_API); } + @Override + public List helpers() throws Exception { + return PackageScanner.getClassNames(getClass().getPackage().getName()); + } + @Override public final ElementMatcher.Junction getClassLoaderMatcher() { // this class has been introduced in servlet spec 3.0 diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/AsyncInstrumentation.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/AsyncInstrumentation.java index 2261d60c30..847eac15e9 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/AsyncInstrumentation.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/AsyncInstrumentation.java @@ -24,10 +24,10 @@ */ package co.elastic.apm.agent.servlet; -import co.elastic.apm.agent.bci.HelperClassManager; -import co.elastic.apm.agent.bci.VisibleForAdvice; -import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.bci.RegisterMethodHandle; +import co.elastic.apm.agent.bootstrap.MethodHandleDispatcher; import co.elastic.apm.agent.impl.transaction.Transaction; +import co.elastic.apm.agent.servlet.helper.AsyncContextAdviceHelper; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.NamedElement; import net.bytebuddy.description.method.MethodDescription; @@ -51,48 +51,28 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import static net.bytebuddy.matcher.ElementMatchers.takesArguments; -/** - * Only the methods annotated with {@link Advice.OnMethodEnter} and {@link Advice.OnMethodExit} may contain references to - * {@code javax.servlet}, as these are inlined into the matching methods. - * The agent itself does not have access to the Servlet API classes, as they are loaded by a child class loader. - * See https://github.com/raphw/byte-buddy/issues/465 for more information. - * However, the helper class {@link AsyncContextAdviceHelper} has access to the Servlet API, - * as it is loaded by the child classloader of {@link AsyncContext} - * (see {@link StartAsyncInstrumentation.StartAsyncAdvice#onExitStartAsync(AsyncContext)} - * and {@link AsyncContextInstrumentation.AsyncContextStartAdvice#onEnterAsyncContextStart(Runnable)}). - */ public abstract class AsyncInstrumentation extends AbstractServletInstrumentation { private static final String SERVLET_API_ASYNC_GROUP_NAME = "servlet-api-async"; - @Nullable - @VisibleForAdvice - // referring to AsyncContext is legal because of type erasure - public static HelperClassManager> asyncHelperManager; - - public AsyncInstrumentation(ElasticApmTracer tracer) { - synchronized (AsyncInstrumentation.class) { - if (asyncHelperManager == null) { - asyncHelperManager = HelperClassManager.ForSingleClassLoader.of(tracer, - "co.elastic.apm.agent.servlet.helper.AsyncContextAdviceHelperImpl", - "co.elastic.apm.agent.servlet.helper.AsyncContextAdviceHelperImpl$ApmAsyncListenerAllocator", - "co.elastic.apm.agent.servlet.helper.ApmAsyncListener"); - } - - } - } @Override public Collection getInstrumentationGroupNames() { return Arrays.asList(ServletInstrumentation.SERVLET_API, SERVLET_API_ASYNC_GROUP_NAME); } - public interface AsyncContextAdviceHelper { - void onExitStartAsync(T asyncContext); - } - public static class StartAsyncInstrumentation extends AsyncInstrumentation { - public StartAsyncInstrumentation(ElasticApmTracer tracer) { - super(tracer); + + @Advice.OnMethodExit(suppress = Throwable.class) + private static void onExitStartAsync(@Advice.Origin Class clazz, @Advice.Return AsyncContext asyncContext) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.servlet.AsyncInstrumentation$StartAsyncInstrumentation$StartAsyncAdvice#onExitStartAsync") + .invoke(null, asyncContext); + } else { + MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.servlet.AsyncInstrumentation$StartAsyncInstrumentation$StartAsyncAdvice#onExitStartAsync") + .invoke(asyncContext); + } } @Override @@ -128,29 +108,43 @@ public ElementMatcher getMethodMatcher() { ); } - @Override - public Class getAdviceClass() { - return StartAsyncAdvice.class; - } - - @VisibleForAdvice public static class StartAsyncAdvice { - @Advice.OnMethodExit(suppress = Throwable.class) - private static void onExitStartAsync(@Advice.Return AsyncContext asyncContext) { - if (tracer != null && asyncHelperManager != null) { - AsyncContextAdviceHelper helperImpl = asyncHelperManager.getForClassLoaderOfClass(AsyncContext.class); - if (helperImpl != null) { - helperImpl.onExitStartAsync(asyncContext); - } - } + private static final AsyncContextAdviceHelper helper = new AsyncContextAdviceHelper(tracer); + + @RegisterMethodHandle + public static void onExitStartAsync(AsyncContext asyncContext) { + helper.onExitStartAsync(asyncContext); } } } public static class AsyncContextInstrumentation extends AsyncInstrumentation { - public AsyncContextInstrumentation(ElasticApmTracer tracer) { - super(tracer); + + @Advice.OnMethodEnter(suppress = Throwable.class) + private static void onEnterAsyncContextStart(@Advice.Origin Class clazz, @Advice.Argument(value = 0, readOnly = false) @Nullable Runnable runnable) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + runnable = (Runnable) MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.servlet.AsyncInstrumentation$AsyncContextInstrumentation$AsyncContextStartAdvice#onEnterAsyncContextStart") + .invoke(null, runnable); + } else { + runnable = (Runnable) MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.servlet.AsyncInstrumentation$AsyncContextInstrumentation$AsyncContextStartAdvice#onEnterAsyncContextStart") + .invoke(runnable); + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Exception.class) + private static void onExitAsyncContextStart(@Advice.Origin Class clazz) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.servlet.AsyncInstrumentation$AsyncContextInstrumentation$AsyncContextStartAdvice#onExitAsyncContextStart") + .invoke(null); + } else { + MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.servlet.AsyncInstrumentation$AsyncContextInstrumentation$AsyncContextStartAdvice#onExitAsyncContextStart") + .invoke(); + } } @Override @@ -171,16 +165,11 @@ public ElementMatcher getMethodMatcher() { .and(takesArguments(Runnable.class)); } - @Override - public Class getAdviceClass() { - return AsyncContextStartAdvice.class; - } - - @VisibleForAdvice public static class AsyncContextStartAdvice { - @Advice.OnMethodEnter(suppress = Throwable.class) - private static void onEnterAsyncContextStart(@Advice.Argument(value = 0, readOnly = false) @Nullable Runnable runnable) { + @Nullable + @RegisterMethodHandle + public static Runnable onEnterAsyncContextStart(@Nullable Runnable runnable) { if (tracer != null && runnable != null && tracer.isWrappingAllowedOnThread()) { final Transaction transaction = tracer.currentTransaction(); if (transaction != null) { @@ -188,10 +177,11 @@ private static void onEnterAsyncContextStart(@Advice.Argument(value = 0, readOnl tracer.avoidWrappingOnThread(); } } + return runnable; } - @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Exception.class) - private static void onExitAsyncContextStart() { + @RegisterMethodHandle + public static void onExitAsyncContextStart() { if (tracer != null) { tracer.allowWrappingOnThread(); } diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/FilterChainInstrumentation.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/FilterChainInstrumentation.java index 46afd92cef..e2b733894e 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/FilterChainInstrumentation.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/FilterChainInstrumentation.java @@ -24,12 +24,18 @@ */ package co.elastic.apm.agent.servlet; -import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.bootstrap.MethodHandleDispatcher; +import co.elastic.apm.agent.impl.transaction.Transaction; +import net.bytebuddy.asm.Advice; import net.bytebuddy.description.NamedElement; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; +import javax.annotation.Nullable; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; import static net.bytebuddy.matcher.ElementMatchers.isInterface; import static net.bytebuddy.matcher.ElementMatchers.nameContains; @@ -42,10 +48,6 @@ */ public class FilterChainInstrumentation extends AbstractServletInstrumentation { - public FilterChainInstrumentation(ElasticApmTracer tracer) { - ServletApiAdvice.init(tracer); - } - @Override public ElementMatcher getTypeMatcherPreFilter() { return nameContains("Chain"); @@ -64,9 +66,37 @@ public ElementMatcher getMethodMatcher() { .and(takesArgument(1, named("javax.servlet.ServletResponse"))); } - @Override - public Class getAdviceClass() { - return ServletApiAdvice.class; + @Nullable + @Advice.OnMethodEnter(suppress = Throwable.class) + private static Transaction onEnterServletService(@Advice.Origin Class clazz, + @Advice.Argument(0) ServletRequest servletRequest) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + return (Transaction) MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.servlet.ServletApiAdvice#onEnterServletService") + .invoke(null, servletRequest); + } else { + return (Transaction) MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.servlet.ServletApiAdvice#onEnterServletService") + .invoke(servletRequest); + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + private static void onExitServletService(@Advice.Origin Class clazz, + @Advice.Argument(0) ServletRequest servletRequest, + @Advice.Argument(1) ServletResponse servletResponse, + @Advice.Enter @Nullable Transaction transaction, + @Advice.Thrown @Nullable Throwable t, + @Advice.This Object thiz) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.servlet.ServletApiAdvice#onExitServletService") + .invoke(null, servletRequest, servletResponse, transaction, t, thiz); + } else { + MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.servlet.ServletApiAdvice#onExitServletService") + .invoke(servletRequest, servletResponse, transaction, t, thiz); + } } } diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/RequestStreamRecordingInstrumentation.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/RequestStreamRecordingInstrumentation.java index e20846c0b6..a3325ca8b1 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/RequestStreamRecordingInstrumentation.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/RequestStreamRecordingInstrumentation.java @@ -24,18 +24,17 @@ */ package co.elastic.apm.agent.servlet; -import co.elastic.apm.agent.bci.HelperClassManager; -import co.elastic.apm.agent.bci.VisibleForAdvice; -import co.elastic.apm.agent.impl.ElasticApmTracer; -import co.elastic.apm.agent.impl.context.Request; +import co.elastic.apm.agent.bci.RegisterMethodHandle; +import co.elastic.apm.agent.bootstrap.MethodHandleDispatcher; import co.elastic.apm.agent.impl.transaction.Transaction; +import co.elastic.apm.agent.servlet.helper.RecordingServletInputStreamWrapper; +import co.elastic.apm.agent.util.CallDepth; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.NamedElement; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; -import javax.annotation.Nullable; import javax.servlet.ServletInputStream; import java.util.Arrays; import java.util.Collection; @@ -50,19 +49,29 @@ public class RequestStreamRecordingInstrumentation extends AbstractServletInstrumentation { - @Nullable - @VisibleForAdvice - // referring to InputStreamWrapperFactory is legal because of type erasure - public static HelperClassManager wrapperHelperClassManager; + @Advice.OnMethodEnter(suppress = Throwable.class) + private static void onReadEnter(@Advice.Origin Class clazz) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.servlet.RequestStreamRecordingInstrumentation$GetInputStreamAdvice#onReadEnter") + .invoke(null); + } else { + MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.servlet.RequestStreamRecordingInstrumentation$GetInputStreamAdvice#onReadEnter") + .invoke(); + } + } - public RequestStreamRecordingInstrumentation(ElasticApmTracer tracer) { - ServletApiAdvice.init(tracer); - synchronized (RequestStreamRecordingInstrumentation.class) { - if (wrapperHelperClassManager == null) { - wrapperHelperClassManager = HelperClassManager.ForSingleClassLoader.of(tracer, - "co.elastic.apm.agent.servlet.helper.InputStreamFactoryHelperImpl", - "co.elastic.apm.agent.servlet.helper.RecordingServletInputStreamWrapper"); - } + @Advice.OnMethodExit(suppress = Throwable.class) + private static void afterGetInputStream(@Advice.Origin Class clazz, @Advice.Return(readOnly = false) ServletInputStream inputStream) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + inputStream = (ServletInputStream) MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.servlet.RequestStreamRecordingInstrumentation$GetInputStreamAdvice#afterGetInputStream") + .invoke(null, inputStream); + } else { + inputStream = (ServletInputStream) MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.servlet.RequestStreamRecordingInstrumentation$GetInputStreamAdvice#afterGetInputStream") + .invoke(inputStream); } } @@ -86,48 +95,25 @@ public Collection getInstrumentationGroupNames() { return Arrays.asList(SERVLET_API, "servlet-input-stream"); } - @Override - public Class getAdviceClass() { - return GetInputStreamAdvice.class; - } - - public interface InputStreamWrapperFactory { - ServletInputStream wrap(Request request, ServletInputStream servletInputStream); - } - public static class GetInputStreamAdvice { - @VisibleForAdvice - public static final ThreadLocal nestedThreadLocal = new ThreadLocal() { - @Override - protected Boolean initialValue() { - return Boolean.FALSE; - } - }; - - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onReadEnter(@Advice.This Object thiz, - @Advice.Local("transaction") Transaction transaction, - @Advice.Local("nested") boolean nested) { - nested = nestedThreadLocal.get(); - nestedThreadLocal.set(Boolean.TRUE); + @RegisterMethodHandle + public static void onReadEnter() { + CallDepth.increment(GetInputStreamAdvice.class); } - @Advice.OnMethodExit(suppress = Throwable.class) - public static void afterGetInputStream(@Advice.Return(readOnly = false) ServletInputStream inputStream, - @Advice.Local("nested") boolean nested) { - if (nested || tracer == null || wrapperHelperClassManager == null) { - return; + @RegisterMethodHandle + public static ServletInputStream afterGetInputStream(ServletInputStream inputStream) { + int callDepth = CallDepth.decrement(GetInputStreamAdvice.class); + if (callDepth > 0 || tracer == null) { + return inputStream; } - try { - final Transaction transaction = tracer.currentTransaction(); - // only wrap if the body buffer has been initialized via ServletTransactionHelper.startCaptureBody - if (transaction != null && transaction.getContext().getRequest().getBodyBuffer() != null) { - inputStream = wrapperHelperClassManager.getForClassLoaderOfClass(inputStream.getClass()).wrap(transaction.getContext().getRequest(), inputStream); - } - } finally { - nestedThreadLocal.set(Boolean.FALSE); + final Transaction transaction = tracer.currentTransaction(); + // only wrap if the body buffer has been initialized via ServletTransactionHelper.startCaptureBody + if (transaction != null && transaction.getContext().getRequest().getBodyBuffer() != null) { + inputStream = new RecordingServletInputStreamWrapper(transaction.getContext().getRequest(), inputStream); } + return inputStream; } } } diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletApiAdvice.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletApiAdvice.java index cc145d521a..188d4b8313 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletApiAdvice.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletApiAdvice.java @@ -24,17 +24,19 @@ */ package co.elastic.apm.agent.servlet; -import co.elastic.apm.agent.bci.VisibleForAdvice; +import co.elastic.apm.agent.bci.RegisterMethodHandle; import co.elastic.apm.agent.configuration.CoreConfiguration; -import co.elastic.apm.agent.impl.ElasticApmTracer; import co.elastic.apm.agent.impl.Scope; import co.elastic.apm.agent.impl.context.Request; import co.elastic.apm.agent.impl.context.Response; import co.elastic.apm.agent.impl.transaction.Transaction; -import net.bytebuddy.asm.Advice; +import co.elastic.apm.agent.servlet.helper.ServletTransactionCreationHelper; +import co.elastic.apm.agent.util.CallDepth; +import com.blogspot.mydailyjava.weaklockfree.DetachedThreadLocal; import javax.annotation.Nullable; import javax.servlet.DispatcherType; +import javax.servlet.Servlet; import javax.servlet.ServletContext; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; @@ -48,55 +50,44 @@ import java.util.List; import java.util.Map; +import static co.elastic.apm.agent.bci.ElasticApmInstrumentation.tracer; import static co.elastic.apm.agent.servlet.ServletTransactionHelper.TRANSACTION_ATTRIBUTE; import static co.elastic.apm.agent.servlet.ServletTransactionHelper.determineServiceName; -/** - * Only the methods annotated with {@link Advice.OnMethodEnter} and {@link Advice.OnMethodExit} may contain references to - * {@code javax.servlet}, as these are inlined into the matching methods. - * The agent itself does not have access to the Servlet API classes, as they are loaded by a child class loader. - * See https://github.com/raphw/byte-buddy/issues/465 for more information. - */ public class ServletApiAdvice { - @Nullable - @VisibleForAdvice - public static ServletTransactionHelper servletTransactionHelper; - - @Nullable - @VisibleForAdvice - public static ElasticApmTracer tracer; + private static final ServletTransactionHelper servletTransactionHelper; + private static final ServletTransactionCreationHelper servletTransactionCreationHelper; - @VisibleForAdvice - public static ThreadLocal excluded = new ThreadLocal() { + private static DetachedThreadLocal excluded = new DetachedThreadLocal(DetachedThreadLocal.Cleaner.INLINE) { @Override - protected Boolean initialValue() { + protected Boolean initialValue(Thread thread) { return Boolean.FALSE; } }; + private static DetachedThreadLocal scopeThreadLocal = new DetachedThreadLocal(DetachedThreadLocal.Cleaner.INLINE); - @VisibleForAdvice - public static final List requestExceptionAttributes = Arrays.asList("javax.servlet.error.exception", "exception", "org.springframework.web.servlet.DispatcherServlet.EXCEPTION", "co.elastic.apm.exception"); + private static final List requestExceptionAttributes = Arrays.asList("javax.servlet.error.exception", "exception", "org.springframework.web.servlet.DispatcherServlet.EXCEPTION", "co.elastic.apm.exception"); - static void init(ElasticApmTracer tracer) { - ServletApiAdvice.tracer = tracer; + static { servletTransactionHelper = new ServletTransactionHelper(tracer); + servletTransactionCreationHelper = new ServletTransactionCreationHelper(tracer); } - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onEnterServletService(@Advice.Argument(0) ServletRequest servletRequest, - @Advice.Local("transaction") Transaction transaction, - @Advice.Local("scope") Scope scope) { + @Nullable + @RegisterMethodHandle + public static Transaction onEnterServletService(ServletRequest servletRequest) { + int depth = CallDepth.increment(Servlet.class); if (tracer == null) { - return; + return null; } + Transaction transaction = null; // re-activate transactions for async requests final Transaction transactionAttr = (Transaction) servletRequest.getAttribute(TRANSACTION_ATTRIBUTE); - if (tracer.currentTransaction() == null && transactionAttr != null) { - scope = transactionAttr.activateInScope(); + if (depth == 0 && tracer.currentTransaction() == null && transactionAttr != null) { + scopeThreadLocal.set(transactionAttr.activateInScope()); } if (tracer.isRunning() && - servletTransactionHelper != null && servletRequest instanceof HttpServletRequest && servletRequest.getDispatcherType() == DispatcherType.REQUEST && !Boolean.TRUE.equals(excluded.get())) { @@ -108,18 +99,12 @@ public static void onEnterServletService(@Advice.Argument(0) ServletRequest serv } final HttpServletRequest request = (HttpServletRequest) servletRequest; - if (ServletInstrumentation.servletTransactionCreationHelperManager != null) { - ServletInstrumentation.ServletTransactionCreationHelper helper = - ServletInstrumentation.servletTransactionCreationHelperManager.getForClassLoaderOfClass(HttpServletRequest.class); - if (helper != null) { - transaction = helper.createAndActivateTransaction(request); - } - } + transaction = servletTransactionCreationHelper.createAndActivateTransaction(request); if (transaction == null) { // if the request is excluded, avoid matching all exclude patterns again on each filter invocation excluded.set(Boolean.TRUE); - return; + return null; } final Request req = transaction.getContext().getRequest(); if (transaction.isSampled() && tracer.getConfig(CoreConfiguration.class).isCaptureHeaders()) { @@ -141,20 +126,23 @@ public static void onEnterServletService(@Advice.Argument(0) ServletRequest serv request.getScheme(), request.getServerName(), request.getServerPort(), request.getRequestURI(), request.getQueryString(), request.getRemoteAddr(), request.getHeader("Content-Type")); } + return transaction; } - @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) - public static void onExitServletService(@Advice.Argument(0) ServletRequest servletRequest, - @Advice.Argument(1) ServletResponse servletResponse, - @Advice.Local("transaction") @Nullable Transaction transaction, - @Advice.Local("scope") @Nullable Scope scope, - @Advice.Thrown @Nullable Throwable t, - @Advice.This Object thiz) { + @RegisterMethodHandle + public static void onExitServletService(ServletRequest servletRequest, + ServletResponse servletResponse, + @Nullable Transaction transaction, + @Nullable Throwable t, + Object thiz) { + int depth = CallDepth.decrement(Servlet.class); if (tracer == null) { return; } excluded.set(Boolean.FALSE); - if (scope != null) { + Scope scope = scopeThreadLocal.get(); + if (depth == 0 && scope != null) { + scopeThreadLocal.clear(); scope.close(); } if (thiz instanceof HttpServlet && servletRequest instanceof HttpServletRequest) { @@ -166,8 +154,7 @@ public static void onExitServletService(@Advice.Argument(0) ServletRequest servl ServletTransactionHelper.setUsernameIfUnset(userPrincipal != null ? userPrincipal.getName() : null, currentTransaction.getContext()); } } - if (servletTransactionHelper != null && - transaction != null && + if (transaction != null && servletRequest instanceof HttpServletRequest && servletResponse instanceof HttpServletResponse) { diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletInstrumentation.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletInstrumentation.java index 960beb15f6..ce86db1aeb 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletInstrumentation.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletInstrumentation.java @@ -24,17 +24,17 @@ */ package co.elastic.apm.agent.servlet; -import co.elastic.apm.agent.bci.HelperClassManager; -import co.elastic.apm.agent.bci.VisibleForAdvice; -import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.bootstrap.MethodHandleDispatcher; import co.elastic.apm.agent.impl.transaction.Transaction; +import net.bytebuddy.asm.Advice; import net.bytebuddy.description.NamedElement; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; import javax.annotation.Nullable; -import javax.servlet.http.HttpServletRequest; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; import static net.bytebuddy.matcher.ElementMatchers.isInterface; @@ -57,21 +57,6 @@ public class ServletInstrumentation extends AbstractServletInstrumentation { static final String SERVLET_API = "servlet-api"; - @Nullable - @VisibleForAdvice - // referring to HttpServletRequest is legal because of type erasure - public static HelperClassManager> servletTransactionCreationHelperManager; - - public ServletInstrumentation(ElasticApmTracer tracer) { - ServletApiAdvice.init(tracer); - if (servletTransactionCreationHelperManager == null) { - servletTransactionCreationHelperManager = HelperClassManager.ForSingleClassLoader.of(tracer, - "co.elastic.apm.agent.servlet.helper.ServletTransactionCreationHelperImpl", - "co.elastic.apm.agent.servlet.helper.ServletRequestHeaderGetter" - ); - } - } - @Override public ElementMatcher getTypeMatcherPreFilter() { return nameContains("Servlet").or(nameContainsIgnoreCase("jsp")); @@ -90,14 +75,36 @@ public ElementMatcher getMethodMatcher() { .and(takesArgument(1, named("javax.servlet.ServletResponse"))); } - @Override - public Class getAdviceClass() { - return ServletApiAdvice.class; + @Nullable + @Advice.OnMethodEnter(suppress = Throwable.class) + private static Transaction onEnterServletService(@Advice.Origin Class clazz, + @Advice.Argument(0) ServletRequest servletRequest) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + return (Transaction) MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.servlet.ServletApiAdvice#onEnterServletService") + .invoke(null, servletRequest); + } else { + return (Transaction) MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.servlet.ServletApiAdvice#onEnterServletService") + .invoke(servletRequest); + } } - @VisibleForAdvice - public interface ServletTransactionCreationHelper { - @Nullable - Transaction createAndActivateTransaction(R request); + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + private static void onExitServletService(@Advice.Origin Class clazz, + @Advice.Argument(0) ServletRequest servletRequest, + @Advice.Argument(1) ServletResponse servletResponse, + @Advice.Enter @Nullable Transaction transaction, + @Advice.Thrown @Nullable Throwable t, + @Advice.This Object thiz) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.servlet.ServletApiAdvice#onExitServletService") + .invoke(null, servletRequest, servletResponse, transaction, t, thiz); + } else { + MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.servlet.ServletApiAdvice#onExitServletService") + .invoke(servletRequest, servletResponse, transaction, t, thiz); + } } } diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletTransactionHelper.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletTransactionHelper.java index 21bd241e3a..c79fc384b1 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletTransactionHelper.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletTransactionHelper.java @@ -25,7 +25,7 @@ package co.elastic.apm.agent.servlet; import co.elastic.apm.agent.bci.ElasticApmInstrumentation; -import co.elastic.apm.agent.bci.VisibleForAdvice; +import co.elastic.apm.agent.bci.RegisterMethodHandle; import co.elastic.apm.agent.configuration.CoreConfiguration; import co.elastic.apm.agent.impl.ElasticApmTracer; import co.elastic.apm.agent.impl.context.Request; @@ -47,21 +47,13 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import static co.elastic.apm.agent.configuration.CoreConfiguration.EventType.OFF; import static co.elastic.apm.agent.impl.transaction.AbstractSpan.PRIO_DEFAULT; import static co.elastic.apm.agent.impl.transaction.AbstractSpan.PRIO_LOW_LEVEL_FRAMEWORK; -import static co.elastic.apm.agent.configuration.CoreConfiguration.EventType.OFF; -/** - * This class must not import classes from {@code javax.servlet} due to class loader issues. - * See https://github.com/raphw/byte-buddy/issues/465 for more information. - */ -@VisibleForAdvice public class ServletTransactionHelper { - @VisibleForAdvice public static final String TRANSACTION_ATTRIBUTE = ServletApiAdvice.class.getName() + ".transaction"; - - @VisibleForAdvice public static final String ASYNC_ATTRIBUTE = ServletApiAdvice.class.getName() + ".async"; private static final String CONTENT_TYPE_FROM_URLENCODED = "application/x-www-form-urlencoded"; @@ -74,18 +66,17 @@ public class ServletTransactionHelper { private final CoreConfiguration coreConfiguration; private final WebConfiguration webConfiguration; - @VisibleForAdvice public ServletTransactionHelper(ElasticApmTracer tracer) { this.coreConfiguration = tracer.getConfig(CoreConfiguration.class); this.webConfiguration = tracer.getConfig(WebConfiguration.class); } - // visible for testing as clearing cache is required between tests execution - static void clearServiceNameCache() { + // needed for testing as clearing cache is required between tests execution + @RegisterMethodHandle + public static void clearServiceNameCache() { nameInitialized.clear(); } - @VisibleForAdvice public static void determineServiceName(@Nullable String servletContextName, ClassLoader servletContextClassLoader, @Nullable String contextPath) { if (ElasticApmInstrumentation.tracer == null || !nameInitialized.add(contextPath == null ? "null" : contextPath)) { return; @@ -109,8 +100,7 @@ public static void determineServiceName(@Nullable String servletContextName, Cla } } - @VisibleForAdvice - public void fillRequestContext(Transaction transaction, String protocol, String method, boolean secure, + void fillRequestContext(Transaction transaction, String protocol, String method, boolean secure, String scheme, String serverName, int serverPort, String requestURI, String queryString, String remoteAddr, @Nullable String contentTypeHeader) { @@ -144,15 +134,13 @@ private void startCaptureBody(Transaction transaction, String method, @Nullable } } - @VisibleForAdvice - public static void setUsernameIfUnset(@Nullable String userName, TransactionContext context) { + static void setUsernameIfUnset(@Nullable String userName, TransactionContext context) { // only set username if not manually set if (context.getUser().getUsername() == null) { context.getUser().withUsername(userName); } } - @VisibleForAdvice public void onAfter(Transaction transaction, @Nullable Throwable exception, boolean committed, int status, boolean overrideStatusCodeOnThrowable, String method, @Nullable Map parameterMap, String servletPath, @Nullable String pathInfo, @Nullable String contentTypeHeader, boolean deactivate) { @@ -175,7 +163,7 @@ public void onAfter(Transaction transaction, @Nullable Throwable exception, bool transaction.end(); } - private void doOnAfter(Transaction transaction, @Nullable Throwable exception, boolean committed, int status, + void doOnAfter(Transaction transaction, @Nullable Throwable exception, boolean committed, int status, boolean overrideStatusCodeOnThrowable, String method, @Nullable Map parameterMap, String servletPath, @Nullable String pathInfo, @Nullable String contentTypeHeader) { fillRequestParameters(transaction, method, parameterMap, contentTypeHeader); @@ -232,7 +220,6 @@ private void fillRequestParameters(Transaction transaction, String method, @Null } } - @VisibleForAdvice public boolean captureParameters(String method, @Nullable String contentTypeHeader) { return contentTypeHeader != null && contentTypeHeader.startsWith(CONTENT_TYPE_FROM_URLENCODED) @@ -316,8 +303,7 @@ private String getHttpVersion(String protocol) { } } - @VisibleForAdvice - public static void setTransactionNameByServletClass(@Nullable String method, @Nullable Class servletClass, Transaction transaction) { + static void setTransactionNameByServletClass(@Nullable String method, @Nullable Class servletClass, Transaction transaction) { if (servletClass == null) { return; } diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletVersionInstrumentation.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletVersionInstrumentation.java index 11e920502c..1439521352 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletVersionInstrumentation.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletVersionInstrumentation.java @@ -25,7 +25,8 @@ package co.elastic.apm.agent.servlet; import co.elastic.apm.agent.bci.ElasticApmInstrumentation; -import co.elastic.apm.agent.bci.VisibleForAdvice; +import co.elastic.apm.agent.bci.RegisterMethodHandle; +import co.elastic.apm.agent.bootstrap.MethodHandleDispatcher; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.NamedElement; import net.bytebuddy.description.method.MethodDescription; @@ -60,12 +61,6 @@ */ public abstract class ServletVersionInstrumentation extends ElasticApmInstrumentation { - @VisibleForAdvice - public static final Logger logger = LoggerFactory.getLogger(ServletVersionInstrumentation.class); - - @VisibleForAdvice - public static volatile boolean alreadyLogged = false; - @Override public ElementMatcher getTypeMatcherPreFilter() { return nameContains("Servlet").or(nameContainsIgnoreCase("jsp")); @@ -95,27 +90,15 @@ public ElementMatcher getMethodMatcher() { @Advice.OnMethodEnter(suppress = Throwable.class) @SuppressWarnings("Duplicates") // duplication is fine here as it allows to inline code - private static void onEnter(@Advice.Argument(0) @Nullable ServletConfig servletConfig) { - if (alreadyLogged) { - return; - } - alreadyLogged = true; - - int majorVersion = -1; - int minorVersion = -1; - String serverInfo = null; - if (servletConfig != null) { - ServletContext servletContext = servletConfig.getServletContext(); - if (null != servletContext) { - majorVersion = servletContext.getMajorVersion(); - minorVersion = servletContext.getMinorVersion(); - serverInfo = servletContext.getServerInfo(); - } - } - - logger.info("Servlet container info = {}", serverInfo); - if (majorVersion < 3) { - logger.warn("Unsupported servlet version detected: {}.{}, no Servlet transaction will be created", majorVersion, minorVersion); + private static void onEnter(@Advice.Origin Class clazz, @Advice.Argument(0) @Nullable ServletConfig servletConfig) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.servlet.ServletVersionInstrumentation$ServletVersionHelper#warnIfVersionNotSupportedServletConfig") + .invoke(null, servletConfig); + } else { + MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.servlet.ServletVersionInstrumentation$ServletVersionHelper#warnIfVersionNotSupportedServletConfig") + .invoke(servletConfig); } } } @@ -134,14 +117,37 @@ public ElementMatcher getMethodMatcher() { @Advice.OnMethodEnter(suppress = Throwable.class) @SuppressWarnings("Duplicates") // duplication is fine here as it allows to inline code - private static void onEnter(@Advice.This Servlet servlet) { + private static void onEnter(@Advice.Origin Class clazz, @Advice.This Servlet servlet) throws Throwable { + if (MethodHandleDispatcher.USE_REFLECTION) { + MethodHandleDispatcher + .getMethod(clazz, "co.elastic.apm.agent.servlet.ServletVersionInstrumentation$ServletVersionHelper#warnIfVersionNotSupportedServlet") + .invoke(null, servlet); + } else { + MethodHandleDispatcher + .getMethodHandle(clazz, "co.elastic.apm.agent.servlet.ServletVersionInstrumentation$ServletVersionHelper#warnIfVersionNotSupportedServlet") + .invoke(servlet); + } + } + } + + public static class ServletVersionHelper { + + private static final Logger logger = LoggerFactory.getLogger(ServletVersionInstrumentation.class); + + private static volatile boolean alreadyLogged = false; + + @RegisterMethodHandle + public static void warnIfVersionNotSupportedServlet(Servlet servlet) { + warnIfVersionNotSupportedServletConfig(servlet.getServletConfig()); + } + + @RegisterMethodHandle + public static void warnIfVersionNotSupportedServletConfig(@Nullable ServletConfig servletConfig) { if (alreadyLogged) { return; } alreadyLogged = true; - ServletConfig servletConfig = servlet.getServletConfig(); - int majorVersion = -1; int minorVersion = -1; String serverInfo = null; diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/ApmAsyncListener.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/ApmAsyncListener.java index 5c2a8c717b..323eb6fa3c 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/ApmAsyncListener.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/ApmAsyncListener.java @@ -60,16 +60,16 @@ public class ApmAsyncListener implements AsyncListener, Recyclable { private final AtomicBoolean completed = new AtomicBoolean(false); - private final AsyncContextAdviceHelperImpl asyncContextAdviceHelperImpl; + private final AsyncContextAdviceHelper asyncContextAdviceHelper; private final ServletTransactionHelper servletTransactionHelper; @Nullable private volatile Transaction transaction; @Nullable private volatile Throwable throwable; - ApmAsyncListener(AsyncContextAdviceHelperImpl asyncContextAdviceHelperImpl) { - this.asyncContextAdviceHelperImpl = asyncContextAdviceHelperImpl; - this.servletTransactionHelper = asyncContextAdviceHelperImpl.getServletTransactionHelper(); + ApmAsyncListener(AsyncContextAdviceHelper asyncContextAdviceHelper) { + this.asyncContextAdviceHelper = asyncContextAdviceHelper; + this.servletTransactionHelper = asyncContextAdviceHelper.getServletTransactionHelper(); } ApmAsyncListener withTransaction(Transaction transaction) { @@ -141,6 +141,7 @@ public void onStartAsync(AsyncEvent event) { // because only the onExitServletService method may contain references to the servlet API // (see class-level Javadoc) private void endTransaction(AsyncEvent event) { + Transaction transaction = this.transaction; // To ensure transaction is ended only by a single event if (completed.getAndSet(true) || transaction == null) { return; @@ -175,7 +176,7 @@ private void endTransaction(AsyncEvent event) { request.getServletPath(), request.getPathInfo(), contentTypeHeader, false ); } finally { - asyncContextAdviceHelperImpl.recycle(this); + asyncContextAdviceHelper.recycle(this); } } diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/AsyncContextAdviceHelperImpl.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/AsyncContextAdviceHelper.java similarity index 93% rename from apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/AsyncContextAdviceHelperImpl.java rename to apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/AsyncContextAdviceHelper.java index f3cc7926cf..7c62e11f56 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/AsyncContextAdviceHelperImpl.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/AsyncContextAdviceHelper.java @@ -11,9 +11,9 @@ * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @@ -29,7 +29,6 @@ import co.elastic.apm.agent.objectpool.Allocator; import co.elastic.apm.agent.objectpool.ObjectPool; import co.elastic.apm.agent.objectpool.impl.QueueBasedObjectPool; -import co.elastic.apm.agent.servlet.AsyncInstrumentation; import co.elastic.apm.agent.servlet.ServletApiAdvice; import co.elastic.apm.agent.servlet.ServletTransactionHelper; import org.jctools.queues.atomic.AtomicQueueFactory; @@ -41,7 +40,7 @@ import static co.elastic.apm.agent.servlet.ServletTransactionHelper.TRANSACTION_ATTRIBUTE; import static org.jctools.queues.spec.ConcurrentQueueSpec.createBoundedMpmc; -public class AsyncContextAdviceHelperImpl implements AsyncInstrumentation.AsyncContextAdviceHelper { +public class AsyncContextAdviceHelper { private static final String ASYNC_LISTENER_ADDED = ServletApiAdvice.class.getName() + ".asyncListenerAdded"; private static final int MAX_POOLED_ELEMENTS = 256; @@ -50,7 +49,7 @@ public class AsyncContextAdviceHelperImpl implements AsyncInstrumentation.AsyncC private final ServletTransactionHelper servletTransactionHelper; private final ElasticApmTracer tracer; - public AsyncContextAdviceHelperImpl(ElasticApmTracer tracer) { + public AsyncContextAdviceHelper(ElasticApmTracer tracer) { this.tracer = tracer; servletTransactionHelper = new ServletTransactionHelper(tracer); @@ -67,11 +66,10 @@ ServletTransactionHelper getServletTransactionHelper() { private final class ApmAsyncListenerAllocator implements Allocator { @Override public ApmAsyncListener createInstance() { - return new ApmAsyncListener(AsyncContextAdviceHelperImpl.this); + return new ApmAsyncListener(AsyncContextAdviceHelper.this); } } - @Override public void onExitStartAsync(AsyncContext asyncContext) { final ServletRequest request = asyncContext.getRequest(); if (request.getAttribute(ASYNC_LISTENER_ADDED) != null) { diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/RecordingServletInputStreamWrapper.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/RecordingServletInputStreamWrapper.java index 5ebb7cba2e..19e21f5b65 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/RecordingServletInputStreamWrapper.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/RecordingServletInputStreamWrapper.java @@ -11,9 +11,9 @@ * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @@ -24,7 +24,6 @@ */ package co.elastic.apm.agent.servlet.helper; -import co.elastic.apm.agent.bci.VisibleForAdvice; import co.elastic.apm.agent.impl.context.Request; import co.elastic.apm.agent.util.IOUtils; @@ -34,13 +33,11 @@ import java.nio.CharBuffer; import java.nio.charset.CoderResult; -@VisibleForAdvice public class RecordingServletInputStreamWrapper extends ServletInputStream { private final Request request; private final ServletInputStream servletInputStream; - @VisibleForAdvice public RecordingServletInputStreamWrapper(Request request, ServletInputStream servletInputStream) { this.request = request; this.servletInputStream = servletInputStream; diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/ServletTransactionCreationHelperImpl.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/ServletTransactionCreationHelper.java similarity index 91% rename from apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/ServletTransactionCreationHelperImpl.java rename to apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/ServletTransactionCreationHelper.java index f260be734c..f9198cc839 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/ServletTransactionCreationHelperImpl.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/ServletTransactionCreationHelper.java @@ -29,7 +29,6 @@ import co.elastic.apm.agent.impl.context.web.WebConfiguration; import co.elastic.apm.agent.impl.transaction.Transaction; import co.elastic.apm.agent.matcher.WildcardMatcher; -import co.elastic.apm.agent.servlet.ServletInstrumentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,21 +37,20 @@ import java.util.Objects; @SuppressWarnings("unused") -public class ServletTransactionCreationHelperImpl implements ServletInstrumentation.ServletTransactionCreationHelper { +public class ServletTransactionCreationHelper { - private static final Logger logger = LoggerFactory.getLogger(ServletTransactionCreationHelperImpl.class); + private static final Logger logger = LoggerFactory.getLogger(ServletTransactionCreationHelper.class); private final ElasticApmTracer tracer; private final CoreConfiguration coreConfiguration; private final WebConfiguration webConfiguration; - public ServletTransactionCreationHelperImpl(ElasticApmTracer tracer) { + public ServletTransactionCreationHelper(ElasticApmTracer tracer) { this.tracer = tracer; coreConfiguration = tracer.getConfig(CoreConfiguration.class); webConfiguration = tracer.getConfig(WebConfiguration.class); } - @Override @Nullable public Transaction createAndActivateTransaction(HttpServletRequest request) { Transaction transaction = null; diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/package-info.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/package-info.java index 9ca1f1ddae..37c5ddb961 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/package-info.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/helper/package-info.java @@ -11,9 +11,9 @@ * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @@ -22,46 +22,6 @@ * under the License. * #L% */ -/** - *

- * The classes in this package are loaded by a class loader which is a child of the class loader the Servlet API is loaded from. - * This works around the problem that the agent does not have access to the Servlet API. - * Instead of injecting the helper classes to the same class loader the Servlet API is loaded from, - * which will most likely not be possible anymore as of Java 11 - * (see http://jdk.java.net/11/release-notes#JDK-8193033), - * we create a class loader as the child of the one which loads the Servlet API. - *

- *

- * The classes loaded by this helper class loader can then access the Servlet API. - * However, the agent itself can't directly access the classes from the helper class loader, - * because they are loaded by a child class loader. - *

- *

- * This problem is circumvented by the agent providing an interface, - * which the helper class implements. - * The agent does then not need to know about the implementation type of the interface. - *

- *

- * One thing to be aware of is that the helper class loader needs to implement child first semantics when loading classes. - * Otherwise, it would load the helper implementation from the system or bootstrap classloader (where the agent is loaded from), - * without access to the Servlet API, - * instead of loading them itself so that the helper classes can access the Servlet API. - *

- *

- * Advices have to manually add their required helper classes to {@link co.elastic.apm.agent.bci.HelperClassManager}, - * which takes care of creating the helper class loaders. - *

- * - *
- *          System/Bootstrap CL (Agent)     provides interface AsyncContextAdviceHelper
- *                     |
- *                     v
- *             App CL (Servlet API)         uses AsyncContextAdviceHelperImpl from Helper CL
- *               /              \
- *              v               v
- * WebApp CL (user code/libs)   Helper CL   loads AsyncContextAdviceHelperImpl and ApmAsyncListener
- * 
- */ @NonnullApi package co.elastic.apm.agent.servlet.helper; diff --git a/apm-agent-plugins/apm-servlet-plugin/src/test/java/co/elastic/apm/agent/servlet/AbstractServletTest.java b/apm-agent-plugins/apm-servlet-plugin/src/test/java/co/elastic/apm/agent/servlet/AbstractServletTest.java index 18a6f3d05c..9ea1ace753 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/test/java/co/elastic/apm/agent/servlet/AbstractServletTest.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/test/java/co/elastic/apm/agent/servlet/AbstractServletTest.java @@ -25,6 +25,7 @@ package co.elastic.apm.agent.servlet; import co.elastic.apm.agent.AbstractInstrumentationTest; +import co.elastic.apm.agent.bootstrap.MethodHandleDispatcher; import okhttp3.OkHttpClient; import okhttp3.Response; import org.eclipse.jetty.server.NetworkConnector; @@ -49,12 +50,7 @@ abstract class AbstractServletTest extends AbstractInstrumentationTest { protected OkHttpClient httpClient; @BeforeEach - void initServerAndClient() throws Exception { - - // because we reuse the same classloader with different servlet context names - // we need to explicitly reset the name cache to make service name detection work as expected - ServletTransactionHelper.clearServiceNameCache(); - + void initServerAndClient() throws Throwable { // server is not reused between tests as handler is provided from subclass // another alternative server = new Server(); @@ -72,6 +68,11 @@ void initServerAndClient() throws Exception { .connectTimeout(10, TimeUnit.SECONDS) .retryOnConnectionFailure(false) .build(); + + // because we reuse the same classloader with different servlet context names + // we need to explicitly reset the name cache to make service name detection work as expected + // we can't call the static method directly because the actual class is within an isolated helper class loader + MethodHandleDispatcher.getMethodHandle(getClass(), "co.elastic.apm.agent.servlet.ServletTransactionHelper#clearServiceNameCache").invoke(); } @AfterEach diff --git a/apm-agent-plugins/apm-spring-webmvc-plugin/src/test/java/co/elastic/apm/agent/spring/webmvc/exception/AbstractExceptionHandlerInstrumentationTest.java b/apm-agent-plugins/apm-spring-webmvc-plugin/src/test/java/co/elastic/apm/agent/spring/webmvc/exception/AbstractExceptionHandlerInstrumentationTest.java index d050775cef..6d3f31d859 100644 --- a/apm-agent-plugins/apm-spring-webmvc-plugin/src/test/java/co/elastic/apm/agent/spring/webmvc/exception/AbstractExceptionHandlerInstrumentationTest.java +++ b/apm-agent-plugins/apm-spring-webmvc-plugin/src/test/java/co/elastic/apm/agent/spring/webmvc/exception/AbstractExceptionHandlerInstrumentationTest.java @@ -75,7 +75,7 @@ public static void setUpAll() { tracer = mockInstrumentationSetup.getTracer(); config = mockInstrumentationSetup.getConfig(); ElasticApmAgent.initInstrumentation(tracer, ByteBuddyAgent.install(), - Arrays.asList(new ServletInstrumentation(tracer), new ExceptionHandlerInstrumentation())); + Arrays.asList(new ServletInstrumentation(), new ExceptionHandlerInstrumentation())); } @AfterClass diff --git a/apm-agent-plugins/apm-spring-webmvc-plugin/src/test/java/co/elastic/apm/agent/spring/webmvc/template/AbstractViewRenderingInstrumentationTest.java b/apm-agent-plugins/apm-spring-webmvc-plugin/src/test/java/co/elastic/apm/agent/spring/webmvc/template/AbstractViewRenderingInstrumentationTest.java index dd7f56ef83..94fb6e0b2e 100644 --- a/apm-agent-plugins/apm-spring-webmvc-plugin/src/test/java/co/elastic/apm/agent/spring/webmvc/template/AbstractViewRenderingInstrumentationTest.java +++ b/apm-agent-plugins/apm-spring-webmvc-plugin/src/test/java/co/elastic/apm/agent/spring/webmvc/template/AbstractViewRenderingInstrumentationTest.java @@ -73,7 +73,7 @@ static void beforeAll() { reporter = mockInstrumentationSetup.getReporter(); config = mockInstrumentationSetup.getConfig(); tracer = mockInstrumentationSetup.getTracer(); - ElasticApmAgent.initInstrumentation(tracer, ByteBuddyAgent.install(), Arrays.asList(new ServletInstrumentation(tracer), new ViewRenderInstrumentation())); + ElasticApmAgent.initInstrumentation(tracer, ByteBuddyAgent.install(), Arrays.asList(new ServletInstrumentation(), new ViewRenderInstrumentation())); } @AfterAll diff --git a/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/TomcatIT.java b/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/TomcatIT.java index c1d025ee66..7a104cb317 100644 --- a/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/TomcatIT.java +++ b/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/TomcatIT.java @@ -11,9 +11,9 @@ * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY