手写一个代码热部署

最近发现javassist中一个十分好玩的类,HotSwapper,可以动态更改运行中的类,于是便想写一个代码热更新的小工具

热更新效果(不仅热更修改部分还保留了原来的变量值)
在这里插入图片描述
Test1逻辑
在这里插入图片描述
编写过程我又突发奇想与原来的MakeR结合自动更新资源文件
MakeR:https://blog.csdn.net/weixin_44598449/article/details/118309945
自动更新资源文件效果
在这里插入图片描述
对指定资源目录的文件进行增删会自动更新R类
更新后的R类
在这里插入图片描述
HotSwapper使用起来也非常简单

// 必须指定虚拟机参数:-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
HotSwapper swapper=new HotSwapper(8000);// 这里的端口要与上面配置的address一致
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//编译代码
int result = compiler.run(null, null, null,"E:/IDEA/MyTest3/src/main/java/part15/TestQ.java");
System.out.println("result=" + result);
//获取字节码并热加载
byte[] bytes = Files.readAllBytes(Paths.get("E:\\IDEA\\MyTest3\\src\\main\\java\\part15\\TestQ.class"));
swapper.reload(TestQ.class.getName(),bytes);

上面的程序还依赖 JDK\lib\tools.jar 这个jar包,可以直接引入jar也可以通过依赖

<dependency>
    <groupId>com.sun</groupId>
    <artifactId>tools</artifactId>
    <version>1.8</version>
    <scope>system</scope><!--使用环境变量-->
    <systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>

代码整体来说还是非常简单的
基本就是通过调用updateCode更新代码,通过代码文件的时间戳判断是否需要更新

@Slf4j
public class CodeUpdate {

    private Path basePath;
    private Map<String,Long> timeStampMap;
    private JavaCompiler compiler;
    private HotSwapper swapper;
    private List<UpdateListenner> listenners;
    private int updateCount;

    public CodeUpdate(String basePackage, int port) {
        initBasePath(basePackage);
        initTimeStampMap();
        initOther(port);
    }

    //  -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
    private void initOther(int port){
        compiler = ToolProvider.getSystemJavaCompiler();
        try {
            swapper=new HotSwapper(port);
            log.info("已成功连接到{}端口",port);
        } catch (IOException|IllegalConnectorArgumentsException e) {
            e.printStackTrace();
            log.error("连接失败请配置虚拟机参数:-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address={}",port);
        }
        listenners=new ArrayList<>();
    }

    public void addListenner(UpdateListenner listenner){
        listenners.add(listenner);
    }

    public void removeListenner(UpdateListenner listenner){
        listenners.remove(listenner);
    }

    private void initBasePath(String basePackage) {
        StringBuilder builder=new StringBuilder();
        builder.append(System.getProperty("user.dir")).append("/src/main/java");
        String[] items = basePackage.split("\\.");
        for (String item : items) {
            builder.append('/').append(item);
        }
        basePath=Paths.get(builder.toString());
    }

    @SneakyThrows
    private void initTimeStampMap(){
        timeStampMap=new LinkedHashMap<>();
        Files.walk(basePath).forEach(path -> {
            File file = path.toFile();
            if(file.getName().endsWith(".java")){
                timeStampMap.put(file.toString(),file.lastModified());
            }
        });
    }

    @SneakyThrows
    public void updateCode(){
        updateCount=0;
        Files.walk(basePath).forEach(path -> {
            File file = path.toFile();
            if(file.getName().endsWith(".java")){
                if(timeStampMap.getOrDefault(file.toString(),0L)<file.lastModified()){
                    timeStampMap.put(file.toString(),file.lastModified());
                    updateCode(file);
                    updateCount++;
                }
            }
        });
        listenners.forEach(listenner -> listenner.update(updateCount));
    }

    private void updateCode(File file) {
        int result = compiler.run(null, null, null,file.getAbsolutePath());
        String className = getClassName(file);
        if(result==0){
            byte[] bytes=getAndDelete(file);
            swapper.reload(className,bytes);
            log.info("热更新{}成功!",className);
        }else {
            log.error("编译{}失败!",className);
        }
    }

    private String getClassName(File file) {
        String path = file.toString();
        String classPath = path.substring(path.indexOf("\\java\\") + 6, path.lastIndexOf('.'));
        return classPath.replaceAll("\\\\",".");
    }

    @SneakyThrows
    private byte[] getAndDelete(File file) {
        String name = file.getName();
        file=new File(file.getParent(),name.substring(0,name.lastIndexOf('.'))+".class");
        byte[] bytes = Files.readAllBytes(file.toPath());
        file.delete();
        return bytes;
    }

}

剩下的就是代码更新并调用updateCode方法,实际我本人更喜欢,把updateCode方法绑定到快捷键,手动应用更新
这里使用WatchService监听文件更改
简单使用

WatchService watchService = FileSystems.getDefault().newWatchService();//获取观察服务
//注册文件夹(只能注册文件夹)并指定监听事件
Paths.get(triggerDir).register(watchService,StandardWatchEventKinds.ENTRY_MODIFY);
/*  支持的文件事件
StandardWatchEventKinds.OVERFLOW:事件丢失或失去
StandardWatchEventKinds.ENTRY_CREATE:目录内实体创建或本目录重命名
StandardWatchEventKinds.ENTRY_MODIFY:目录内实体修改
StandardWatchEventKinds.ENTRY_DELETE:目录内实体删除或重命名
*/
try {
    while (!Thread.interrupted()){
        WatchKey poll = watchService.take();//阻塞获取事件
        //watchService.poll();//非阻塞获取 没有事件返回null
        //watchService.poll(12, TimeUnit.SECONDS);//指定有超时时间的
        if(fileChange!=null)fileChange.change(poll.pollEvents());//必须pollEvents否则事件不消失
        poll.reset();//重置方便复用
    }
}finally {
    watchService.close();//关闭服务
}

具体封装为了FileTrigger监听文件更改

public class FileTrigger extends Thread{

    private String triggerDir;
    private WatchService watchService;
    private FileChange fileChange;

    @SneakyThrows
    public FileTrigger(String triggerDir) {
        this.triggerDir = triggerDir;
        watchService = FileSystems.getDefault().newWatchService();//获取观察服务
    }

    public void setFileChange(FileChange fileChange) {
        this.fileChange = fileChange;
    }

    @SneakyThrows
    @Override
    public void run() {
        //注册文件夹(只能注册文件夹)并指定监听事件
        Paths.get(triggerDir).register(watchService,StandardWatchEventKinds.ENTRY_MODIFY,
                StandardWatchEventKinds.ENTRY_DELETE);
        try {
            while (!Thread.interrupted()){
                WatchKey poll = watchService.take();//阻塞获取事件
                if(fileChange!=null)fileChange.change(poll.pollEvents());//必须pollEvents否则事件不消失
                poll.reset();//重置方便复用
            }
        }finally {
            watchService.close();
        }

    }

}

做完这些我便想,监控文件更新自动更新R不就可以想安卓一样了吗
资源R类:https://blog.csdn.net/weixin_44598449/article/details/118309945
实际这里没有写什么代码直接修改的原来的MakeR使其更适应多次创建使用,并结合FileTrigger就完成了
实际动态替换字节码后原来的类文件并未直接释放
下面是我每秒热更100次方法区的内存占用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uhU4Uwp4-1631950563802)(E:\文本学习文件\markdown\image\image-20210917085144145.png)]
基本只需5s方法区就从2M达到了30M,然后便开始了FullGC,但是正常使用过程中肯定没有每秒热更100次的需求

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值