java程序如何实现hotswap

java程序如何实现hotswap

本文是根据《Java动态追踪技术探究》结合自己写的一些demo来对java热更新(hotswap)的一些见解。热更就是不需重启也修改程序。下面讲一下实现热更的几种方式:

1. classLoader重载类

java中的class文件都是通过classLoader加载到程序中的,正常情况下,classLoader只会加载一次,经过我的实验:如果多次加载同一个类会报如下错误:

Exception in thread "main" java.lang.LinkageError: loader (instance of  hotswap/HotSwapClassLoader): attempted  duplicate class definition for name: "hotswap/TestPrint"
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
    at hotswap.HotSwapClassLoader.loadByPath(HotSwapClassLoader.java:25)
    at hotswap.Main.main(Main.java:20)

所以我们需要每次new一个新的类加载器,强制程序重新加载类,下面是我写的demo:

package hotswap;

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Scanner;

public class Main {

    /**
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        Scanner scanner = new Scanner(System.in);
        URL url = new URL("file:E:\\testHotswap.jar");

        URLClassLoader classloader = null;

        while (true) {
            Thread.sleep(1000);
            classloader = new URLClassLoader(new URL[]{url});
            Class clazz = Class.forName("hotswap.TestPrint", true, classloader);
            Method method = clazz.getMethod("print");
            method.invoke(clazz.newInstance());
        }
    }

}

package hotswap;

public class TestPrint {


    /**
     * @param
     */
    public void print() {
        System.out.println("bbb");
    }


}

首先需要将建两个project分别这两个类放一个中,至于为啥,因为classloader依照有双亲委派机制,如果在一个project中会都会被父classloader加载。
接下来演示如何进行试验:

  1. 把TestPrint所在的那个project打包成jar,在idea中如此操作即可,把jar包输出目录设在E盘:


    设置输出目录
  2. 输出jar包


    输出jar包
  3. 启动Main 类的main函数:


    启动并查看输出

    可以看到每隔1000毫秒输出"aaa"字符串。

  4. 修改TestPrint.print()函数,重新打包jar,并查看Main 类的输出:


    修改后输出

试验结束,通过使用classloader重新加载类实现了hotswap功能。《Java动态追踪技术探究》文章中提

JSP文件修改过后,之所以能及时生效,是因为Web容器(Tomcat)会检查请求的JSP文件是否被更改过。如果发生过更改,那么就将JSP文件重新解析翻译成一个新的Sevlet类,并加载到JVM中。之后的请求,都会由这个新的Servet来处理。这里有个问题,根据Java的类加载机制,在同一个ClassLoader中,类是不允许重复的。为了绕开这个限制,Web容器每次都会创建一个新的ClassLoader实例,来加载新编译的Servlet类。之后的请求都会由这个新的Servlet来处理,这样就实现了新旧JSP的切换。

其实我的demo的做法与之相似,但是这种做法成本太高,意味着程序员要写适用的代码去重新加载类,不具备通用型,即对任意一java程序可以替换任意class。

2. Instrumentation

这是个java提供的不通过classloader修改class的方法。我也是刚知道它的存在。话不多说直接上代码。
首先我需要三个project,一个project1是平时正常运行的项目,第二个project2打包成javaagent(jar包),第三个project3将使用javaagent去修改project1中的类。

1. project1

package hotswap;

public class Main2 {

    /**
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {


        while (true) {
            Thread.sleep(1000);
           new Print().print();
        }
    }
}


package hotswap;

public class Print {

    public void print() {
        System.out.println("aaa");
    }
}

运行起来后程序将每隔1000ms输出一个字符串,模仿一个线上一直在运行的项目。

2. project2

import java.io.*;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;

public class Agent {

    private static byte[] getBytes(String filePath){

        byte[] buffer = null;
        try {
            File file = new File(filePath);
            FileInputStream fis = new FileInputStream(file);
            ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);
            byte[] b = new byte[1000];
            int n;
            while ((n = fis.read(b)) != -1) {
                bos.write(b, 0, n);
            }
            fis.close();
            bos.close();
            buffer = bos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return buffer;
    }


    public static void agentmain(String arg, Instrumentation instrumentation) {
        System.err.println("agentmain , args is " + arg);

        try {

            ClassDefinition classDefinition = new ClassDefinition(Class.forName("hotswap.Print"),getBytes(arg));
            instrumentation.redefineClasses(classDefinition);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这个类将打包成一个jar包作为javaagent被加载,被加载时候将执行· agentmain方法,agentmain有两个参数arginstrumentationargs是可自定义的参数,instrumentation是程序能重载类的关键,他提供两个关键方法redefineClassesretransformClasses,这里我使用了redefineClasses,他们的用途如下:

retransformClasses:对于已经加载的类重新进行转换处理,即会触发重新加载类定义,需要注意的是,新加载的类不能修改旧有的类声明,譬如不能增加属性、不能修改方法声明
redefineClasses:与如上类似,但不是重新进行转换处理,而是直接把处理结果(bytecode)直接给JVM
(第二次修改:本质上他们都起到了替换class的作用,而且替换效果是一样的:只不过retransformClasses的修改是可回退的,redefineClasses不可回退,因为retransformClasses相当于给字节码提供了一层“代理”当去掉代理后,可以拿到原来的字节码,而redefineClasses是直接替换了字节码。Java5中引入了重定义功能,Java6中引入了重传功能。我的猜测是重传是作为一种更通用的功能引入的,但是为了向后兼容,必须保留重定义。 再推荐一个开源的Java诊断工具arthas,其中有教程Arthas mc-redefine命令和Arthas mc-retransform命令,就是分别利用了这两种方式修改class)

redefineClasses使用需要传入参数ClassDefinition ,ClassDefinition 可以理解为类定义,他需要完整类名以及被编译的.class文件的byte[]数组来表示一个类。这里我传入的args就是.class文件的目录。

2. project3

使用javaagent去修改project1中的类。java调用javaagent有两种方式,一种是启动时加载(启动时加入参数如:-javaagent:D:\Redefine\out\artifacts\Redefine_jar\Redefine.jar=1111),另一种是运行时候加载,而这个project3就需要运行时加载。

package hotswap;

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;



public class AttachTest {
    public static void main(String... args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        VirtualMachine virtualMachine = VirtualMachine.attach("12760");
        virtualMachine.loadAgent("D:\\Redefine\\out\\artifacts\\Redefine_jar\\Redefine.jar","C:\\Users\\xuecm\\IdeaProjects\\untitled1\\out\\production\\untitled1\\hotswap\\Print.class");
        virtualMachine.detach();
    }
}

VirtualMachine 是jdk安装目录中tools.jar中的一个类。attach方法接受参数是要修改的java进程pid,loadAgent接受参数分别为project2打包成的jar的目录以及一个自定义参数。这里我第二个参数为Print.class的目录。结合project2,我希望重载Print.class。

接下来开始测试:
首先运行project1:


project1修改前

修改Print类并重新编译:


修改Print类

运行project2并查看project1的运行结果:


project1修改后

可以看到我的修改已经生效

你可能感兴趣的:(java程序如何实现hotswap)