javaagent realizes hot update

       In the development of game servers, heat is a relatively common requirement. For example, if there is a bug online, if there is no hot update, then it needs to be repackaged and released to the server after the bug is fixed, and then restarted. Restarting is a huge loss for an online game.

       The implementation idea is as follows: use a thread to monitor the directory where class files are stored, read if there are class files, then reload them into memory, and delete these class files after completion. This implementation method can only modify the content of the method body, but it can already meet most needs.

Specific implementation code:

package com.hotswap;

import java.lang.instrument.Instrumentation;
import java.util.logging.ConsoleHandler;
import java.util.logging.Logger;


/**
 * class hot replacement
 *
 * <pre>
 * For when the application is running, after modifying the code, it will take effect without restarting the server.
 * Only the code in the modified method body will take effect, and other cases will not apply.
 * </pre>
 *
 * @author zyb
 *
 *@date May 26, 2017 at 5:33:15 PM
 */
public class HotSwapAgent {

	/** Default scan interval */
	private static final int DEFAULT_SCAN_INTERVAL = 500;

	private static final Logger log = Logger.getLogger(HotSwapAgent.class.getName());

	private final Instrumentation instrumentation;

	/** Class file path to monitor */
	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 parameter
 *
 * <pre>
 * At program startup, the command line specifies parameters.
 * example:
 * -javaagent:${path}/hotswap-agent-1.0.jar="classes=${classPath}, interval=1000, logLevel=FINE"
 * hotswap-agent-1.0.jar is the jar file of this project
 * classPath: used to specify the directory where the class files to be scanned are located
 * interval: time interval for scanning files
 * logLevel: log level, the level refers to JDK's java.util.logging.Level
 * </pre>
 *
 * @author zyb
 *
 *@date May 26, 2017 at 7:42:19 PM
 */
public class AgentArgs {

	/** The storage path of the class file*/
	private static final String CLASSES_PATH = "classPath";

	/** Scan interval time */
	private static final String SCAN_INTERVAL = "interval";
	
	/** log level */
	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;

/**
 * Hot replacement task
 *
 * @author zyb
 *
 *@date June 1, 2017 at 10:03:21 AM
 */
public class HotSwapMonitor implements Runnable {

	/** Directory to watch */
	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);
		}
	}

	/**
	 * Scan class files
	 */
	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)));
		}
	}

	/**
	 * reload 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 });
	}

	/**
	 * Whether the class file
	 *
	 * @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>

 

Create a new META-INF directory under src, and then create a new MANIFEST.MF file

Manifest-Version: 1.0
Agent-Class: com.hotswap.HotSwapAgent
Premain-Class: com.hotswap.HotSwapAgent
Can-Redefine-Classes: true

 
 Use ant to package the project into a jar and put it on the classpath. Then add -javaagent:${path}/hotswap-agent-1.0.jar="classes=${classPath}, interval=1000, logLevel=FINE" to the JVM startup parameters. ${xxx} represents a variable, replace it with yourself The directory is enough, classes is used to specify the directory where the class files are stored, and the class files to be hot-updated can be placed in this directory.

 

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=327039893&siteId=291194637