Write a java hot load plug-in yourself

Posted by johnoc on Thu, 06 Jan 2022 03:27:39 +0100

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:

  1. A java object will contain a corresponding class pointer in heap memory
  2. Find the corresponding method in the corresponding class by class name + method name + method description (parameter and return value)
  3. 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
  4. 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

  1. 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).
  2. 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

  1. Load on jvm startup

    # Fat refers to any jar that can run fat
    java -javaagent:myagent.jar fat
  2. 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

  1. 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
  2. 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
  3. 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
  4. Get the bytecode file path from the parameter and get the byte stream
  5. 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

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

  1. Regularly scan the jsp file directory and compare whether the modification time of the file has changed
  2. If the jsp file modification time is changed, set the corresponding Classloader (jsloader) to null
  3. Programming jsp files into Servlet classes
  4. Recreate a new Classload and load a new Servlet class
  5. Create a Servlet instance through the newly created Classloader

Topics: Java Back-end