Java游戏服使用Groovy在线修复玩家内存数据

生命不息,战斗不休。 --剑魔

当玩家因为逻辑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在线修复玩家内存数据。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272