All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.fitbur.bytebuddy.agent.ByteBuddyAgent Maven / Gradle / Ivy

There is a newer version: 1.0.0
Show newest version
package com.fitbur.bytebuddy.agent;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.instrument.Instrumentation;
import java.lang.management.ManagementFactory;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.com.fitbur.MalformedURLException;
import java.com.fitbur.URL;
import java.com.fitbur.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.logging.Logger;

/**
 * 

* The Byte Buddy agent provides a JVM {@link java.lang.instrument.Instrumentation} in order to allow Byte Buddy the * redefinition of already loaded classes. An agent must normally be specified via the command line via the * {@code javaagent} parameter. As an argument to this parameter, one must specify the location of this agent's jar * file such as for example in *

*

* * java -javaagent:byte-buddy-agent.jar -jar app.jar * *

*

* Note: The runtime installation of a Java agent is not possible on all JVMs. See the documentation for * {@link ByteBuddyAgent#install()} for details on JVMs that are supported out of the box. *

*

* Important: This class's name is known to the Byte Buddy main application and must not be altered. *

*/ public class ByteBuddyAgent { /** * The manifest property specifying the agent class. */ private static final String AGENT_CLASS_PROPERTY = "Agent-Class"; /** * The manifest property specifying the can redefine property. */ private static final String CAN_REDEFINE_CLASSES_PROPERTY = "Can-Redefine-Classes"; /** * The manifest property specifying the can retransform property. */ private static final String CAN_RETRANSFORM_CLASSES_PROPERTY = "Can-Retransform-Classes"; /** * The manifest property specifying the can set native method prefix property. */ private static final String CAN_SET_NATIVE_METHOD_PREFIX = "Can-Set-Native-Method-Prefix"; /** * The manifest property value for the manifest version. */ private static final String MANIFEST_VERSION_VALUE = "1.0"; /** * The size of the buffer for copying the agent installer file into another jar. */ private static final int BUFFER_SIZE = 1024; /** * Convenience indices for reading and writing to the buffer to make the code more readable. */ private static final int START_INDEX = 0, END_OF_FILE = -1; /** * Base for access to a reflective member to make the code more readable. */ private static final Object STATIC_MEMBER = null; /** * Representation of the bootstrap {@link java.lang.ClassLoader}. */ private static final ClassLoader BOOTSTRAP_CLASS_LOADER = null; /** * Empty command line arguments. */ private static final String WITHOUT_ARGUMENTS = ""; /** * The default prefix of the Byte Buddy agent jar file. */ private static final String AGENT_FILE_NAME = "byteBuddyAgent"; /** * The jar file extension. */ private static final String JAR_FILE_EXTENSION = ".jar"; /** * The class file extension. */ private static final String CLASS_FILE_EXTENSION = ".class"; /** * The name of the {@code attach} method of the {@code VirtualMachine} class. */ private static final String ATTACH_METHOD_NAME = "attach"; /** * The name of the {@code loadAgent} method of the {@code VirtualMachine} class. */ private static final String LOAD_AGENT_METHOD_NAME = "loadAgent"; /** * The name of the {@code detach} method of the {@code VirtualMachine} class. */ private static final String DETACH_METHOD_NAME = "detach"; /** * The name of this class'S {@code instrumentation} field. */ private static final String INSTRUMENTATION_FIELD_NAME = "instrumentation"; /** * An indicator variable to express that no instrumentation is available. */ private static final Instrumentation UNAVAILABLE = null; /** * The agent provides only {@code static} utility methods and should not be instantiated. */ private ByteBuddyAgent() { throw new UnsupportedOperationException(); } /** *

* Installs an agent on the currently running Java virtual machine. Unfortunately, this does * not always work. The runtime installation of a Java agent is supported for: *

*
    *
  • JVM version 9+: For Java VM of at least version 9, the attachment API was merged * into a Jigsaw module and the runtime installation is always possible.
  • *
  • OpenJDK / Oracle JDK / IBM J9 versions 8-: The installation for HotSpot is only * possible when bundled with a JDK up until Java version 8. It is not possible for runtime-only * installations of HotSpot or J9 for these versions.
  • *
*

* If an agent cannot be installed, a {@link IllegalStateException} is thrown. *

*

* Important: This is a rather computation-heavy operation. Therefore, this operation is * not repeated after an agent was successfully installed for the first time. Instead, the previous * instrumentation instance is returned. However, invoking this method requires synchronization * such that subsequently to an installation, {@link ByteBuddyAgent#getInstrumentation()} should * be invoked instead. *

* * @return An instrumentation instance representing the currently running JVM. */ public static Instrumentation install() { return install(AttachmentProvider.DEFAULT); } /** * Installs a Java agent using the Java attach API. This API is available under different * access routes for different JVMs and JVM versions or it might not be available at all. * If a Java agent cannot be installed by using the supplied attachment provider, a * {@link IllegalStateException} is thrown. * * @param attachmentProvider The attachment provider to use for the installation. * @return An instrumentation instance representing the currently running JVM. */ public static synchronized Instrumentation install(AttachmentProvider attachmentProvider) { Instrumentation instrumentation = doGetInstrumentation(); if (instrumentation != null) { return instrumentation; } AttachmentProvider.Accessor accessor = attachmentProvider.attempt(); if (accessor.isAvailable()) { try { doInstall(accessor); } catch (Exception exception) { throw new IllegalStateException("Error during attachment using: " + attachmentProvider, exception); } return getInstrumentation(); } else { throw new IllegalStateException("This JVM does not support attachment using: " + attachmentProvider); } } /** * Performs the actual installation of the Byte Buddy agent. * * @param accessor An available accessor for accessing the Java attachment API. * @throws Exception If the installation is not possible. */ private static void doInstall(AttachmentProvider.Accessor accessor) throws Exception { Class virtualMachine = accessor.getVirtualMachineType(); Object virtualMachineInstance = virtualMachine .getDeclaredMethod(ATTACH_METHOD_NAME, String.class) .invoke(STATIC_MEMBER, accessor.getProcessId()); File agentJar = File.createTempFile(AGENT_FILE_NAME, JAR_FILE_EXTENSION); try { saveAgentJar(agentJar); virtualMachine .getDeclaredMethod(LOAD_AGENT_METHOD_NAME, String.class, String.class) .invoke(virtualMachineInstance, agentJar.getAbsolutePath(), WITHOUT_ARGUMENTS); } finally { try { virtualMachine.getDeclaredMethod(DETACH_METHOD_NAME).invoke(virtualMachineInstance); } finally { if (!agentJar.delete()) { Logger.getAnonymousLogger().info("Cannot delete temporary file: " + agentJar); } } } } /** * Saves all necessary classes for the Byte Buddy agent jar to the given file. * * @param agentFile The target file to which all required classes are to be written. * @throws Exception If the writing to the file is not possible. */ private static void saveAgentJar(File agentFile) throws Exception { InputStream inputStream = Installer.class.getResourceAsStream('/' + Installer.class.getName().replace('.', '/') + CLASS_FILE_EXTENSION); if (inputStream == null) { throw new IllegalStateException("Cannot locate class file for Byte Buddy installer"); } try { Manifest manifest = new Manifest(); manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, MANIFEST_VERSION_VALUE); manifest.getMainAttributes().put(new Attributes.Name(AGENT_CLASS_PROPERTY), Installer.class.getName()); manifest.getMainAttributes().put(new Attributes.Name(CAN_REDEFINE_CLASSES_PROPERTY), Boolean.TRUE.toString()); manifest.getMainAttributes().put(new Attributes.Name(CAN_RETRANSFORM_CLASSES_PROPERTY), Boolean.TRUE.toString()); manifest.getMainAttributes().put(new Attributes.Name(CAN_SET_NATIVE_METHOD_PREFIX), Boolean.TRUE.toString()); JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(agentFile), manifest); try { jarOutputStream.putNextEntry(new JarEntry(Installer.class.getName().replace('.', '/') + CLASS_FILE_EXTENSION)); byte[] buffer = new byte[BUFFER_SIZE]; int index; while ((index = inputStream.read(buffer)) != END_OF_FILE) { jarOutputStream.write(buffer, START_INDEX, index); } jarOutputStream.closeEntry(); } finally { jarOutputStream.close(); } } finally { inputStream.close(); } } /** *

* Looks up the {@link java.lang.instrument.Instrumentation} instance of an installed Byte Buddy agent. Note that * this method implies reflective lookup and reflective invocation such that the returned value should be cached * rather than calling this method several times. *

*

* Note: This method throws an {@link java.lang.IllegalStateException} If the Byte Buddy agent is not * properly installed. *

* * @return The {@link java.lang.instrument.Instrumentation} instance which is provided by an installed * Byte Buddy agent. */ public static Instrumentation getInstrumentation() { Instrumentation instrumentation = doGetInstrumentation(); if (instrumentation == null) { throw new IllegalStateException("The Byte Buddy agent is not initialized"); } return instrumentation; } /** * Performs the actual lookup of the {@link java.lang.instrument.Instrumentation} from an installed * Byte Buddy agent. * * @return The Byte Buddy agent's {@link java.lang.instrument.Instrumentation} instance. */ @SuppressFBWarnings(value = "REC_CATCH_EXCEPTION", justification = "Legal state where reflection communicates errors by exception") private static Instrumentation doGetInstrumentation() { try { return (Instrumentation) ClassLoader.getSystemClassLoader() .loadClass(Installer.class.getName()) .getDeclaredField(INSTRUMENTATION_FIELD_NAME) .get(STATIC_MEMBER); } catch (Exception ignored) { return UNAVAILABLE; } } /** * An attachment provider is responsible for making the Java attachment API available. */ @SuppressFBWarnings(value = "IC_SUPERCLASS_USES_SUBCLASS_DURING_INITIALIZATION", justification = "No circularity, initialization is safe") public interface AttachmentProvider { /** * The default attachment provider to be used. */ AttachmentProvider DEFAULT = new Compound(ForJigsawVm.INSTANCE, ForToolsJarVm.JVM_ROOT, ForToolsJarVm.JDK_ROOT, ForToolsJarVm.MACINTOSH); /** * Attempts the creation of an accessor for a specific JVM's attachment API. * * @return The accessor this attachment provider can supply for the currently running JVM. */ Accessor attempt(); /** * An accessor for a JVM's attachment API. */ interface Accessor { /** * The name of the {@code VirtualMachine} class. */ String VIRTUAL_MACHINE_TYPE_NAME = "com.sun.tools.attach.VirtualMachine"; /** * Determines if this accessor is applicable for the currently running JVM. * * @return {@code true} if this accessor is available. */ boolean isAvailable(); /** * Returns the {@code com.sun.tools.attach.VirtualMachine} class. This method must only be called * for available accessors. * * @return The virtual machine type. */ Class getVirtualMachineType(); /** * Returns the current JVM instance's process id. This method must only be called * for available accessors. * * @return The current JVM instance's process id. */ String getProcessId(); /** * A canonical implementation of an unavailable accessor. */ enum Unavailable implements Accessor { /** * The singleton instance. */ INSTANCE; @Override public boolean isAvailable() { return false; } @Override public Class getVirtualMachineType() { throw new IllegalStateException("Cannot read the virtual machine type for an unavailable accessor"); } @Override public String getProcessId() { throw new IllegalStateException("Cannot read process ID for an unavailable accessor"); } @Override public String toString() { return "ByteBuddyAgent.AttachmentProvider.Accessor.Unavailable." + name(); } } /** * A simple implementation of an accessible accessor. */ class Simple implements Accessor { /** * A dispatcher for reading the current process's process id. */ private static final Dispatcher DISPATCHER = Dispatcher.ForJava9CapableVm.make(); /** * The {@code com.sun.tools.attach.VirtualMachine} class. */ private final Class virtualMachineType; /** * The current JVM instance's process id. */ private final String processId; /** * Creates a new simple accessor. * * @param virtualMachineType The {@code com.sun.tools.attach.VirtualMachine} class. * @param processId The current JVM instance's process id. */ protected Simple(Class virtualMachineType, String processId) { this.virtualMachineType = virtualMachineType; this.processId = processId; } /** * Creates an accessor by reading the process id from the JMX runtime bean and by attempting * to load the {@code com.sun.tools.attach.VirtualMachine} class from the provided class loader. * * @param classLoader A class loader that is capable of loading the virtual machine type. * @return An appropriate accessor. */ public static Accessor of(ClassLoader classLoader) { try { return of(classLoader.loadClass(VIRTUAL_MACHINE_TYPE_NAME)); } catch (ClassNotFoundException ignored) { return Unavailable.INSTANCE; } } /** * Creates an accessor by reading the process id from the JMX runtime bean. * * @param virtualMachineType The virtual machine type. * @return An appropriate accessor. */ protected static Accessor of(Class virtualMachineType) { return DISPATCHER.resolve(virtualMachineType); } @Override public boolean isAvailable() { return true; } @Override public Class getVirtualMachineType() { return virtualMachineType; } @Override public String getProcessId() { return processId; } @Override public boolean equals(Object other) { if (this == other) return true; if (other == null || getClass() != other.getClass()) return false; Simple simple = (Simple) other; return virtualMachineType.equals(simple.virtualMachineType) && processId.equals(simple.processId); } @Override public int hashCode() { int result = virtualMachineType.hashCode(); result = 31 * result + processId.hashCode(); return result; } @Override public String toString() { return "ByteBuddyAgent.AttachmentProvider.Accessor.Simple{" + "virtualMachineType=" + virtualMachineType + ", processId='" + processId + '\'' + '}'; } /** * A dispatcher for reading a process id of the current process. */ protected interface Dispatcher { /** * Resolves an accessor for the supplied virtual machine type. * * @param virtualMachineType The virtual machine type to attach to. * @return A suitable accessor. */ Accessor resolve(Class virtualMachineType); /** * A dispatcher for a legacy VM that cannot read the current process's id via a process handle. */ enum ForLegacyVm implements Dispatcher { /** * The singleton instance. */ INSTANCE; @Override public Accessor resolve(Class virtualMachineType) { String runtimeName = ManagementFactory.getRuntimeMXBean().getName(); int processIdIndex = runtimeName.indexOf('@'); return processIdIndex == -1 ? Unavailable.INSTANCE : new Simple(virtualMachineType, runtimeName.substring(0, processIdIndex)); } @Override public String toString() { return "ByteBuddyAgent.AttachmentProvider.Accessor.Simple.Dispatcher.ForLegacyVm." + name(); } } /** * A dispatcher for reading a process id of the current process via a process handle. */ class ForJava9CapableVm implements Dispatcher { /** * The {@code java.lang.ProcessHandle#current()} method. */ private final Method current; /** * The {@code java.lang.ProcessHandle#getPid()} method. */ private final Method getPid; /** * Creates a new Java 9 capable dispatcher for reading the current process's id. * * @param current The {@code java.lang.ProcessHandle#current()} method. * @param getPid The {@code java.lang.ProcessHandle#getPid()} method. */ protected ForJava9CapableVm(Method current, Method getPid) { this.current = current; this.getPid = getPid; } /** * Attempts to create a dispatcher for a Java 9 VM and falls back to a legacy dispatcher * if this is not possible. * * @return A dispatcher for the current VM. */ public static Dispatcher make() { try { return new ForJava9CapableVm(Class.forName("java.lang.ProcessHandle").getDeclaredMethod("current"), Class.forName("java.lang.ProcessHandle").getDeclaredMethod("getPid")); } catch (RuntimeException exception) { throw exception; } catch (Exception ignored) { return ForLegacyVm.INSTANCE; } } @Override public Accessor resolve(Class virtualMachineType) { try { return new Simple(virtualMachineType, getPid.invoke(current.invoke(STATIC_MEMBER)).toString()); } catch (IllegalAccessException exception) { throw new UnsupportedOperationException("Not yet implemented"); } catch (InvocationTargetException exception) { throw new UnsupportedOperationException("Not yet implemented"); } } @Override public boolean equals(Object other) { if (this == other) return true; if (other == null || getClass() != other.getClass()) return false; ForJava9CapableVm that = (ForJava9CapableVm) other; return current.equals(that.current) && getPid.equals(that.getPid); } @Override public int hashCode() { int result = current.hashCode(); result = 31 * result + getPid.hashCode(); return result; } @Override public String toString() { return "ByteBuddyAgent.AttachmentProvider.Accessor.Simple.Dispatcher.ForJava9CapableVm{" + "current=" + current + ", getPid=" + getPid + '}'; } } } } } /** * An attachment provider that locates the attach API directly from the bootstrap class loader. */ enum ForJigsawVm implements AttachmentProvider { /** * The singleton instance. */ INSTANCE; @Override public Accessor attempt() { return Accessor.Simple.of(ClassLoader.getSystemClassLoader()); } @Override public String toString() { return "ByteBuddyAgent.AttachmentProvider.ForJigsawVm." + name(); } } /** * An attachment provider that is dependant on the existence of a tools.jar file on the local * file system. */ enum ForToolsJarVm implements AttachmentProvider { /** * An attachment provider that locates the tools.jar from a Java home directory. */ JVM_ROOT("../lib/tools.jar"), /** * An attachment provider that locates the tools.jar from a Java installation directory. * In practice, several virtual machines do not return the JRE's location for the * java.home property against the property's specification. */ JDK_ROOT("lib/tools.jar"), /** * An attachment provider that locates the tools.jar as it is set for several JVM * installations on Apple Macintosh computers. */ MACINTOSH("../Classes/classes.jar"); /** * The JVM property for this JVM instance's home folder. */ private static final String JAVA_HOME_PROPERTY = "java.home"; /** * The path to the tools.jar file, starting from the Java home directory. */ private final String toolsJarPath; /** * Creates a new attachment provider that loads the virtual machine class from the tools.jar. * * @param toolsJarPath The path to the tools.jar file, starting from the Java home directory. */ ForToolsJarVm(String toolsJarPath) { this.toolsJarPath = toolsJarPath; } @Override public Accessor attempt() { File toolsJar = new File(System.getProperty(JAVA_HOME_PROPERTY).replace('\\', '/') + "/../" + toolsJarPath); return toolsJar.isFile() && toolsJar.canRead() ? Accessor.Simple.of(AccessController.doPrivileged(new ClassLoaderCreationAction(toolsJar))) : Accessor.Unavailable.INSTANCE; } @Override public String toString() { return "ByteBuddyAgent.AttachmentProvider.ForToolsJarVm." + name(); } /** * The action creates a class loader that is capable of reading form the provided tools.jar. */ protected static class ClassLoaderCreationAction implements PrivilegedAction { /** * The file representing the tools.jar location. */ private final File toolsJar; /** * Creates a new class loader creation action. * * @param toolsJar The file representing the tools.jar location. */ public ClassLoaderCreationAction(File toolsJar) { this.toolsJar = toolsJar; } @Override public ClassLoader run() { try { return new URLClassLoader(new URL[]{toolsJar.toURI().toURL()}, BOOTSTRAP_CLASS_LOADER); } catch (MalformedURLException exception) { throw new IllegalStateException("Could not represent " + toolsJar + " as URL"); } } @Override public boolean equals(Object other) { if (this == other) return true; if (other == null || getClass() != other.getClass()) return false; ClassLoaderCreationAction that = (ClassLoaderCreationAction) other; return toolsJar.equals(that.toolsJar); } @Override public int hashCode() { return toolsJar.hashCode(); } @Override public String toString() { return "ByteBuddyAgent.AttachmentProvider.ForToolsJarVm.ClassLoaderCreationAction{" + "toolsJar=" + toolsJar + '}'; } } } /** * A compound attachment provider that attempts the attachment by delegation to other providers. If * none of the providers of this compound provider is capable of providing a valid accessor, an * non-available accessor is returned. */ class Compound implements AttachmentProvider { /** * A list of attachment providers in the order of their application. */ private final List attachmentProviders; /** * Creates a new compound attachment provider. * * @param attachmentProvider A list of attachment providers in the order of their application. */ public Compound(AttachmentProvider... attachmentProvider) { this(Arrays.asList(attachmentProvider)); } /** * Creates a new compound attachment provider. * * @param attachmentProviders A list of attachment providers in the order of their application. */ public Compound(List attachmentProviders) { this.attachmentProviders = attachmentProviders; } @Override public Accessor attempt() { for (AttachmentProvider attachmentProvider : attachmentProviders) { Accessor accessor = attachmentProvider.attempt(); if (accessor.isAvailable()) { return accessor; } } return Accessor.Unavailable.INSTANCE; } @Override public boolean equals(Object other) { if (this == other) return true; if (other == null || getClass() != other.getClass()) return false; Compound compound = (Compound) other; return attachmentProviders.equals(compound.attachmentProviders); } @Override public int hashCode() { return attachmentProviders.hashCode(); } @Override public String toString() { return "ByteBuddyAgent.AttachmentProvider.Compound{" + "attachmentProviders=" + attachmentProviders + '}'; } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy