笔者最近在研究如何在不停止已经运行程序的情况下,只要类发生一旦变更就能够自动重新加载新修改的类。之所以研究这个主要原因有以下几个:
- 不用停止程序就能够完成新逻辑变更
- 通用程序启动,启动类永远是一个,具体逻辑通过继承相应的接口实现
实现方案
Redis + 哈希(Hash)
将写好的Java代码编译成class文件,然后将字节码文件存到redis中,主程序启动的时候通过自定义classLoader加载相关的类。优点是存取快,缺点是缺少监听机制,需要定时监控redis值的变化,检测到变更触发重新加载类操作。
Redis + 发布订阅
实现方式同方式1,区别在于多了监听机制,redis的消息更新即可触发监听机制从而触发重新加载操作,缺点是发布订阅不能从头开始订阅,消息订阅只能消费一次,如果程序一旦崩溃是获取不到已经上传过的字节码文件。
kafka
kafka支持从头消费,也支持持久化,缺点同方式1,缺少监听机制,只能通过检测kafka值发生变化来重新加载类
zookeeper
zookeeper方式优于上述三种方案,主要是支持持久化,同样有相应的监听机制,节点信息变更能触发相应的回调函数,我们在回调函数里面重新执行加载类的操作即可,这也是笔者选择zookeeper实现的主要原因。
实现过程
- 将编译好的class文件上传到zookeeper
- 自定义类加载
- 主程序根据配置信息动态加载相应的类
- 修改回调函数重新执行加载操作
相关代码
- 上传字节码文件到zookeeper
public static void upload(ZkClient zk, String path, byte[] data){ String[] paths = path.split("/"); String realPath = ""; for(String tmpPath : paths){ if(tmpPath.isEmpty()) continue; realPath += "/" + tmpPath; if(!zk.exists(realPath)){ zk.create(path, "", CreateMode.PERSISTENT); log.info("create node success : {}",path); } } if(!zk.exists(path)){ zk.create(path, Base64.getEncoder().encodeToString(data), CreateMode.PERSISTENT); log.info("create node success : {}",path); }else{ zk.writeData(path, Base64.getEncoder().encodeToString(data)); log.info("writeData success : {}",path); } }
注意:上传的字节码,笔者遇到使用zookeeper默认序列化的方式会出错,自定义序列化的方式同样也会出现问题,string跟byte转换的过程会有点问题,笔者尝试了Base64编码解码最后没有问题了,有兴趣的可以尝试一下
相关的单元测试类,涉及到路径相关的按照自己真实的修改即可
package com.monica.test;
import com.monica.zk.ZKUtils;
import org.I0Itec.zkclient.ZkClient;
import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
public class ZkTest {
private ZkClient zkClient;
@Before
public void init(){
String connStr = "localhost:2181";
zkClient = new ZkClient(connStr);
}
@Test
public void testCreateNode() throws IOException, InterruptedException {
String userPath = "/code/java/test/firstProcess";
byte[] data = getClassByte("/home/monica/study/dynamic-load-class-parent/dynamic-logic/target/classes/com/monica/dynamic/FirstProcess.class");
ZKUtils.upload(zkClient,userPath,data);
}
@Test
public void testDeleteNode() throws IOException, InterruptedException {
String userPath = "/code/java/test/firstProcess";
zkClient.delete(userPath);
}
private static byte[] getClassByte(String path) throws IOException {
File file = new File(path);
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);
while (true) {
int i = fc.read(by);
if (i == 0 || i == -1) {
break;
}
by.flip();
wbc.write(by);
by.clear();
}
fis.close();
return baos.toByteArray();
}
}
主程序启动类
package com.monica.main.process;
import com.monica.dynamic.api.ProcessApi;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import java.util.Base64;
public class MainProcess {
private static volatile ProcessApi process = null;
public static void main(String[] args) throws Exception {
String connStr = "localhost:2181";
ZkClient zk = new ZkClient(connStr);
String path = "/code/java/test/firstProcess";
// 注册【数据】事件
zk.subscribeDataChanges(path, new IZkDataListener() {
@Override
public void handleDataDeleted(String arg0) throws Exception {
System.err.println("数据删除:" + path);
System.exit(-1);
}
@Override
public void handleDataChange(String arg0, Object arg1) throws Exception {
System.err.println("数据修改:" + path );
synchronized (process) {
String data = zk.readData(path);
MyClassLoader myClassLoader = new MyClassLoader(Base64.getDecoder().decode(data));
Class clazz = myClassLoader.loadClass("com.monica.dynamic.FirstProcess");
process = (ProcessApi) clazz.newInstance();
myClassLoader = null;
}
}
});
String data = zk.readData(path);
MyClassLoader myClassLoader = new MyClassLoader(Base64.getDecoder().decode(data));
Class clazz = myClassLoader.loadClass("com.monica.dynamic.FirstProcess");
process = (ProcessApi) clazz.newInstance();
myClassLoader = null;
while (true) {
process.doProcess();
Thread.sleep(3000);
}
}
}
笔者这里将源码也上传方便用到的同学研究,下载不了可能得等待审核通过才行。可以尝试运行住程序,一边修改相关类重新上传验证一下,笔者这里也验证是可以正常加载的,如有表述不对的地方欢迎指正!