热更新Demo:java底层及agentmain()三种实现方式

一、 热更新入门级Demo,原文:[探秘Java热部署](https://www.jianshu.com/p/731bc8293365)
热更新思路

代码编写:
1)新建一个类AccountMain.java,执行替换ClassLoader 的操作。它的main()方法是一个间隔 20 秒的死循环,为什么间隔20秒呢?因为我们要在启动之后,修改类,并重新编译,因此需要20秒时间。代码解析:

  • 创建一个自定义的 ClassLoader 对象,加载类的步骤不遵守双亲委派模型,而是直接加载
  • 使用刚刚创建的类加载器加载指定的类
  • 得到刚刚的Class 对象,使用反射创建对象,并调用对象的 operation 方法
public class AccountMain {

    public static void main(String[] args)
            throws ClassNotFoundException, InterruptedException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {

        while (true) {
            ClassLoader loader = new ClassLoader() {
                @Override
                public Class<?> loadClass(String name) throws ClassNotFoundException {
                    try {
                        // Class clazz = loader.loadClass("hotSwap.Account");
                        System.out.println("name:" + name);          // hotSwap.Account
                        String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                        System.out.println("fileName:" + fileName);  // Account.class
                        InputStream is = getClass().getResourceAsStream(fileName);
                        if (is == null) {
                            return super.loadClass(name);
                        }

                        byte[] b = new byte[is.available()];

                        is.read(b);
                        return defineClass(name, b, 0, b.length);

                    } catch (IOException e) {
                        e.printStackTrace();
                        throw new ClassNotFoundException(name);
                    }
                }
            };

            Class clazz = loader.loadClass("hotSwap.Account");
            Object account = clazz.newInstance();
            account.getClass().getMethod("operation", new Class[]{}).invoke(account);
            Thread.sleep(20000);
        }
    }

}

2)新建一个测试类Account .java,用于随时修改并编译。它的main()方法的作用是打印 operation…字符串。

public class Account {
  public void operation() {
    System.out.println("operation...");
    try {
      Thread.sleep(10);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

3)再新建一个执行类ReCompileAccount.java,用于运行修改后的Account.java,因为运行后肯定重新编译了,可以省掉cmd命令行手动javac编译的操作

class ReCompileAccount {

  public static void main(String[] args) {
    new Account().operation();
  }
}

4)测试步骤:

  1. 执行AccountMain.java的main()方法,控制台打印出operation… 字符串,并开始死循环等待20秒
  2. 修改Account .java的输出内容,如operation…new
  3. 启动ReCompileAccount.java,重新编译Account .java
  4. 等待控制台打印 AccountMain.java的main()输出内容
    结果如下
    输出结果

二、热更新进阶:java底层实现,原文:[java实现热更新](https://huangyunbin.iteye.com/blog/2179267)

思路:使用ClassLoader加载新的类,替换掉旧的对象。
注意最好使用接口,我们只是加载实现类,接口类一直使用旧的ClassLoader,这样就不会存在类型转换的报错
代码编写:我这里是先把Main.java建了,难点在于这个类,别的类只是修饰
1)新建Main.java,执行替换ClassLoader 的操作。这里面的代码是调用cmd执行代码编译

package Main;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

public class Main {
    static Target obj = new TargetImpl();

    public static void main(String[] args) throws Exception {
        while (true) {
           // 重新编译,热部署自动实现
           Runtime runtime=Runtime.getRuntime();
            Process process=null;
            try {
               process= runtime.exec("cmd /c   cd D:\\work\\MyWordSpace\\HotSwap && mvn compile");
               process.waitFor();
               process.destroy();
            } catch (IOException e) {
                e.printStackTrace();
            }
            String path = "D:\\work\\MyWordSpace\\HotSwap\\target\\classes\\Main\\TargetImpl.class";
            byte[] b = getBytes(path);
            Class c = new DynamicClassLoader().findClass(b);
            obj = (Target) c.newInstance();
            System.err.println(obj.name());
            TimeUnit.SECONDS.sleep(5);
        }
    }

    // 从本地读取文件
    private static byte[] getBytes(String filename) throws IOException {
        File file = new File(filename);
        long len = file.length();
        byte raw[] = new byte[(int) len];
        FileInputStream fin = new FileInputStream(file);
        fin.read(raw);
        fin.close();
        return raw;
    }
}

// 重新编译方式二:调用maven接口,热部署自动实现,这个方式的解释和所需jar,请点击这里

InvocationRequest request = new DefaultInvocationRequest();
            request.setPomFile( new File( "pom.xml" ) );
            request.setGoals( Collections.singletonList( "compile" ) );

