美文网首页Java游戏服务器编程
Java游戏服使用Groovy在线修复玩家内存数据

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

作者: 小圣996 | 来源:发表于2020-05-09 10:30 被阅读0次

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

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

    相关文章

      网友评论

        本文标题:Java游戏服使用Groovy在线修复玩家内存数据

        本文链接:https://www.haomeiwen.com/subject/cqngwhtx.html