Java热更

前言

    我们服务器在线上跑着,发现了一些小bug,需要修改了更新到线上去,此时我们应该怎么做,不能动不动就停服更新,毕竟线上那么多玩家和用户,此时我们就需要进行热更。本文我就和大家一起来探讨怎么进行热更。

    这里涉及到两个工程:javaagent工程和项目工程,我画了一张大概的架构图:

​​​​​​​

 一.javaagent工程

    javaagent工程主要负责提供热更的接口,java文件就一个HotSwapAgent.java,直接上代码:

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HotSwapAgent {
    private static final Logger logger = LoggerFactory.getLogger(HotSwapAgent.class);
    private static Instrumentation inst;
    //需要热更的class文件所在文件夹
    private static final String PATCH_DIR = "patches";

    public static void premain(String args, Instrumentation inst) {
        HotSwapAgent.inst = inst;
    }

    /**
     * 对外提供的热更接口
     * @param cls 要热更的类的Class对象
     * @param file 要热更的类的实际路径 也就是class文件
     * @param serverId 游戏服对应的服务器id
     */
    public static String reload(Class<?> cls, File file, int serverId) throws IOException, ClassNotFoundException, UnmodifiableClassException {
        if (inst == null) {
            return "该应用没有添加此特性, 请检查启动参数 javaagent";
        } else {
            byte[] code = loadBytesFromClassFile(file);
            if (code == null) {
                throw new IOException("FileNotFoundException " + file.getName());
            } else {
                ClassDefinition def = new ClassDefinition(cls, code);
                inst.redefineClasses(new ClassDefinition[]{def});
                //TODO 此处可以把MD5码打出来
                return "[hot swap v2] " + cls.getName() + " reloaded zone " + serverId;
            }
        }
    }

    /**
     * 将class文件转成byte数组
     */
    private static byte[] loadBytesFromClassFile(File classFile) throws IOException {
        byte[] buffer = new byte[(int) classFile.length()];
        FileInputStream fis = new FileInputStream(classFile);
        BufferedInputStream bis = new BufferedInputStream(fis);
        try {
            bis.read(buffer);
        } catch (IOException e) {
            throw e;
        } finally {
            try {
                bis.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return buffer;
    }

    /**
     * 生成全限定类名和文件路径的对应关系
     */
    public static Map<String, String> getFullClassNameFilePathMap(String packageName) {
        Map<String, String> map = new HashMap();
        Collection<File> patchClassFiles = FileUtils.listFiles(new File(PATCH_DIR), new String[]{"class"}, true);
        Iterator iterator = patchClassFiles.iterator();
        while (iterator.hasNext()) {
            File patchFile = (File) iterator.next();
            String filePath = patchFile.getPath();
            String fullClassName = getFullClassName(filePath);
            if (fullClassName.startsWith(packageName)) {
                map.put(fullClassName, filePath);
                logger.info("[hot swap] find class file {} in {}.", fullClassName, "patches");
            }
        }
        return map;
    }

    /**
     * 将文件路径转换成全限定类名
     */
    private static String getFullClassName(String filePath) {
        int start = filePath.indexOf(File.separator) + 1;
        int end = filePath.lastIndexOf(46);
        String fullClassPath = filePath.substring(start, end);
        return fullClassPath.replace(File.separator, ".");
    }
}

     将javaagent工程打包成HotSwapAgent-1.0-SNAPSHOT.jar,该jar对外提供了接口reload函数进行热更。 注意:打出来的jar中必须有MANIFEST.MF文件内容如下:

Manifest-Version: 1.0
Premain-Class: com.agent.HotSwapAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

二.项目工程

    模拟项目工程的代码,里面嵌入了jetty:    

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;

public class WorkingProject {
    public static void main(String[] args) throws Exception {
        //运行线上业务代码
        WorkingProject workingProject = new WorkingProject();
        workingProject.working();

        //Jetty嵌入
        startJettyServer();
    }

    /**
     * Jetty嵌入
     * 实际开发中,jettyServer要单提出一个类来管理
     * 这里为了方便演示,将jetty和项目工程都写在这个类里
     */
    public static void startJettyServer() {
        try {
            Server server = new Server(8080);
            ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
            context.setContextPath("/");
            server.setHandler(context);
            //管理Servlet
            context.addServlet(new ServletHolder(new HotSwapServlet()), "/hotSwap");
            server.start();
            server.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 模拟线上运行的业务代码
     */
    public void working() {
        Thread thread = new Thread(() -> {
            while (true) {
                doSomeWork();
            }
        });
        thread.start();
    }

    public void doSomeWork() {
        try {
            Thread.sleep(3000);
            String desc = "热更前";
            System.out.println(desc);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

    HotSwapServlet代码

import com.agent.HotSwapAgent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.lang.instrument.UnmodifiableClassException;
import java.util.Map;
import javax.servlet.http.*;

public class HotSwapServlet extends HttpServlet {
    private static final Logger log = LoggerFactory.getLogger(HotSwapServlet.class);
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws  IOException {
        //假定我们是1服进行热更
        //这些参数其实都可以通过HttpServletRequest传进来
        int serverId = 1;
        //包名
        String packageName = "com.newbie";
        //热更
        String res = HotSwapServlet.reloadClass(serverId, packageName);

        response.setContentType("text/plain");
        response.setStatus(HttpServletResponse.SC_OK);
        response.getWriter().println(res);
    }

    /**
     * 遍历包,将包中所有的class进行热更
     * 这些代码实际开发中需要提出去,不能放在Servlet代码中
     * 为了方便演示,就先放在这里了
     */
    public static String reloadClass(int serverId, String packageName) {
        String res = "";
        try {
            /* full class name -> file path */
            Map<String, String> files = HotSwapAgent.getFullClassNameFilePathMap(packageName);
            if (files.isEmpty()) {
                log.info("cant find class files in patchs");
            } else {
                for (Map.Entry<String, String> en : files.entrySet()) {
                    String className = en.getKey();
                    File classFile = new File(en.getValue());
                    if (classFile.exists()) {
                        res += reload(classFile, className, serverId) + "\r\n";
                    }
                }
            }
        } catch (Exception e) {
            res += e.getMessage();
        }
        log.info(res);
        return res;
    }

    /**
     * 调用javaagent工程中的HotSwapAgent.reload进行热更
     * @param file class文件所在路径
     * @param className 类的全限定名
     * @param serverId 服务器id
     */
    private static String reload(File file, String className, int serverId) {
        String msg = "";
        try {
            Class<?> cls = Class.forName(className);
            //调用javaagent工程中的HotSwapAgent.reload进行热更
            return HotSwapAgent.reload(cls, file, serverId);
        } catch (IOException e) {
            msg = "[hot swap] load class file error : " + file.getName();
            log.error(msg, e);
        } catch (UnmodifiableClassException e) {
            msg = "[hot swap] class is unmodifiable :" + className;
            log.error(msg, e);
        } catch (ClassNotFoundException e) {
            msg = "[hot swap] class can't found :" + className;
            log.error(msg, e);
        }
        return msg + " zone " + serverId;
    }

 三.测试热更

    操作步骤如下    

    1.将项目工程WorkingProject运行起来

      启动时一定记得添加jvm启动参数-javaagent:lib/HotSwapAgent-1.0-SNAPSHOT.jar,如未添加,后续的热更不会成功。

    控制台输出如下:

    2.修改WorkingProject.java代码

    将【String desc = "热更前"】;修改成【String desc = "热更后"】;

    3.重新编译

    将编译后的WorkingProject.class文件复制到patches路径下(实际开发中,编译和复制文件都可以通过脚本来实现)

    4.执行url进行热更

     http://127.0.0.1:8080/hotSwap

    通过控制台最新输出可以看出,我们已经成功的对运行中的代码进行了热更。

总结

    本文通过Instrumentation以及结合Jetty嵌入,实现了对线上运行中代码热更,希望能对学习java的小伙伴有所帮助。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值