package org.stagemonitor.core.instrument; import net.bytebuddy.ByteBuddy; import net.bytebuddy.agent.ByteBuddyAgent; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.dynamic.scaffold.MethodGraph; import net.bytebuddy.dynamic.scaffold.TypeValidation; import net.bytebuddy.matcher.ElementMatcher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.stagemonitor.core.CorePlugin; import org.stagemonitor.core.Stagemonitor; import org.stagemonitor.core.util.ClassUtils; import org.stagemonitor.util.IOUtils; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.ServiceLoader; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.jar.JarFile; import __redirected.org.stagemonitor.dispatcher.Dispatcher; import static net.bytebuddy.matcher.ElementMatchers.any; import static net.bytebuddy.matcher.ElementMatchers.isBootstrapClassLoader; import static net.bytebuddy.matcher.ElementMatchers.nameContains; import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; import static net.bytebuddy.matcher.ElementMatchers.not; import static org.stagemonitor.core.instrument.ClassLoaderNameMatcher.classLoaderWithName; import static org.stagemonitor.core.instrument.ClassLoaderNameMatcher.isReflectionClassLoader; import static org.stagemonitor.core.instrument.TimedElementMatcherDecorator.timed; /** * Attaches the {@link ByteBuddyAgent} at runtime and registers all {@link StagemonitorByteBuddyTransformer}s */ public class AgentAttacher { private static final Logger logger = LoggerFactory.getLogger(AgentAttacher.class); private static final String DISPATCHER_CLASS_NAME = "__redirected.org.stagemonitor.dispatcher.Dispatcher"; private static final String IGNORED_CLASSLOADERS_KEY = AgentAttacher.class.getName() + "hashCodesOfClassLoadersToIgnore"; private static final Runnable NOOP_ON_SHUTDOWN_ACTION = new Runnable() { public void run() { } }; private static CorePlugin corePlugin = Stagemonitor.getPlugin(CorePlugin.class); private static boolean runtimeAttached = false; private static Set<String> hashCodesOfClassLoadersToIgnore = Collections.emptySet(); private static Instrumentation instrumentation; private AgentAttacher() { } /** * Attaches the profiler and other instrumenters at runtime so that it is not necessary to add the * -javaagent command line argument. * * @return A runnable that should be called on shutdown to unregister this class file transformer */ public static synchronized Runnable performRuntimeAttachment() { if (runtimeAttached || !corePlugin.isStagemonitorActive() || !corePlugin.isAttachAgentAtRuntime()) { return NOOP_ON_SHUTDOWN_ACTION; } runtimeAttached = true; final List<ClassFileTransformer> classFileTransformers = new ArrayList<ClassFileTransformer>(); final AutoEvictingCachingBinaryLocator binaryLocator = new AutoEvictingCachingBinaryLocator(); if (initInstrumentation()) { final long start = System.currentTimeMillis(); classFileTransformers.add(initByteBuddyClassFileTransformer(binaryLocator)); if (corePlugin.isDebugInstrumentation()) { logger.info("Attached agents in {} ms", System.currentTimeMillis() - start); } TimedElementMatcherDecorator.logMetrics(); } return new Runnable() { public void run() { for (ClassFileTransformer classFileTransformer : classFileTransformers) { instrumentation.removeTransformer(classFileTransformer); } // This ClassLoader is shutting down so don't try to retransform classes of it in the future hashCodesOfClassLoadersToIgnore.add(ClassUtils.getIdentityString(AgentAttacher.class.getClassLoader())); binaryLocator.close(); } }; } private static boolean initInstrumentation() { try { try { instrumentation = ByteBuddyAgent.getInstrumentation(); } catch (IllegalStateException e) { instrumentation = ByteBuddyAgent.install( new ByteBuddyAgent.AttachmentProvider.Compound( new EhCacheAttachmentProvider(), ByteBuddyAgent.AttachmentProvider.DEFAULT)); } ensureDispatcherIsAppendedToBootstrapClasspath(instrumentation); if (!Dispatcher.getValues().containsKey(IGNORED_CLASSLOADERS_KEY)) { Dispatcher.put(IGNORED_CLASSLOADERS_KEY, Collections.newSetFromMap(new ConcurrentHashMap<Integer, Boolean>())); } hashCodesOfClassLoadersToIgnore = Dispatcher.get(IGNORED_CLASSLOADERS_KEY); return true; } catch (Exception e) { logger.warn("Failed to perform runtime attachment of the stagemonitor agent. Make sure that you run your " + "application with a JDK (not a JRE)." + "To make stagemonitor work with a JRE, you have to add the following command line argument to the " + "start of the JVM: -javaagent:/path/to/byte-buddy-agent-<version>.jar", e); return false; } } private static void ensureDispatcherIsAppendedToBootstrapClasspath(Instrumentation instrumentation) throws ClassNotFoundException, IOException { final ClassLoader bootstrapClassloader = ClassLoader.getSystemClassLoader().getParent(); try { bootstrapClassloader.loadClass(DISPATCHER_CLASS_NAME); // already injected } catch (ClassNotFoundException e) { final JarFile jarfile = new JarFile(createTempDispatcherJar()); instrumentation.appendToBootstrapClassLoaderSearch(jarfile); bootstrapClassloader.loadClass(DISPATCHER_CLASS_NAME); } } private static File createTempDispatcherJar() throws IOException { final InputStream input = AgentAttacher.class.getClassLoader() .getResourceAsStream("stagemonitor-dispatcher.jar.gradlePleaseDontExtract"); if (input == null) { throw new IllegalStateException("If you see this exception inside you IDE, you have to execute gradle " + "processResources before running the tests."); } final File tempDispatcherJar = File.createTempFile("stagemonitor-dispatcher", ".jar"); tempDispatcherJar.deleteOnExit(); IOUtils.copy(input, new FileOutputStream(tempDispatcherJar)); return tempDispatcherJar; } private static ClassFileTransformer initByteBuddyClassFileTransformer(AutoEvictingCachingBinaryLocator binaryLocator) { AgentBuilder agentBuilder = createAgentBuilder(binaryLocator); for (StagemonitorByteBuddyTransformer transformer : getStagemonitorByteBuddyTransformers()) { agentBuilder = agentBuilder .type(transformer.getMatcher()) .transform(transformer.getTransformer()) .asDecorator(); } final long start = System.currentTimeMillis(); try { return agentBuilder.installOn(instrumentation); } finally { if (corePlugin.isDebugInstrumentation()) { logger.info("Installed agent in {} ms", System.currentTimeMillis() - start); } } } private static AgentBuilder createAgentBuilder(AutoEvictingCachingBinaryLocator binaryLocator) { final ByteBuddy byteBuddy = new ByteBuddy() .with(TypeValidation.of(corePlugin.isDebugInstrumentation())) .with(MethodGraph.Compiler.ForDeclaredMethods.INSTANCE); return new AgentBuilder.Default(byteBuddy) .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) .with(getListener()) .with(binaryLocator) .ignore(any(), timed("classloader", "bootstrap", isBootstrapClassLoader())) .or(any(), timed("classloader", "reflection", isReflectionClassLoader())) .or(any(), timed("classloader", "groovy-call-site", classLoaderWithName("org.codehaus.groovy.runtime.callsite.CallSiteClassLoader"))) .or(any(), new IsIgnoredClassLoaderElementMatcher()) .or(timed("type", "global-exclude", nameStartsWith("java") .or(nameStartsWith("com.sun.")) .or(nameStartsWith("sun.")) .or(nameStartsWith("jdk.")) .or(nameStartsWith("org.aspectj.")) .or(nameStartsWith("org.groovy.")) .or(nameStartsWith("com.p6spy.")) .or(nameStartsWith("net.bytebuddy.")) .or(nameStartsWith("org.slf4j.").and(not(nameStartsWith("org.slf4j.impl.")))) .or(nameContains("javassist")) .or(nameContains(".asm.")) .or(nameStartsWith("org.stagemonitor") .and(not(nameContains("Test") .or(nameContains("benchmark")) .or(nameStartsWith("org.stagemonitor.demo"))))) )) .disableClassFormatChanges(); } private static AgentBuilder.Listener getListener() { List<AgentBuilder.Listener> listeners = new ArrayList<AgentBuilder.Listener>(2); if (corePlugin.isDebugInstrumentation()) { listeners.add(new ErrorLoggingListener()); } if (!corePlugin.getExportClassesWithName().isEmpty()) { listeners.add(new FileExportingListener(corePlugin.getExportClassesWithName())); } return new AgentBuilder.Listener.Compound(listeners); } private static Iterable<StagemonitorByteBuddyTransformer> getStagemonitorByteBuddyTransformers() { List<StagemonitorByteBuddyTransformer> transformers = new ArrayList<StagemonitorByteBuddyTransformer>(); for (StagemonitorByteBuddyTransformer transformer : ServiceLoader.load(StagemonitorByteBuddyTransformer.class, Stagemonitor.class.getClassLoader())) { if (transformer.isActive() && !isExcluded(transformer)) { transformers.add(transformer); if (corePlugin.isDebugInstrumentation()) { logger.info("Registering {}", transformer.getClass().getSimpleName()); } } else if (corePlugin.isDebugInstrumentation()) { logger.info("Excluding {}", transformer.getClass().getSimpleName()); } } Collections.sort(transformers, new Comparator<StagemonitorByteBuddyTransformer>() { @Override public int compare(StagemonitorByteBuddyTransformer o1, StagemonitorByteBuddyTransformer o2) { return o1.getOrder() > o2.getOrder() ? -1 : 1; } }); return transformers; } private static boolean isExcluded(StagemonitorByteBuddyTransformer transformer) { return corePlugin.getExcludedInstrumenters().contains(transformer.getClass().getSimpleName()); } private static class IsIgnoredClassLoaderElementMatcher implements ElementMatcher<ClassLoader> { @Override public boolean matches(ClassLoader target) { return hashCodesOfClassLoadersToIgnore.contains(ClassUtils.getIdentityString(target)); } } }