前言
本文需要了解 Javaagent,不清楚的朋友可以借阅 参考资料-1 、参考资料-2
简单的讲就是使用 JVM 提供的钩子,无感修改字节码
事件的起因是需要对现在的 Bytebuddy 逻辑修改。
因为 Bytebuddy 好用是好用,但是一旦加载进去,会影响后续所有的 Class 的加载速度。
如果 Class 很多,那么会导致整个流程里面的速度变慢。
所以当时设想的解决方案很简单,业务上能不能滞后启动?原理上能不能滞后启动?
确认业务上没问题后,原理上就是把 premain 改成 agentmain,加载时修改变更为加载后修改。
大纲
修改过程
现有的逻辑
将含有 Focus
注解的方法委派给 FocusInterceptor
然后通过 java agent
的方案将对应的字节码转化后,写入 Classloader 内的。
修改逻辑 - 第一版
如果要使用 retransform 的方式在类加载之后,再次处理类,通过网上查询的资料发现需要这么写。
然后我就兴致冲冲的写好了,感觉没啥问题啊,很简单。
跑一下试试。一、二、三。
吖,报错了。
没关系,报错是正常的,确认下原因。
是这里的参数绑定出问题了。绑定的是哪一个参数呢?
排除法试试。。。发现是 @SuperCall
这个注解的绑定方案。为啥会出问题呢?
查查资料,StackOverFlow、Bytebuddy 官网、源码 Debug 一下。终于发现了问题
定位原因
这边要从 retransform 的方式说起了。
见上文 https://www.yuque.com/3dot141/gpa3qy/yod2il?inner=yb3du 代码块中所讲,支持 Retransform 主要是两部分的设置。
- 重定义类型
_REDEFINITION_
_ _
_RETRANSFORMATION_
- DISABLED
- 修改类的方式
- REBASE
- REDEFINE
默认直接 installOn
能够支持 SuperCall
的默认配置是
总结一下
支持 SuperCall | 不支持 SuperCall |
---|
重定义策略 -DISABLED | RETRANSFORMATION |
类转化策略 -REBASE | REDEFINE |
这个原因是啥呢?
根据官网的文档所说
redefine 会直接改变对应的类的方法字节码
rebase 会在原来类的基础上,加上辅助类,然后替换原先的字节。
让我们写个 DEMO 确认一下。
如上代码会生成两个类,如下
TestClass1 的反编译,没有发现 testPrint original SYROTrei accessor I5YpbWwx 这个方法。
看一下字节码
发现字节码里面是存在对应的方法的。
结论
所以 Rebase
的方案,就是会生成辅助类、辅助方法,从而能够将对应的类转化为 Callable
直接进行调用。并且保存着原来的方法。和官网说的是一致的。
这样的话生成的辅助类 BytebuddyTest$TestClass1$auxiliary$htJkCEkt
就可以赋值给 SuperCall
绑定的方法,从而让 TestInterceptor
进行调用。
而 Redefine
呢,会直接消除掉辅助的方法从而让 SuperCall
无法绑定,从而出现问题。
本章说
问题一:既然 Rebase 能保留,为啥不能直接用 Rebase,非得用 Redefine
问题二:重定义的区别
修改逻辑 - 第二版
既然 SuperCall
这个注解不能用,那么有没有其他的方案呢?答案是肯定的,那就是 Advice 方案。
相信大家对 Advice 这个概念都不陌生,Spring 生成 Bean 时的 CGLIB 可能是大家第一次接触切面的概念。
Bytebuddy 中同样也存在 Advice 这个概念。计算好一个方法的入口出口,从而修改对应的方法。
参考 Bytebuddy 的源码分析-Advice 篇 的使用逻辑,将 Interceptor 转化成 Advice
改完收工,然后我就一同改啊。将之前的各种 xxInterceptor
改成 xxAdvice
。心里美滋滋。小菜一碟,交代码,过 PR。收工。
没有人知道,问题已经悄悄埋下,静静地等着第二天,某一个幸运儿发现它。
问题来自于这样一段转化
引发报错
定位原因 - 第一次
其实就是访问 lambda 表达式出错了,为什么会出错呢?
这里有几个知识点
- 如果切面不做配置,那么默认是需要内联到原方法体中的。
- 如果不内联,则不能修改返回值。
- 跳过默认的方法体
skipOn
- lambda 表达式的访问层级
第四个即是这个问题的重点,让我们看一看这个类的字节码(因为反编译工具是看不出来的==)
如上,默认的 lambda 表达式的访问层级是 private 的。
当内联到目标方法后,逻辑是这样的
所以自然而然就会抛错,修复也很简单,既然 lambda 表达式会抛错,那么直接用非匿名的不就好了。(猜测匿名内部类也是一样。)
所以直接新建一个 AdviceCallable
然后再次尝试运行,报错变更
定位原因 - 再一次
问题是啥?为什么会出现这个问题?
总结一下,如何判断当前调用是直接被外部调用,还是经过 Bytebuddy 的 Advice 被调用?
1、通过线程,来获取一个变量,从而知道当前是被谁在调用
需要考虑堆栈的进入、出去。考虑当前线程,子线程。考虑自己调自己
2、能否用一个特征值来告诉当前是从哪里调用的?类似于改变方法体?
重点是运行到自己的时候需要有一个独一无二的值,在 JVM 里面哪些东西符合要求?
本章说
问题一:MethodHandles / invokeDynamic 的引入和原因
问题二:匿名内部类生成后的范围是啥?也是 Private 的吗?
后记
定位问题的时候,源码是要看的,但抛开源码的细节,往上走一层,输入输出往往可以更直观的告诉你问题。
todo:JVM-Sandbox 也是同样的 transform 方案。和 Bytebuddy 有什么异同呢?API 上哪种更好用?设计的更直观?
参考文档
Bytebuddy 的基础使用
Bytebuddy 的源码分析-Advice 篇