生命不息,战斗不休。 --剑魔
当玩家因为逻辑bug导致其游戏数据错乱时,通常的做法是停服写SQL脚本修复或在重启服务器时写代码修复。在《Java游戏服热更新》一文中,我们已经提供了一种利用Java agent技术不停服修复玩家数据的方法,但是对于有些项目是打成jar包的情况下,如果采用新类修复玩家数据可能难以操作(原因见《Java游戏服热更新》),这篇将介绍另一种方法,即使用Groovy在线修复玩家内存数据,它是可以方便新增新类的。
百度百科中这样介绍groovy:Groovy是一种基于JVM(Java虚拟机)的敏捷开发语言,它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy 代码能够与 Java 代码很好地结合,也能用于扩展现有代码。由于其运行在 JVM 上的特性,Groovy也可以使用其他非Java语言编写的库。
通俗的讲就是,Java运行于JVM之上,Groovy也是运行于JVM之上的,在java项目中可以嵌入Groovy,利用Groovy做一些我们想做的事,Groovy与java项目的集成方式之一是可以用 Groovy 的 ClassLoader ,动态地加载一个脚本(新类)并执行它的行为,Groovy ClassLoader是一个定制的类装载器,负责编译.groovy或.java文件,最终生成java的class类文件并加载它。Groovy ClassLoader可以把它看成是一个自定义类加载器,如果把它挂在AppClassLoader下,那么我们java项目原有在AppClassLoader或其父加载器中的类对它来说是可见可用的,利用这点,足以让我们增加新类来修复玩家内存中错误数据了。
如果将AppClassLoader作为Groovy ClassLoader的父加载器,那么整个类加载器的层级关系为:
null // 即Bootstrap ClassLoader ↑ sun.misc.Launcher.ExtClassLoader // 即Extension ClassLoader ↑ sun.misc.Launcher.AppClassLoader // 即System ClassLoader ↑ org.codehaus.groovy.tools.RootLoader // 以下为User Custom ClassLoader ↑ groovy.lang.GroovyClassLoader ↑ groovy.lang.GroovyClassLoader.InnerLoader
groovy各个类加载器的作用为:
RootLoader:管理了Groovy的classpath,负责加载Groovy及其依赖的第三方库中的类,它不是使用双亲委派模型。
GroovyClassLoader:负责在运行时编译groovy源代码为Class的工作,从而使Groovy实现了将groovy源代码动态加载为Class的功能。
GroovyClassLoader.InnerLoader:Groovy脚本类的直接ClassLoader,它将加载工作委派给GroovyClassLoader,它的存在是为了支持不同源码里使用相同的类名,以及加载的类能顺利被GC。
(参考:《Groovy深入探索——Groovy的ClassLoader体系》)
Java agent是重定义class文件,但Groovy ClassLoader是可以直接使用.groovy或.java源文件的,而groovy语法完全兼容java语法,因此我们初始写groovy代码时,可以先写个.java类,然后直接改名为.groovy文件既可。等熟悉groovy语法后,groovy是可以写出比java更简洁的代码的。从上述可知,Groovy ClassLoader可以编译.groovy或.java文件,最终生成class文件并加载它们,利用这点,我们甚至可以在线撸功能,把写好的java文件上传到远程服务器,服务器Groovy ClassLoader编译并加载这些文件,就可以使我们的java项目不停服添加新功能了。
当知道Groovy ClassLoader作为一个类加载器可以直接编译加载.groovy或.java源文件后,它的使用逻辑就变得简单了,我们可以仿《Java游戏服热更新》逻辑,让它扫描某个目录下的.groovy或.java文件,然后编译并加载它们。
@Service
public class GroovyHotSwap implements Runnable, InitializingBean{
private ScheduledExecutorService executor = null;
private static File path = null;
private static Logger logger = LoggerFactory.getLogger(GroovyHotSwap.class);
@Override
public void afterPropertiesSet() throws Exception {
String grvpath = GameConfig.getInstance().getServerConfigPath();
if (!grvpath.endsWith("/")) {
grvpath += "/";
}
grvpath += "groovy";
path = new File(grvpath);
executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(this, 0, 3000, TimeUnit.MILLISECONDS);
}
@Override
public void run() {
try {
scanGroovyFile();
} catch (Exception e) {
logger.error("error", e);
}
}
public void scanGroovyFile() throws Exception {
File[] files = path.listFiles();
if (files != null && files.length > 0) {
boolean success = false;
long now = System.currentTimeMillis();
File[] bakFiles = files;
int fileNum = files.length;
for (int i = 0; i < fileNum; ++i) {
File file = bakFiles[i];
if (this.isJavaOrGroovyFile(file)) {
GroovyProcessor processor = GroovyUtil.processor(path.getAbsolutePath(), file.getName());
processor.process();
logger.info(String.format("Groovy Reload %s success", file.getPath()));
file.delete();
success = true;
}
}
if (success) {
logger.info(String.format("Groovy Reload success, cost time:%sms", System.currentTimeMillis() - now));
}
}
}
private boolean isJavaOrGroovyFile(File file) {
return file.getName().contains(".java") || file.getName().contains(".groovy");
}
}
我们也是隔几秒扫描某个路径文件夹下是否有.groovy或.java源文件,然后利用Groovy ClassLoader编译并加载它们:
public class GroovyUtil {
private static Map<String, Long> timesMap = new ConcurrentHashMap<>();
private static Map<String, Object> filesMap = new ConcurrentHashMap<>();
private static GroovyClassLoader groovyClassLoader = null;
static public GroovyProcessor processor(String grvpath, String name) throws Exception {
if (!grvpath.endsWith("/")) {
grvpath += "/";
}
if (!name.endsWith(".groovy") && !name.endsWith(".java")) {//支持groovy和java文件
name += ".groovy";
}
return grv(new File(grvpath + name));
}
static public <T> T grv(File file) throws Exception {
if (!file.exists()) {
return null;
}
String pathname = file.getPath();
Long lastModified = timesMap.get(pathname);
if (lastModified == null || lastModified != file.lastModified()) {
if (groovyClassLoader == null) {//避免每次新增类加载器
ClassLoader classLoader = ClassLoader.getSystemClassLoader();//这里我们把应用加载器作为groovy加载器的父加载器
groovyClassLoader = new GroovyClassLoader(classLoader);
}
Class c = groovyClassLoader.parseClass(file);
T script = (T) c.newInstance();
timesMap.put(pathname, file.lastModified());
filesMap.put(pathname, script);
}
return (T) filesMap.get(pathname);
}
}
所有加载的新类继承GroovyProcessor接口,以便统一处理:
public interface GroovyProcessor {
String process() throws Exception;
}
可以随便写个新类测试一下:
public class GroovyTest implements GroovyProcessor {
public static final int a = 50;
static{
System.out.println("a = " + a);
System.out.println(HeroHandler.class.getSimpleName() + " classLoader:" + HeroHandler.class.getClassLoader());
}
@Override
public String process() throws Exception {
System.out.println(GroovyTest.class.getSimpleName() + " classLoader:" + GroovyTest.class.getClassLoader());
//TODO 修复玩家内存数据逻辑
return "sucess";
}
}
最后打印如下:
a = 50
HeroHandler classLoader:sun.misc.Launcher$AppClassLoader@18b4aac2
GroovyTest classLoader:groovy.lang.GroovyClassLoader$InnerLoader@2e77e64a
如此,便实现了使用Groovy在线修复玩家内存数据。
网友评论