在游戏服务器的开发中,热更是一个比较常见的需求。比如说线上出现了一个Bug,如果没有热更的话,那么就需要在修复Bug之后重新打包发布到服务器上,然后重新启动。重启对于一个线上的游戏来说是损失比较大的。
实现思路如下:用一个线程监听存放class文件的目录,如果有class文件就读取,然后重新加载到内存中,完成之后再把这些class文件删除。此种实现方法只能修改方法体的内容,但是已经能满足大部分需求了。
具体实现代码:
package com.hotswap; import java.lang.instrument.Instrumentation; import java.util.logging.ConsoleHandler; import java.util.logging.Logger; /** * class热替换 * * <pre> * 用于在应用程序运行期间,修改代码后,不用重启服务器,就能生效。 * 只有在修改方法体里的代码才生效,其他情况都不适用。 * </pre> * * @author zyb * * @date 2017年5月26日 下午5:33:15 */ public class HotSwapAgent { /** 默认扫描间隔时间 */ private static final int DEFAULT_SCAN_INTERVAL = 500; private static final Logger log = Logger.getLogger(HotSwapAgent.class.getName()); private final Instrumentation instrumentation; /** 要监视的class文件路径 */ private final String classPath; public static void premain(String agentArgs, Instrumentation inst) { init(agentArgs, inst); } public static void agentmain(String agentArgs, Instrumentation inst) { init(agentArgs, inst); } private static void init(String agentArgs, Instrumentation inst) { AgentArgs args = new AgentArgs(agentArgs); if (!args.isValid()) { throw new RuntimeException("args is invalid"); } new HotSwapAgent(inst, args); } public HotSwapAgent(Instrumentation inst, AgentArgs args) { this.instrumentation = inst; this.classPath = args.getClassPath(); int scanInterval = DEFAULT_SCAN_INTERVAL; if (args.getInterval() > scanInterval) { scanInterval = args.getInterval(); } log.setUseParentHandlers(false); log.setLevel(args.getLogLevel()); ConsoleHandler consoleHandler = new ConsoleHandler(); consoleHandler.setLevel(args.getLogLevel()); log.addHandler(consoleHandler); HotSwapMonitor monitor = new HotSwapMonitor(instrumentation, classPath, args.getInterval()); monitor.start(); log.info("class path: " + classPath); log.info("scan interval (ms): " + scanInterval); log.info("log level: " + log.getLevel()); } }
package com.hotswap; import java.io.File; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; /** * Agent参数 * * <pre> * 在程序启动时,命令行指定参数。 * 例: * -javaagent:${path}/hotswap-agent-1.0.jar="classes=${classPath}, interval=1000, logLevel=FINE" * hotswap-agent-1.0.jar就是本项目的jar文件 * classPath:用于指定要扫描的class文件所在的目录 * interval:扫描文件的时间间隔 * logLevel:日志级别,级别参照JDK的java.util.logging.Level * </pre> * * @author zyb * * @date 2017年5月26日 下午7:42:19 */ public class AgentArgs { /** class文件的存放路径 */ private static final String CLASSES_PATH = "classPath"; /** 扫描间隔时间 */ private static final String SCAN_INTERVAL = "interval"; /** 日志级别 */ private static final String LOG_LEVEL = "logLevel"; private String classPath; private int interval; private Level logLevel; private AgentArgs() { this.classPath = null; this.interval = -1; this.logLevel = Level.WARNING; } public AgentArgs(String agentArgs) { this(); if (agentArgs != null && agentArgs.length() > 0) { if (agentArgs.indexOf("=") != -1) { initArgs(agentArgs); } } } public String getClassPath() { return classPath; } public Level getLogLevel() { return logLevel; } public int getInterval() { return interval; } private void initArgs(String agentArgs) { String[] args = agentArgs.split(","); Map<String, String> argsMap = new HashMap<String, String>(); for (String s : args) { String[] param = s.split("="); argsMap.put(param[0].trim(), param[1]); } if (argsMap.containsKey(CLASSES_PATH)) { setClassPath(argsMap.get(CLASSES_PATH)); } if (argsMap.containsKey(SCAN_INTERVAL)) { setInterval(argsMap.get(SCAN_INTERVAL)); } if (argsMap.containsKey(LOG_LEVEL)) { setLogLevel(argsMap.get(LOG_LEVEL)); } } public boolean isValid() { return classPath != null; } private void setClassPath(String classPath) { this.classPath = parsePath(classPath); } private void setLogLevel(String logLevel) { try { this.logLevel = Level.parse(logLevel.trim()); } catch (Exception e) { this.logLevel = Level.WARNING; } } private void setInterval(String interval) { try { this.interval = Integer.parseInt(interval.trim()); } catch (NumberFormatException e) { this.interval = -1; } } private static String parsePath(String path) { if (path != null) { String result = path.trim(); return result.endsWith(File.separator) ? result : result + File.separator; } return null; } }
package com.hotswap; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.lang.instrument.ClassDefinition; import java.lang.instrument.Instrumentation; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import jdk.internal.org.objectweb.asm.ClassReader; /** * 热替换任务 * * @author zyb * * @date 2017年6月1日 上午10:03:21 */ public class HotSwapMonitor implements Runnable { /** 要监视的目录 */ private String classPath; private Instrumentation instrumentation; private ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); private int interval; private static final Logger logger = Logger.getLogger(HotSwapMonitor.class.getName()); public HotSwapMonitor(Instrumentation instrumentation, String classPath, int interval) { this.instrumentation = instrumentation; this.classPath = classPath; this.interval = interval; } public void start() { logger.info("HotSwapMonitor start..."); executor.scheduleAtFixedRate(this, 0, interval, TimeUnit.MILLISECONDS); } @Override public void run() { try { scanClassFile(); } catch (Exception e) { logger.log(Level.SEVERE, "HotSwapMonitor error", e); } } /** * 扫描class文件 */ public void scanClassFile() throws Exception { File path = new File(classPath); File[] files = path.listFiles(); if (files == null) { return; } String classFilePath = null; boolean success = false; long now = System.currentTimeMillis(); for (File file : files) { if (!isClassFile(file)) { continue; } classFilePath = file.getPath(); reloadClass(classFilePath); logger.fine(String.format("Reload %s success", classFilePath)); file.delete(); success = true; } if (success) { logger.fine(String.format("Reload success, cost time:%sms", (System.currentTimeMillis() - now))); } } /** * 重新加载class * * @param classFilePath */ private void reloadClass(String classFilePath) throws Exception { File file = new File(classFilePath); byte[] buff = new byte[(int) file.length()]; DataInputStream in = new DataInputStream(new FileInputStream(file)); in.readFully(buff); in.close(); FileInputStream fis = new FileInputStream(file); ClassReader reader = new ClassReader(fis); fis.close(); ClassDefinition definition = new ClassDefinition(Class.forName(reader.getClassName()), buff); instrumentation.redefineClasses(new ClassDefinition[] { definition }); } /** * 是否class文件 * * @param file * @return */ private boolean isClassFile(File file) { return file.getName().contains(".class"); } }
build.xml
<project name="hotswap-agent" default="dist" basedir="."> <property name="version" value="1.0" /> <property name="jar.name" value="${ant.project.name}-${version}" /> <property name="classes.dir" value="bin" /> <property name="jar.dir" value="jar" /> <property name="javac.version" value="1.5" /> <target name="prepare"> <mkdir dir="${classes.dir}" /> <mkdir dir="${jar.dir}" /> </target> <target name="build" depends="prepare"> <javac target="${javac.version}" srcdir="src" debug="true" destdir="${classes.dir}"> <include name="**/*.java" /> <exclude name="**/com/hotswap/HotSwapAgent.java" /> </javac> </target> <target name="dist"> <antcall target="clean" /> <antcall target="build" /> <jar basedir="${classes.dir}" jarfile="${jar.dir}/${jar.name}.jar" manifest="src/META-INF/MANIFEST.MF" /> </target> <target name="clean"> <delete dir="${classes.dir}" /> <delete dir="${jar.dir}" /> </target> </project>
在src下新建META-INF目录,再新建MANIFEST.MF文件
Manifest-Version: 1.0 Agent-Class: com.hotswap.HotSwapAgent Premain-Class: com.hotswap.HotSwapAgent Can-Redefine-Classes: true
用ant将项目打包成jar,放到classpath下。然后在JVM启动参数中加入-javaagent:${path}/hotswap-agent-1.0.jar="classes=${classPath}, interval=1000, logLevel=FINE" ${xxx}表示一个变量,替换成自己的目录就行了,classes用于指定存放class文件的目录,将要热更的class文件放到此目录下即可。