目标
理解热替换技术原理,代码实现
原理
java的类加载机制,主要分为4层

从上到下,各司其职,BootstrapClassLoader负责加载jre/lib下的核心api或-Xbootclasspath指定的jar,ExtClassLoader负责加载java.ext.dirs,即jre/bin/ext定义的扩展包,AppClassLoader负责加载你指定的web应用的class,最后你自己可以通过CustomClassLoader实现自己定义类加载器,可以用来加载你自己想要加载的class文件.
而热替换正是利用CustomClassLoader来实现自定类的加载实现的.
根据双亲委派模式, CustomClassLoader要加载某个类也会从上向下加载,而我们为了实现热替换,对于我们需要热替换的类,完全不需要遵循这种双亲委派模式.
我们只需要,在更新此类文件时,生成一个新的CustomClassLoader,重新加载对应的需要替换类,然后用他来替换原有CustomClassLoader加载的有类(tomcat的热部署也是类似这种机制).
demo实例
附上demo源码:git@gitee.com:kaiyang_taichi/hot_replacement_demo.git ,测试类里放的就是我们用来热升级的TestCall类

demo解释
- 定义HotDeployFactory类,用于加载自定义类,此工厂通过静态方法init进行初始化加载所需要的类, upgrade进行升级使用,此时会重新生成对应的currentClassLoader类,加载最新的Call类.
package com.example.hotdeploy.hot;
import java.io.*;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @Description: 需要热替换的服务类工厂
* @Author: kai.yang
* @Date: 2019-03-10 07:13
*/
public class HotDeployFactory {
/**
* 正在使用的classloader
*/
private static ClassLoader currentClassLoader;
private static String[] needHotClasses;
private static AtomicBoolean inited = new AtomicBoolean(false);
/**
* 默认加载根目录
*/
private static String javaRoot = "/Users/kai.yang/Desktop/java-test/hot/";
public static void init(String... needHotClasses) {
HotDeployFactory.inited.compareAndSet(false, true);
HotDeployFactory.currentClassLoader = new hotClassLoader(javaRoot, needHotClasses);
HotDeployFactory.needHotClasses = needHotClasses;
}
public static void upgrade() {
if (inited.get()){
HotDeployFactory.currentClassLoader = new hotClassLoader(HotDeployFactory.javaRoot, needHotClasses);
}else{
throw new RuntimeException("非法异常");
}
}
public static Object getObject(String className) throws Exception {
Class<?> aClass = currentClassLoader.loadClass(className);
return aClass.newInstance();
}
/**
* 自定义classloader类
*/
static class hotClassLoader extends ClassLoader {
private String baseDir;
private HashSet<String> myClasses = new HashSet<>();
public hotClassLoader(String baseDir, String[] classNames) {
super(null);
this.baseDir = baseDir;
loadDir(classNames);
}
private void loadDir(String[] classNames) {
if (classNames == null || classNames.length == 0) {
return;
} else {
for (String clazz : classNames) {
try {
StringBuilder sb = new StringBuilder(baseDir);
sb.append(clazz.replace(".", File.separator)).append(".class");
loadClazz(clazz, new File(sb.toString()));
myClasses.add(clazz);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private Class loadClazz(String name, File file) throws IOException {
InputStream inputStream = new FileInputStream(file);
byte[] raw = new byte[(int) file.length()];
inputStream.read(raw);
return defineClass(name, raw, 0, (int) file.length());
}
}
}
- 使用是定义在service层,因为没有实现父类加载器加载接口方法,demo中用反射调用此方法,可参考文末链接学习
public class FeeServiceImpl implements FeeService {
private static final String CallClass="TestCall";
static{
HotDeployFactory.init("TestCall");
}
@Override
public String callUserFee(BigDecimal amount, String feeRate) throws Exception {
Object object = HotDeployFactory.getObject(CallClass);
Method call = object.getClass().getMethod("call", BigDecimal.class, String.class);
Object invoke = call.invoke(object, amount, feeRate);
return invoke.toString();
}
}
- 定义TestCall类,因为我们现在只加载class文件,不处理java文件,所以手动编译java类
public class TestCall {
public String call(BigDecimal amount, String feeRule) {
return amount.add(new BigDecimal(feeRule)).toString();
}
}
编译后
kaiyangdeMacBook-Pro-2:hot kai.yang$ javac TestCall.java
kaiyangdeMacBook-Pro-2:hot kai.yang$ ls
TestCall.class TestCall.java
4.启动springboot服务,访问api
@Controller
@RequestMapping("/api")
public class BillController {
@Autowired
FeeService feeService;
@RequestMapping("/fee/{amount}/{feerate}")
@ResponseBody
public String test(@PathVariable String amount, @PathVariable String feerate) {
try {
return feeService.callUserFee(new BigDecimal(amount), feerate);
} catch (Exception e) {
e.printStackTrace();
}
return "fail";
}
@RequestMapping("/up")
@ResponseBody
public String upgrade() {
HotDeployFactory.upgrade();
return "ok";
}
}
访问url,http://localhost:8888/api/fee/11/10,显示结果:

- 手动修改TestCall.java类,改为
public class TestCall {
public String call(BigDecimal amount, String feeRule) {
return amount.multiply(new BigDecimal(feeRule)).toString();
}
}
重新编译java类

调用升级api:http://localhost:8888/api/up
5.重新调用http://localhost:8888/api/fee/11/10

好了,热替换完成了!
参考资料:https://www.ibm.com/developerworks/cn/java/j-lo-hotswapcls/index.html
网友评论