持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
一、事出有因
某日晚,做饭时,收到了大佬的信息,需要临时导出一小批数据,示意如下:
由图可知,从源数据到结果数据,中间需要调用service、mapper、dubbo等
幸运的是当时只需要导出测试环境的数据即可,所以直接本机编写相关逻辑,运行,一气呵成就搞定了。
但是,事情就到这了吗?
- 万一,大佬要的是线上的数据呢?
- 万一,需要用这批数据来做更新操作呢?
- 万一,出现了万一,我还能快速处理吗?
PS:这边日常开发使用的是kotlin,处理数据是真的挺方便滴
二、常用的在线上执行数据导出或逻辑处理的方法
1.XXL-JOB的GLUE模式
仍记得当时刚接触到这个时候,It shock me!哇!好强大!太厉害了吧!居然还能支持@Autowire注解!好方便!到后来大致了解了一下其中的原理,如下图:
- 对目标应用来说,入参是源码,也就是java代码
- 支持@Autowire的方式是使用了反射,将bean实例set进去,而bean来源的ApplicationContext是通过XxlJobExecutor进行静态暴露
即使强大如它,目前在实际使用时也会遇到一些小问题:
- 需要目标服务接入了XXL-JOB
- 尚不支持kotin语言
更多关于XXL-JOB的信息可以参考官方文档
2.Arthas的ognl命令
使用ognl表达式也可以写一些数据查询处理逻辑,但是该语法写起来没有java、kotlin顺手,要实现复杂的数据查询和处理是比较困难的,如果只是简单的调用或者触发,那ognl是非常非常合适的!
3.使用正常的代码分支,编译并发布到预发环境或者线上环境
整个流程会比较长,难以及时响应,当出现数据不符合预期的时候,修改成本也比较高
4.预埋一个空实现方法,然后热更新该类
使用Arthas的retransform即可很方便实现热更新,但预埋需要侵入正常代码
详情见 Arthas 的retransfrom使用文档
三、有没有更合适的方法呢?
- 支持使用kotlin编写逻辑
- 不要求目标应用有特定依赖或侵入代码
- 处理生效速度较快
答:如果能在运行时加载并执行外部class字节码文件,那不就能满足了?
四、理论基础
从java类加载过程可以知道:
- jvm规范并没有限定class的来源
- jvm支持在运行时加载类并调用类中方法
五、实现思路
一图胜千言:
这里说明一下关键的点:
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加载的,所以无法被卸载!
七、结果验证
查看控制台输出: It‘s working!!
八、后续扩展
- 可以利用这个机制加载一些工具类,在使用arthas的各项命令时直接调用,可以极大地提高问题排查的效率
- 入参不一定是 ApplicationContext ,还可以是各种你期待的对象
方法不分对错,合适即可,如有想法,欢迎交流!