一、 热更新入门级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)测试步骤:
- 执行AccountMain.java的main()方法,控制台打印出operation… 字符串,并开始死循环等待20秒
- 修改Account .java的输出内容,如operation…new
- 启动ReCompileAccount.java,重新编译Account .java
- 等待控制台打印 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,保存,就可以看到控制台热部署成功了。把这个步骤拆分,实际的执行流程如下:
- IDE打开终端Terminal,定位到当前项目目录
- 执行编译命令mvn compile
- 生成的class文件默认放在项目下的target目录
- 找到Main.java,修改指定class
String path = "D:\\work\\MyWordSpace\\HotSwap\\target\\classes\\Main\\TargetImpl.class";
- 执行Main.java的main()
- 等待方法执行成功,IDE切换回控制台,看到打印出了TargetImpl.java里的name值
- 此时再次打开终端Terminal,执行编译命令mvn compile
- Main.java里main()还在循环执行,IDE切换回控制台,看到打印出了新修改的name值
我是把name值从“测试1”改成了“测试22”,结果如下
三、agentmain()实现热更新,转自:[探秘 Java 热部署三](https://www.jianshu.com/p/6096bfe19e41)
注意事项:
- MANIFEST.MF文件的最后一行为空行,就是需要在原来代码后多敲一下回车
- Agent-Class为包名全路径
- 把class和MF文件打成jar需要注意outputpath,即输出目录
- 把jar绑定到目标VM,VM接受的参数为目标应用程序的进程id,通过Attach Tools API的VirtualMachine.attach方法绑定,并向其中加载代理jar。写绑定VM代码的时候 IDE 可能提示找不到 jar 包,这时打开你的jdk安装目录,在lib下找到tools.jar ,把它添加到项目的 classpath 中
- 修改字节码功能,这里提供一下代码,主要就是重写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 包配合字节码库,可以很方便的动态修改类,或者进行热部署。