background
In the process of java project development and testing, you need to repeatedly modify the code, compile and deploy. In some large projects, the whole compilation and deployment process may take several minutes or even tens of minutes. During the joint debugging of front and rear interfaces or the modification of test problems, only one parameter may be modified. The front-end, back-end and test need to wait for tens of minutes. If java can support hot loading and reduce unnecessary time, it can also have development efficiency like nodejs.
problem analysis
To implement java code hot deployment, you first need to understand how java code runs.
java method execution process
The execution process of a method is very complex. Static parsing and dynamic dispatch are distinguished. For convenience of understanding, it can be roughly regarded as the following process:
- A java object will contain a corresponding class pointer in heap memory
- Find the corresponding method in the corresponding class by class name + method name + method description (parameter and return value)
- For static parsing, you can directly get a method reference address. For dynamically dispatched methods, you can get a symbol reference and find the target method address through the symbol reference
- The method instance is obtained through the method address, and the corresponding method bytecode instruction is pressed into the stack frame for execution
For details, please refer to: < in depth understanding of Java virtual machine - version 3 > Chapter 8.3
Summary: the calling of object methods will find the relevant method bytecode in the corresponding Class and load it into the thread stack frame for execution. Then we only need to get a new Class and load or replace the new Class into the method area to realize the hot loading function
How to get a Java class (bytecode) at run time
- Use javac or javax Java compiler api compilation source code under tools package
- Use ASM, Javassist, Bytebuddy and other third-party class libraries to generate bytecode (there are many descriptions on the Internet for detailed usage)
- Write java bytecode instructions directly (if you are not a jvm developer, there should not be many people who can write bytecode instructions directly to write a program, dissuade and dissuade)
How can I load classes at run time
java.lang.instrument.Instrumentation provides two methods
- redefineClasses: provide a class file to redefine a class. You can change the method body, constant pool and properties. You cannot add, delete or rename fields or methods. If there is an error in the bytecode, an exception is thrown.
- retransformClasses: updates a class. This method will not trigger class initialization, so the value of the static variable will remain in the state before the call.
Official api documentation: https://docs.oracle.com/javas...
However, there are still some shortcomings in redefining classes with Instrumentation
- The jvm limits the classes redefined at run time for security. It can only modify the logic, constants and attributes in the method (which can be solved by using dcevm jdk).
- Even if dcevm jdk is used to support new classes, methods and fields, some third-party frameworks will perform some internal initialization operations when starting. For example, beans will be scanned and instantiated when spring starts. After defining a new @ Service class using redefinitecclasses of Instrumentation, it will not be registered in spring, This requires a mechanism to notify spring to load new beans
dcevm jdk is a customized jdk that supports redefinition of classes, methods, and fields. Project home page: http://dcevm.github.io/
Now that we know that we can redefine a class using redefinitecclasses of Instrumentation, how can we get the Instrumentation object?
Starting from jdk 5, you can use java to write the agent implementation. You can define the premain or agentmain method in the agent class to obtain the Instrumentation instance.
premain
It is executed before the main method is started. When the jvm is started, the agent class is loaded through the - javaagent parameter
// Priority 1 is greater than 2 [1] public static void premain(String agentArgs, Instrumentation inst); [2] public static void premain(String agentArgs);
You need to specify premain class: org. In ManiFest example. MyAgent
agentmain
Load via Attach API
// Priority 1 is greater than 2 [1] public static void agentmain(String agentArgs, Instrumentation inst); [2] public static void agentmain(String agentArgs);
You need to specify agent class: org. In ManiFest example. MyAgent
Example
package org.example; public class MyAgent { /** * Load on startup */ public static void premain(String args, Instrumentation inst) { System.out.println("premain"); } /** * Runtime load (attach api) */ public static void agentmain(String args, Instrumentation inst) { System.out.println("agentmain"); } }
maven packaging
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.3.0</version> <executions> <execution> <goals> <goal>single</goal> </goals> <phase>package</phase> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifestEntries> <Premain-Class>org.example.MyAgent</Premain-Class> <Agent-Class>org.example.MyAgent</Agent-Class> </manifestEntries> </archive> </configuration> </execution> </executions> </plugin>
use
Load on jvm startup
# Fat refers to any jar that can run fat java -javaagent:myagent.jar fat
Load via Attach API
import java.io.IOException; import com.sun.tools.attach.*; public class AttachTest { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { if (args.length <= 1) { System.out.println("Usage: java AttachTest <PID> /path/to/myagent.jar"); return; } VirtualMachine vm = VirtualMachine.attach(args[0]); vm.loadAgent(args[1]); } }
Determine the implementation scheme
Through the review of the above related knowledge and the analysis of the actual development scenario, we can preliminarily draw the following implementation steps
Listen for changes to the project source file (. java)
- Listen for directory file changes through nio2's WatchService
- apache commons. The FileAlterationListenerAdaptor class under the IO package has encapsulated the processing logic related to file listening, which is convenient to use
Compile the changed source file into a bytecode file (. class)
- It can be generated through the automatic compilation function of ide (such as IntelliJ IDEA) class file
- Compile through javac or Java compiler related APIs
- Use the Attach API to load the custom agent class, and pass the class name and bytecode file path to the target jvm process through parameters
- Get the bytecode file path from the parameter and get the byte stream
- The user-defined agent class obtains the Instrumentation object, and redefines the class through the redefineClasses method
Redefiniteclasses only needs to obtain the class name and bytecode byte stream to redefine the class. Then you can also open a server on the remote server to provide a file upload interface to upload bytecode files to the server to realize the hot loading of remote jvm processes
architecture design
Source code implementation
Relevant code implementations have been put into github: https://github.com/fengjx/jav... , it can be used for reference.
reference resources
- https://tech.meituan.com/2019...
- https://tech.meituan.com/2019...
- https://tech.meituan.com/2020...
- https://leokongwq.github.io/2...
- https://blog.csdn.net/program...
Extended reading
How to realize jsp hot loading with tomcat
We all know that jsp files will eventually be compiled into a Servlet implementation class
Let's take a look at several key source codes for tomcat to realize jsp hot loading
// JspCompilationContext.java public void compile() throws JasperException, FileNotFoundException { createCompiler(); // Judge whether the document is changed if (jspCompiler.isOutDated()) { if (isRemoved()) { throw new FileNotFoundException(jspUri); } try { // Delete previously compiled files jspCompiler.removeGeneratedFiles(); // If it is set to null, a new jspLoader will be created (because repeated loading of classes is not allowed in the same Classloader, a new Classloader needs to be created) jspLoader = null; // Compile jsp into Servlet jspCompiler.compile(); jsw.setReload(true); jsw.setCompilationException(null); } catch (JasperException ex) { // Cache compilation exception jsw.setCompilationException(ex); if (options.getDevelopment() && options.getRecompileOnFail()) { // Force a recompilation attempt on next access jsw.setLastModificationTest(-1); } throw ex; } catch (FileNotFoundException fnfe) { // Re-throw to let caller handle this - will result in a 404 throw fnfe; } catch (Exception ex) { JasperException je = new JasperException( Localizer.getMessage("jsp.error.unable.compile"), ex); // Cache compilation exception jsw.setCompilationException(je); throw je; } } }
// JspServletWrapper.java public Servlet getServlet() throws ServletException { if (getReloadInternal() || theServlet == null) { synchronized (this) { if (getReloadInternal() || theServlet == null) { destroy(); final Servlet servlet; try { InstanceManager instanceManager = InstanceManagerFactory.getInstanceManager(config); // Create a servlet instance from the newly created JasperLoader servlet = (Servlet) instanceManager.newInstance(ctxt.getFQCN(), ctxt.getJspLoader()); } catch (Exception e) { Throwable t = ExceptionUtils .unwrapInvocationTargetException(e); ExceptionUtils.handleThrowable(t); throw new JasperException(t); } servlet.init(config); if (theServlet != null) { ctxt.getRuntimeContext().incrementJspReloadCount(); } theServlet = servlet; reload = false; } } } return theServlet; }
// JspCompilationContext.java public ClassLoader getJspLoader() { // If it is set to null, a new JasperLoader will be created here if( jspLoader == null ) { jspLoader = new JasperLoader (new URL[] {baseUrl}, getClassLoader(), rctxt.getPermissionCollection()); } return jspLoader; }
The overall process is as follows
- Regularly scan the jsp file directory and compare whether the modification time of the file has changed
- If the jsp file modification time is changed, set the corresponding Classloader (jsloader) to null
- Programming jsp files into Servlet classes
- Recreate a new Classload and load a new Servlet class
- Create a Servlet instance through the newly created Classloader