万能钥匙!java运行时加载并执行外部class文件!

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

一、事出有因

某日晚,做饭时,收到了大佬的信息,需要临时导出一小批数据,示意如下: image.png

由图可知,从源数据到结果数据,中间需要调用service、mapper、dubbo等

幸运的是当时只需要导出测试环境的数据即可,所以直接本机编写相关逻辑,运行,一气呵成就搞定了。
但是,事情就到这了吗?

  • 万一,大佬要的是线上的数据呢?
  • 万一,需要用这批数据来做更新操作呢?
  • 万一,出现了万一,我还能快速处理吗?

PS:这边日常开发使用的是kotlin,处理数据是真的挺方便滴

二、常用的在线上执行数据导出或逻辑处理的方法

1.XXL-JOB的GLUE模式

仍记得当时刚接触到这个时候,It shock me!哇!好强大!太厉害了吧!居然还能支持@Autowire注解!好方便!到后来大致了解了一下其中的原理,如下图: image.png

  • 对目标应用来说,入参是源码,也就是java代码
  • 支持@Autowire的方式是使用了反射,将bean实例set进去,而bean来源的ApplicationContext是通过XxlJobExecutor进行静态暴露

即使强大如它,目前在实际使用时也会遇到一些小问题:

  • 需要目标服务接入了XXL-JOB
  • 尚不支持kotin语言

更多关于XXL-JOB的信息可以参考官方文档

2.Arthas的ognl命令

使用ognl表达式也可以写一些数据查询处理逻辑,但是该语法写起来没有java、kotlin顺手,要实现复杂的数据查询和处理是比较困难的,如果只是简单的调用或者触发,那ognl是非常非常合适的!

更多关于ognl的信息可以参考官方文档 还有 Artahs 中ognl的使用文档

3.使用正常的代码分支,编译并发布到预发环境或者线上环境

整个流程会比较长,难以及时响应,当出现数据不符合预期的时候,修改成本也比较高

4.预埋一个空实现方法,然后热更新该类

使用Arthas的retransform即可很方便实现热更新,但预埋需要侵入正常代码

详情见 Arthas 的retransfrom使用文档

三、有没有更合适的方法呢?

  • 支持使用kotlin编写逻辑
  • 不要求目标应用有特定依赖或侵入代码
  • 处理生效速度较快

答:如果能在运行时加载并执行外部class字节码文件,那不就能满足了?

四、理论基础

从java类加载过程可以知道:

  • jvm规范并没有限定class的来源
  • jvm支持在运行时加载类并调用类中方法

五、实现思路

一图胜千言: 掘金-执行外部class文件.jpg

这里说明一下关键的点:

1.使用class字节码文件作为目标应用的输入

  • 语言无关:java和kotlin等语言编写的逻辑最后会编译成class字节码文件
  • 获取便捷:当前代码编写都在ide中进行,在此基础上编译成字节码也很方便

2.指定 LaunchedURLClassLoader 为class文件的类加载器

  • 编写的脚本逻辑会依赖项目中的类,而这些类由 LaunchedURLClassLoader 进行加载

这里只考虑使用fatjar启动的场景,可参考:java中常见jar包形式的联系和结构解析

3.以 ApplicationContext 作为方法参数

可以从ApplicationContext获取到bean,并且也可以从bean中拿到dubbo接口的实例

六、具体实现

1.class文件准备

下边给出示例代码,编译后即可得到class字节码文件

  • java
import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Script {

    /**
     * 执行入口
     */
    public static JSONObject run(ApplicationContext applicationContext) {
        SynchroLearnInteractService bean = applicationContext.getBean(YourService.class);
        AuthApiService reference = getReference(applicationContext, AuthApiService.class);

        System.out.println("get bean"+bean);
        System.out.println("get reference"+reference);

        return new JSONObject();

    }

    /**
     * 获取dubbo的reference
     */
    public static <T> T getReference(ApplicationContext applicationContext, Class<T> type) {
        try {
            AnnotationBean annotationBean = applicationContext.getBean(com.alibaba.dubbo.config.spring.AnnotationBean.class);
            Field field = annotationBean.getClass().getDeclaredField("referenceConfigs");
            if (!field.isAccessible()) {
                field.setAccessible(true);
            }
            ConcurrentHashMap<String, ReferenceBean<?>> referenceMap = (ConcurrentHashMap<String, ReferenceBean<?>>) field.get(annotationBean);
            for (Map.Entry<String, ReferenceBean<?>> entry : referenceMap.entrySet()) {
                if (entry.getKey().contains(type.getCanonicalName())) {
                    return (T) entry.getValue().get();
                }
            }
            return null;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

}
复制代码
  • kotlin
object Script2 {

    /**
     * 执行入口
     */
    fun run(applicationContext: ApplicationContext): JSONObject {
        val bean =
            applicationContext.getBean(YourService::class.java)
        val reference = getReference(applicationContext, AuthApiService::class.java)

        println("get bean:${bean}")
        println("get reference:${reference}")

        return JSONObject()
    }

    /**
     * 获取dubbo的reference
     */
    private fun <T> getReference(applicationContext: ApplicationContext, type: Class<T>): T? {
        return try {
            val annotationBean = applicationContext.getBean(AnnotationBean::class.java)
            val field = annotationBean.javaClass.getDeclaredField("referenceConfigs")
            if (!field.isAccessible) {
                field.isAccessible = true
            }
            val referenceMap = field[annotationBean] as ConcurrentHashMap<String, ReferenceBean<*>>
            for ((key, value) in referenceMap) {
                if (key.contains(type.canonicalName)) {
                    return value.get() as T
                }
            }
            null
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
    }

}
复制代码

2.将class文件目录添加到类加载器的搜索url中

vmtool --action getInstances --className org.springframework.boot.loader.LaunchedURLClassLoader -express '#url=new java.net.URL("file:/path/to/classes/"),instances[0].addURL(#url)'
复制代码

3.获取并传入上下文,触发方法执行

vmtool --action getInstances  --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader --className org.springframework.context.ApplicationContext -express '@[email protected](instances[1])'
复制代码

instances 中一般有多个对象,需要看下那个含有需要的bean

4.重复修改,使用retransform热更新该类

retransform /path/to/classes/com/cvte/app/Script.class
复制代码

PS:该类是被LaunchedURLClassLoader加载的,所以无法被卸载!

七、结果验证

查看控制台输出: image.png It‘s working!!
177648d9c2ded5c78bda70b79ddb83a1.gif

八、后续扩展

  • 可以利用这个机制加载一些工具类,在使用arthas的各项命令时直接调用,可以极大地提高问题排查的效率
  • 入参不一定是 ApplicationContext ,还可以是各种你期待的对象

方法不分对错,合适即可,如有想法,欢迎交流!

猜你喜欢

转载自juejin.im/post/7104446719307284493