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 timesMap = new ConcurrentHashMap<>();
    private static Map 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 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在线修复玩家内存数据)