            Invoker invoker = new DefaultInvoker();
            invoker.setMavenHome(new File("D:/work/environment/Maven/apache-maven-3.3.9"));

            invoker.setLogger(new PrintStreamLogger(System.err,  InvokerLogger.ERROR){

            } );
            invoker.setOutputHandler(new InvocationOutputHandler() {
                @Override
                public void consumeLine(String s) throws IOException {

                }
            });
            try{
                invoker.execute( request )
            }catch (MavenInvocationException e) {
                e.printStackTrace();
            }

2)新建DynamicClassLoader.java

package Main;
public class DynamicClassLoader extends ClassLoader {
    public Class<?> findClass(byte[] b) throws ClassNotFoundException {
        return defineClass(null, b, 0, b.length);
    }
}

3)新建接口Target

package Main;
public interface Target {
    String name();
}

4)新建TargetImpl.java

package Main;
public class TargetImpl implements Target {
    public String name() {
        return "测试1";
    }
}

测试步骤:运行Main.java的main(),然后修改TargetImpl.java里的name值,修改后按Ctrl+S,保存,就可以看到控制台热部署成功了。把这个步骤拆分,实际的执行流程如下:

  1. IDE打开终端Terminal,定位到当前项目目录
  2. 执行编译命令mvn compile
  3. 生成的class文件默认放在项目下的target目录
  4. 找到Main.java,修改指定class
String path = "D:\\work\\MyWordSpace\\HotSwap\\target\\classes\\Main\\TargetImpl.class";
  1. 执行Main.java的main()
  2. 等待方法执行成功,IDE切换回控制台,看到打印出了TargetImpl.java里的name值
  3. 此时再次打开终端Terminal,执行编译命令mvn compile
  4. Main.java里main()还在循环执行,IDE切换回控制台,看到打印出了新修改的name值
    我是把name值从“测试1”改成了“测试22”,结果如下
    控制台输出内容

三、agentmain()实现热更新,转自:[探秘 Java 热部署三](https://www.jianshu.com/p/6096bfe19e41)

注意事项:

  1. MANIFEST.MF文件的最后一行为空行,就是需要在原来代码后多敲一下回车
  2. Agent-Class为包名全路径
  3. 把class和MF文件打成jar需要注意outputpath,即输出目录
  4. 把jar绑定到目标VM,VM接受的参数为目标应用程序的进程id,通过Attach Tools API的VirtualMachine.attach方法绑定,并向其中加载代理jar。写绑定VM代码的时候 IDE 可能提示找不到 jar 包,这时打开你的jdk安装目录,在lib下找到tools.jar ,把它添加到项目的 classpath 中
  5. 修改字节码功能,这里提供一下代码,主要就是重写agentmain(),将手动编译修改的java类,生成的class文件放到指定位置
public static void agentmain(String args, Instrumentation inst) throws Exception {
		System.out.println("Agent start");
		Class<?>[] allClass = inst.getAllLoadedClasses();
		for (Class<?> c : allClass) {
			if (c.getName().endsWith("TestHotUpdate")) {
				String pathname = "D:\\work\\MyWordSpace\\HotSwap\\src\\main\\HotSwap.class";
				File file = new File(pathname);
				try {
					byte[] bytes = fileToBytes(file);
					System.out.println("size:" + bytes.length);
					System.out.println("replace file");
					ClassDefinition classDefinition = new ClassDefinition(c, bytes);
					inst.redefineClasses(classDefinition);
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		System.out.println("success");
	}
	
	// class to byte[]
	public static byte[] fileToBytes(File file) throws IOException {
		FileInputStream in = new FileInputStream(file);
		byte[] bytes = new byte[in.available()];
		in.read(bytes);
		in.close();
		return bytes;
	}

agentmain 可以在目标程序丝毫不改动,甚至连启动参数都不加的情况下修改类,并且是运行后修改,而且不重新创建类加载器,其主要得益于 JVM 底层的对类的重定义。热部署,其实就是动态或者说运行时修改类,大的方向说有2种方式:
1、使用 agentmain,不需要重新创建类加载器,可直接修改类,但是有很多限制。
2、使用 premain 可以在类第一次加载之前修改,加载之后修改需要重新创建类加载器。或者在自定义的类加载器种修改,但这种方式比较耦合。

无论是哪种,都需要字节码修改的库,比如ASM,javassist ,cglib 等,很多。总之,通过java.lang.instrument 包配合字节码库,可以很方便的动态修改类,或者进行热部署。

猜你喜欢

转载自blog.csdn.net/qq_36688143/article/details/85099374