前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >jAVA不停服执行代码

jAVA不停服执行代码

作者头像
Qwe7
发布2022-03-23 08:13:10
4880
发布2022-03-23 08:13:10
举报
文章被收录于专栏:网络收集

尽管我们有了JAVA热更新1:Agent方式热更JAVA热更新2:动态加载子类热更,能修复大部分线上的BUG,在项目上线之后,不可避免的会遇到出数据错乱的情况。之前的做法可能是提前写好一段代码,然后通过后台接口来进行调用,用以解决线上数据规整。但这种方式必须得提前写好规整逻辑,但不能覆盖所有情况。

因此我们就期望直接在线上执行一段代码,来进行我们业务数据的规整,结果就像JavaScript中的eval()函数一样,丢一串字符串进去,就可以像正常类一样执行,并且要能调用现有正在跑的代码。

例如:我们直接获取用户1234的信息,然后把用户年龄改为15,然后把修改后的值返回出来。

代码语言:javascript
复制
public class ChangeInfoTest {     public static int changeUserInfo() {        UserInfoVO info = UserInfoCache.getUserInfo(1234);        info.setAge(15);        return info.getAge();    }}

设计思路

如果要实现上述功能,本质上也就是我们期望写一段代码然后后在应用上执行。其实JDK的底层本身就提供了动态加载类文件的能力,它就是JavaCompiler。

如果使用JavaCompiler动态加载类文件内容,那就需要经过下述流程:

  • 把Java代码组装成一个格式正确的java源码,编译为class字节流
  • 利用ClassLoader将class字节流加载进入JVM,得到对应的class
  • 基于class则可以反射调用对应的逻辑

JavaCompiler的标准工作流程

如果代码片段格式正确,我们就通过Java编译器动态编译源代码得到了class。

代码语言:javascript
复制
// 以下仅为示例代码,具体实际可运行代码可参考文末的示例代码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 方法

代码语言:javascript
复制
// 以下仅为示例代码,具体实际可运行代码可参考文末的示例代码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");    }}

问题:为什么我们写的类能调用到的目标jvm的代码?

上面我们看到 new DynamicClassLoader(cl)的时候传递了一个参数:cl,这是DynamicClassLoader的parent,也就是它的父ClassLoader。 基于ClassLoader的双亲委派的原则,子ClassLoader是可以访问父ClassLoader里面的类的,所以我们写的代码是可以直接访问到线上的代码逻辑,而不会报类不存在。

关于ClassLoader的实现细节,我们在讲Arthas的原理时会详细再讲解。

示例代码github:

https://github.com/cm4j/cm4j-all

运行测试

JavaEvalUtilTest.evalTest1():直接运行java源码,运行即可计算1+2得到结果3

JavaEvalUtilTest.evalTest2():读取本地的一个类文件,并执行运行第一个public static 方法,结果与上一个方法同样

总结

我们想要线上动态执行代码来进行业务调整,需要经过以下步骤:

  • 实现端代码片段,里面包含自己的业务逻辑,组装成一个格式正确的java源码
  • 使用JavaCompiler,编译上述的字符串,并利用ClassLoader加载出对应的class
  • 利用反射动态调用class里面的逻辑

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 设计思路
    • JavaCompiler的标准工作流程
      • 线上如何执行代码?
      • 问题:为什么我们写的类能调用到的目标jvm的代码?
      • 示例代码github:
        • 运行测试
        • 总结
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档