尽管我们有了JAVA热更新1:Agent方式热更、JAVA热更新2:动态加载子类热更,能修复大部分线上的BUG,在项目上线之后,不可避免的会遇到出数据错乱的情况。之前的做法可能是提前写好一段代码,然后通过后台接口来进行调用,用以解决线上数据规整。但这种方式必须得提前写好规整逻辑,但不能覆盖所有情况。
因此我们就期望直接在线上执行一段代码,来进行我们业务数据的规整,结果就像JavaScript中的eval()函数一样,丢一串字符串进去,就可以像正常类一样执行,并且要能调用现有正在跑的代码。
例如:我们直接获取用户1234的信息,然后把用户年龄改为15,然后把修改后的值返回出来。
public class ChangeInfoTest { public static int changeUserInfo() { UserInfoVO info = UserInfoCache.getUserInfo(1234); info.setAge(15); return info.getAge(); }}
如果要实现上述功能,本质上也就是我们期望写一段代码然后后在应用上执行。其实JDK的底层本身就提供了动态加载类文件的能力,它就是JavaCompiler。
如果使用JavaCompiler动态加载类文件内容,那就需要经过下述流程:
如果代码片段格式正确,我们就通过Java编译器动态编译源代码得到了class。
// 以下仅为示例代码,具体实际可运行代码可参考文末的示例代码public class JavaCompilerUsage { public void compileTest() throws ClassNotFoundException { // 这里设置类名和源码,content必须是符合语法规范的类文件内容 String className = "", content = ""; // cl:是作为DynamicClassLoader的parent,一般是用当前应用的classloader // 主要作用是通过它来实现线上的代码对代码片段的可见性(双亲委派) ClassLoader cl = ClassLoader.getSystemClassLoader(); // 动态ClassLoader,主要用它来加载编译好的class文件 DynamicClassLoader dynamicClassLoader = new DynamicClassLoader(cl); JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); // 文件管理器 StandardJavaFileManager standardFileManager = javaCompiler.getStandardFileManager(null, null, null); JavaFileManager fileManager = new DynamicJavaFileManager(standardFileManager, dynamicClassLoader); DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>(); // 添加类名和对应的源码 List<JavaFileObject> compilationUnits = new ArrayList<>(new StringSource(className, content)); // 构建编译任务 JavaCompiler.CompilationTask task = javaCompiler.getTask(null, fileManager, collector, new ArrayList<>(), null, compilationUnits); // 执行编译过程 boolean result = task.call(); if (result) { // 通过dynamicClassLoader获取编译之后的类文件 Map<String, Class<?>> classes = dynamicClassLoader.getClasses(); } }}
得到class之后,我们想要调用class的方法,最直接的就是反射调用,相对就比较简单了,下面就是一段示例代码,直接调用类中第一个 public static 方法
// 以下仅为示例代码,具体实际可运行代码可参考文末的示例代码public class ClassCaller { private static Object call(Class<?> methtClass) throws Exception { Method[] declaredMethods = methtClass.getDeclaredMethods(); for (Method declaredMethod : declaredMethods) { // 调用类中第一个public static的方法 if (Modifier.isPublic(declaredMethod.getModifiers()) && Modifier.isStatic(declaredMethod.getModifiers())) { Object result = declaredMethod.invoke(null); LOG.error("JavaEval return: {}", JSON.toJSON(result)); return result; } } throw new RuntimeException("NO method is [public static], cannot eval"); }}
上面我们看到 new DynamicClassLoader(cl)的时候传递了一个参数:cl,这是DynamicClassLoader的parent,也就是它的父ClassLoader。 基于ClassLoader的双亲委派的原则,子ClassLoader是可以访问父ClassLoader里面的类的,所以我们写的代码是可以直接访问到线上的代码逻辑,而不会报类不存在。
关于ClassLoader的实现细节,我们在讲Arthas的原理时会详细再讲解。
https://github.com/cm4j/cm4j-all
JavaEvalUtilTest.evalTest1():直接运行java源码,运行即可计算1+2得到结果3
JavaEvalUtilTest.evalTest2():读取本地的一个类文件,并执行运行第一个public static 方法,结果与上一个方法同样
我们想要线上动态执行代码来进行业务调整,需要经过以下步骤:
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。