前言
我们服务器在线上跑着,发现了一些小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的小伙伴有所帮助。