├── README.md └── RedefineClassAgent.java /README.md: -------------------------------------------------------------------------------- 1 | # Java classes can be changed at runtime 2 | Class redefinition is the act of replacing the bytecode of a class after the class has already been loaded. 3 | 4 | A common example is changing code while debugging. The debugger may recompile the class, then have the JVM's debug agent replace the class bytecode while the application is running. This way the programmer can immediately see the effect of the change they made. 5 | 6 | This repo is about how to programmatically redefine classes by a fully supported method that isn't well documented online. The rest of this README describes the process and [`RedefineClassAgent.java`](https://github.com/turn/RedefineClassAgent/blob/master/RedefineClassAgent.java) completely implements it. 7 | 8 | # Programmatically redefining classes using the debug agent 9 | 10 | [`javassist`](http://jboss-javassist.github.io/javassist/) provides a class called [`HotSwapper`](https://jboss-javassist.github.io/javassist/html/javassist/util/HotSwapper.html) that is able to use the debug agent to redefine arbitrary classes. You pass it a class and the class' new bytecode. 11 | 12 | The downside of this approach is that a) the debug agent must be running and listening on a port, and b) the debug agent is buggy. 13 | 14 | We've noticed that the debug agent can freeze the JVM if the JVM is under heavy load on a large multithreaded system. We've even had to disable the debug agent on one class of machines because the agent would cause a freeze due to a thread safety problem in its class loading hooks. Those machines do a lot of online class loading of generated code. 15 | 16 | # Programmatically redefining classes using the Instrumentation class 17 | 18 | Java 1.6 introduced the [`Instrumentation`](https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html) class that has a handy [`redefineClasses`](https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html#redefineClasses-java.lang.instrument.ClassDefinition...-) method. The trick is how to obtain an instance of `Instrumentation`. 19 | 20 | When an agent is loaded by the JVM `agentmain()` is passed an instance of `Instrumentation`. If the agent's `MANIFEST` contains `Can-Redefine-Classes: true` then that instance of `Instrumentation` can be used to redefine classes. 21 | 22 | [`RedefineClassAgent`](https://github.com/turn/RedefineClassAgent/blob/master/RedefineClassAgent.java) does everything you need to do: 23 | 24 | * Agents are specified as JARs. For simplicity we can programmatically generate such a JAR in a temporary location. We set the appropriate properties. 25 | * Connect to the JVM. One way to do so is by PID. We attempt to parse the PID out of an MXBean. Yes this is platform dependent, but it appears to work well. 26 | * Load the agent into the JVM. 27 | * The agent saves the `Instrumentation` instance into a `static` variable for later use. The agent's job is done; it exists. 28 | * The cached `Instrumentation` instance is used for class redefinition. 29 | 30 | `RedefineClassAgent.redefineClasses()` does all that for you. 31 | 32 | # Requirements 33 | 34 | * `javassist` is a dependency. It's used to dump `RedefineClassAgent`'s bytecode into the temporary JAR. You'll probably use `javassist` to construct your own new bytecode anyway. 35 | * `tools.jar` is a dependency. It's bundled with the JDK. Add it to your classpath if it's not there already. 36 | * PID detection may break on your platform. It works on HotSpot on Linux. 37 | * Temporary directory must be writeable. 38 | 39 | # Usage 40 | 41 | Class clazz; // class to redefine 42 | byte[] bytecode; // new class bytecode 43 | 44 | ClassDefinition definition = new ClassDefinition(clazz, bytecode)); 45 | RedefineClassAgent.redefineClasses(definition); 46 | 47 | Note that you can't change the schema of the class. You can change method bodies, but you can't add or remove methods. 48 | 49 | # Examples 50 | 51 | ## Reload from .class file 52 | Handy if you're testing a change by a hot patch and you'd like to apply it live without having to restart the JVM. Useful if, for example, your service must load a lot of state before it can start serving requests (a restart is expensive) or if you wish to not break existing network connections or internal state. 53 | 54 | Class clazz = Class.forName(className); // the class to reload 55 | 56 | // load the bytecode from the .class file 57 | URL url = new URL(clazz.getProtectionDomain().getCodeSource().getLocation(), 58 | className.replace(".", "/") + ".class"); 59 | InputStream classStream = url.openStream(); 60 | byte[] bytecode = IOUtils.toByteArray(classStream); 61 | 62 | ClassDefinition definition = new ClassDefinition(clazz, bytecode)); 63 | RedefineClassAgent.redefineClasses(definition); 64 | 65 | ## Inject a bit of code into a method 66 | Use `javassist` to find the method, compile and inject new code, then dump new bytecode of the entire class. 67 | 68 | // find a reference to the class and method you wish to inject 69 | ClassPool classPool = ClassPool.getDefault(); 70 | CtClass ctClass = classPool.get(className); 71 | ctClass.stopPruning(true); 72 | 73 | // javaassist freezes methods if their bytecode is saved 74 | // defrost so we can still make changes. 75 | if (ctClass.isFrozen()) { 76 | ctClass.defrost(); 77 | } 78 | 79 | CtMethod method; // populate this from ctClass however you wish 80 | 81 | method.insertBefore("{ System.out.println(\"Wheeeeee!\"); }"); 82 | byte[] bytecode = ctClass.toBytecode(); 83 | 84 | ClassDefinition definition = new ClassDefinition(Class.forName(className), bytecode); 85 | RedefineClassAgent.redefineClasses(definition); 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /RedefineClassAgent.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Turn Inc 3 | All rights reserved. 4 | 5 | The contents of this file are subject to the MIT License as provided 6 | below. Alternatively, the contents of this file may be used under 7 | the terms of Mozilla Public License Version 1.1, 8 | the terms of the GNU Lesser General Public License Version 2.1 or later, 9 | or the terms of the Apache License Version 2.0. 10 | 11 | License: 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of 14 | this software and associated documentation files (the "Software"), to deal in 15 | the Software without restriction, including without limitation the rights to 16 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 17 | the Software, and to permit persons to whom the Software is furnished to do so, 18 | subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in all 21 | copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 25 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 26 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 27 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 28 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 29 | */ 30 | 31 | import com.sun.tools.attach.VirtualMachine; 32 | import javassist.CannotCompileException; 33 | import javassist.ClassPool; 34 | import javassist.CtClass; 35 | import javassist.NotFoundException; 36 | 37 | import java.io.File; 38 | import java.io.FileOutputStream; 39 | import java.io.IOException; 40 | import java.lang.instrument.ClassDefinition; 41 | import java.lang.instrument.Instrumentation; 42 | import java.lang.instrument.UnmodifiableClassException; 43 | import java.lang.management.ManagementFactory; 44 | import java.util.jar.Attributes; 45 | import java.util.jar.JarEntry; 46 | import java.util.jar.JarOutputStream; 47 | import java.util.jar.Manifest; 48 | import java.util.logging.Level; 49 | import java.util.logging.Logger; 50 | 51 | /** 52 | * Packages everything necessary to be able to redefine a class using {@link Instrumentation} as provided by 53 | * Java 1.6 or later. Class redefinition is the act of replacing a class' bytecode at runtime, after that class 54 | * has already been loaded. 55 | *

