javaagent实现热更

       在游戏服务器的开发中,热更是一个比较常见的需求。比如说线上出现了一个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文件放到此目录下即可。

 

猜你喜欢

转载自rejoy.iteye.com/blog/2387833