一、 热更新入门级Demo,原文:[探秘Java热部署](https://www.jianshu.com/p/731bc8293365)
代码编写:
1)新建一个类AccountMain.java,执行替换ClassLoader 的操作。它的main()方法是一个间隔 20 秒的死循环,为什么间隔20秒呢?因为我们要在启动之后,修改类,并重新编译,因此需要20秒时间。代码解析:
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)测试步骤:
二、热更新进阶: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,保存,就可以看到控制台热部署成功了。把这个步骤拆分,实际的执行流程如下:
String path = "D:\\work\\MyWordSpace\\HotSwap\\target\\classes\\Main\\TargetImpl.class";
三、agentmain()实现热更新,转自:[探秘 Java 热部署三](https://www.jianshu.com/p/6096bfe19e41)
注意事项:
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 包配合字节码库,可以很方便的动态修改类,或者进行热部署。