56 | * The scheme employed by this class uses an agent (defined by this class) that, when loaded into the JVM, provides 57 | * an instance of {@link Instrumentation} which in turn provides a method to redefine classes. 58 | *

59 | * Users of this class only need to call {@link #redefineClasses(ClassDefinition...)}. The agent stuff will be done 60 | * automatically (and lazily). 61 | *

62 | * Note that classes cannot be arbitrarily redefined. The new version must retain the same schema; methods and fields 63 | * cannot be added or removed. In practice this means that method bodies can be changed. 64 | *

65 | * Note that this is a replacement for javassist's {@code HotSwapper}. {@code HotSwapper} depends on the debug agent 66 | * to perform the hotswap. That agent is available since Java 1.3, but the JVM must be started with the agent enabled, 67 | * and the agent often fails to perform the swap if the machine is under heavy load. This class is both cleaner and more 68 | * reliable. 69 | * 70 | * @see Instrumentation#redefineClasses(ClassDefinition...) 71 | * 72 | * @author Adam Lugowski 73 | */ 74 | public class RedefineClassAgent { 75 | /** 76 | * Use the Java logger to avoid any references to anything not supplied by the JVM. This avoids issues with 77 | * classpath when compiling/loading this class as an agent. 78 | */ 79 | private static final Logger LOGGER = Logger.getLogger(RedefineClassAgent.class.getSimpleName()); 80 | 81 | /** 82 | * Populated when this class is loaded into the JVM as an agent (via {@link #ensureAgentLoaded()}. 83 | */ 84 | private static volatile Instrumentation instrumentation = null; 85 | 86 | /** 87 | * How long to wait for the agent to load before giving up and assuming the load failed. 88 | */ 89 | private static final int AGENT_LOAD_WAIT_TIME_SEC = 10; 90 | 91 | /** 92 | * Agent entry point. Do not call this directly. 93 | *

94 | * This method is called by the JVM when this class is loaded as an agent. 95 | *

96 | * Sets {@link #instrumentation} to {@code inst}, provided {@code inst} supports class redefinition. 97 | * 98 | * @param agentArgs ignored. 99 | * @param inst This is the reason this class exists. {@link Instrumentation} has the 100 | * {@link Instrumentation#redefineClasses(ClassDefinition...)} method. 101 | */ 102 | public static void agentmain(String agentArgs, Instrumentation inst) { 103 | if (!inst.isRedefineClassesSupported()) { 104 | LOGGER.severe("Class redefinition not supported. Aborting."); 105 | return; 106 | } 107 | 108 | instrumentation = inst; 109 | } 110 | 111 | /** 112 | * Attempts to redefine class bytecode. 113 | *

114 | * On first call this method will attempt to load an agent into the JVM to obtain an instance of 115 | * {@link Instrumentation}. This agent load can introduce a pause (in practice 1 to 2 seconds). 116 | * 117 | * @see Instrumentation#redefineClasses(ClassDefinition...) 118 | * 119 | * @param definitions classes to redefine. 120 | * @throws UnmodifiableClassException as thrown by {@link Instrumentation#redefineClasses(ClassDefinition...)} 121 | * @throws ClassNotFoundException as thrown by {@link Instrumentation#redefineClasses(ClassDefinition...)} 122 | * @throws FailedToLoadAgentException if agent either failed to load or if the agent wasn't able to get an 123 | * instance of {@link Instrumentation} that allows class redefinitions. 124 | */ 125 | public static void redefineClasses(ClassDefinition... definitions) 126 | throws UnmodifiableClassException, ClassNotFoundException, FailedToLoadAgentException { 127 | ensureAgentLoaded(); 128 | instrumentation.redefineClasses(definitions); 129 | } 130 | 131 | /** 132 | * Lazy loads the agent that populates {@link #instrumentation}. OK to call multiple times. 133 | * 134 | * @throws FailedToLoadAgentException if agent either failed to load or if the agent wasn't able to get an 135 | * instance of {@link Instrumentation} that allows class redefinitions. 136 | */ 137 | private static void ensureAgentLoaded() throws FailedToLoadAgentException { 138 | if (instrumentation != null) { 139 | // already loaded 140 | return; 141 | } 142 | 143 | // load the agent 144 | try { 145 | File agentJar = createAgentJarFile(); 146 | 147 | // Loading an agent requires the PID of the JVM to load the agent to. Find out our PID. 148 | String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName(); 149 | String pid = nameOfRunningVM.substring(0, nameOfRunningVM.indexOf('@')); 150 | 151 | // load the agent 152 | VirtualMachine vm = VirtualMachine.attach(pid); 153 | vm.loadAgent(agentJar.getAbsolutePath(), ""); 154 | vm.detach(); 155 | } catch (Exception e) { 156 | throw new FailedToLoadAgentException(e); 157 | } 158 | 159 | // wait for the agent to load 160 | for (int sec = 0; sec < AGENT_LOAD_WAIT_TIME_SEC; sec++) { 161 | if (instrumentation != null) { 162 | // success! 163 | return; 164 | } 165 | 166 | try { 167 | LOGGER.info("Sleeping for 1 second while waiting for agent to load."); 168 | Thread.sleep(1000); 169 | } catch (InterruptedException e) { 170 | Thread.currentThread().interrupt(); 171 | throw new FailedToLoadAgentException(); 172 | } 173 | } 174 | 175 | // agent didn't load 176 | throw new FailedToLoadAgentException(); 177 | } 178 | 179 | /** 180 | * An agent must be specified as a .jar where the manifest has an Agent-Class attribute. Additionally, in order 181 | * to be able to redefine classes, the Can-Redefine-Classes attribute must be true. 182 | * 183 | * This method creates such an agent Jar as a temporary file. The Agent-Class is this class. If the returned Jar 184 | * is loaded as an agent then {@link #agentmain(String, Instrumentation)} will be called by the JVM. 185 | * 186 | * @return a temporary {@link File} that points at Jar that packages this class. 187 | * @throws IOException if agent Jar creation failed. 188 | */ 189 | private static File createAgentJarFile() throws IOException { 190 | File jarFile = File.createTempFile("agent", ".jar"); 191 | jarFile.deleteOnExit(); 192 | 193 | // construct a manifest that allows class redefinition 194 | Manifest manifest = new Manifest(); 195 | Attributes mainAttributes = manifest.getMainAttributes(); 196 | mainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); 197 | mainAttributes.put(new Attributes.Name("Agent-Class"), RedefineClassAgent.class.getName()); 198 | mainAttributes.put(new Attributes.Name("Can-Retransform-Classes"), "true"); 199 | mainAttributes.put(new Attributes.Name("Can-Redefine-Classes"), "true"); 200 | 201 | try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile), manifest)) { 202 | // add the agent .class into the .jar 203 | JarEntry agent = new JarEntry(RedefineClassAgent.class.getName().replace('.', '/') + ".class"); 204 | jos.putNextEntry(agent); 205 | 206 | // dump the class bytecode into the entry 207 | ClassPool pool = ClassPool.getDefault(); 208 | CtClass ctClass = pool.get(RedefineClassAgent.class.getName()); 209 | jos.write(ctClass.toBytecode()); 210 | jos.closeEntry(); 211 | } catch (CannotCompileException | NotFoundException e) { 212 | // Realistically this should never happen. 213 | LOGGER.log(Level.SEVERE, "Exception while creating RedefineClassAgent jar.", e); 214 | throw new IOException(e); 215 | } 216 | 217 | return jarFile; 218 | } 219 | 220 | /** 221 | * Marks a failure to load the agent and get an instance of {@link Instrumentation} that is able to redefine 222 | * classes. 223 | */ 224 | public static class FailedToLoadAgentException extends Exception { 225 | public FailedToLoadAgentException() { 226 | super(); 227 | } 228 | 229 | public FailedToLoadAgentException(Throwable cause) { 230 | super(cause); 231 | } 232 | } 233 | } 234 | --------------------------------------------------------------------------------