字节码增强:原理与实战
本文由一个拦截器逻辑的使用场景及演变历程,引入字节码增强技术。介绍字节码的本质,字节码增强的原理及JVM 启动过程中的 Agent 加载、生效流程,并对常见字节码操作工具进行了简单应用。
注:本文仅讨论 javaagent “启动时加载”。
一、技术为业务需求服务
技术是工具,是解决问题的途径。针对不同的业务需求场景,可以使用不同的技术实现。
通过一部拦截器的流浪史来引入主题:
一个简单的demo
1、基础版:新建一个Dog对象,然后调用成员方法输出到控制台
被调用方
调用方
2、加强版:需要统计方法执行的时间
常规开发:
被调用方
调用方
3、从被调用方剥离非业务逻辑
面向对象设计原则,对象应该尽可能专注自己职责范围内的事情,狗只负责叫,不负责统计自己叫了多长时间,因此统计代码应该移出Dog类。
3.1 方法提取
3.2 类提取–(参考SpringMVC-Interceptor)
3.3 类解耦合(使用动态代理方式-CGLib/JDK Proxy,这里Dog类没有实现接口,使用CGLib)
至此,非业务逻辑由从被调用方剥离出来了,同时我们也发现调用方代码却遭到改变,Main class里面需要添加动态代理类的处理逻辑。假如不允许改变调用方代码,进一步处理。
4、调用方代码剥离(切面–AspectJ)
切面
被调方
调用方
注意:此时直接运行Main class切面不会生效,运行前先进行编译期织入 java -jar $ASPECTJ_TOOLS -cp $ASPECTJ_RT -sourceroots src/main/java/ -d target/classes ...
至此,调用方不用显式地调用动态代理逻辑,编译期织入到class中去了(这里已经闻到了代码增强的气味了)。
切面逻辑虽然与具体的业务逻辑解耦合了,独立出切面类。但是是否生效仍然由业务代码(切面类)去控制。无论如何,都需要业务方改造,添加切面逻辑代码。
能不能更进一步,连切面都不写,也让切面逻辑生效呢?
5、javaagent 版本–隐式地,无侵入地添加切面逻辑
新建独立的agent工程
添加MANIFEST.MF文件以及Premain-Class,premain属性
编译包含目标逻辑的源文件生成class文件
注册ClassFileTransfer,在transform方法中替换byte[]
MANIFEST.MF指定premain函数和打开类增强开关
编译输出jar包
MANIFEST.MF文件。
待替换的新class文件(忽略中文乱码)。
class转换器,将新的Dog.class替换旧的Dog.class。
maven打包输出Agent.jar
上面的javaagent实现细节可以先存疑,后面会深入描述,只需要知道按照这样的步骤可以实现我们的需求。
对于业务方而言:代码完全没有变化:
被调用方
调用方
想要使切面逻辑生效,只需要在启动命令参数中加入-javaagent 选项,指向 Agent 的 jar 包。
这样,拦截器逻辑以一种插件的形式抽取出来了,使用的时候加载插件就可以了。
小结一下
不同需求场景下,可以不同的方式实现切面拦截逻辑;
AspectJ或者SpringAop只是一种对开发者友好的快捷方式,本质上还是修改的业务代码,只不过隐藏了调用逻辑,并不能真正“无侵入“;
javaagent可以无侵入的修改一个已发布的java组件的运行逻辑。
二、什么是字节码?
byte[]
1、回归原始:JDK 里面提供了很多有用的工具
在我们刚开始学习 Java 语言时候的 demo 运行:
编写原始 Java 文件:
使用 Javac 编译字节码文件:
Javac生产的 class 文件有什么作用呢?
Java 语言一次编译,到处运行的核心基础-JVM。
2、class文件到底是个什么东西?
先用文本编辑器暴力打开看看:
看不懂?换个方式:
想看个明白?继续整:使用010editor打开。
各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙, 这样可以使得class文件非常紧凑, 体积轻巧, 可以被JVM快速的加载至内存, 并且占据较少的内存空间。
主要包含的信息:
(1)魔数
(2)版本号(参考文末例子:JRE版本错误)
(3)常量池容量
(4)常量池:
文字字符串, 常量值
当前类的类名, 字段名, 方法名, 各个字段和方法的描述符
对当前类的字段和方法的引用信息, 当前类中对其他类的引用信息等等
(5)其他属性
常量池如何索引:
相互索引:
例如方法索引,获取classIndex和nameAndTypeIndex,通过数组下标,可以找到该方法所属的class和方法名称。
MethodRef
|-----|classIndex
|-----|-----|nameIndex --→ classNmae
|-----|nameAndTypeIndex
|-----|-----|nameIndex --→ methodName
常量池索引和字节码指令的执行。
使用jre自带工具javap反编译class文件如下:
Main.class字节码:
Dog.class字节码:
可以看到字节码具备一定的可读性,对照着源码,可以按照执行逻辑走一遍字节码执行流程,相关指令的含义很容易从网上查询到。
至此,我们通过一个简单的demo执行流程,大致了解了常量的引用以及一个简单java方法对应的字节码指令执行过程。
注:
stack:最大操作数栈,JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度。
locals:局部变量所需的存储空间,单位为Slot。
args_size: 方法参数的个数。
压栈:字节码指令执行过程中涉及到了很多压栈操作:JVM是一个基于栈的架构。方法执行的时候(包括main方法),在栈上会分配一个新的帧,这个栈帧包含一组局部变量。
这组局部变量包含了方法运行过程中用到的所有变量,包括this引用,所有的方法参数,以及其它局部定义的变量。
小结一下
class文件即字节码是所有属性,方法逻辑的合集。
通过字节码二进制文件将开发者与虚拟机进行了“解耦”。
推理:修改某些字节或者替换整个二进制流可以修改运行时逻辑 。
三、如何增强字节码?
byte[] → byte[]
思路:
如前述方式直接替换为目标逻辑编译后的字节码。
手术刀式精准操作,修改/添加某些位置的byte。
高级API。
工具集:/ASM/javaassist/ByteBuddy 等等。
示例:
ASM
指令级别的字节码操作(性能强悍)。
指令→ASM api 对应关系(这里将原始类做了简化,将字符串拼接逻辑去掉,仅仅输出时间。因为一个简单的字符串拼接过程,转换成字节码指令可能需要很多行)。
先看看目标源码与字节码指令的一一映射关系。
再看看增强字节码逻辑与目标源码的字节码的一一映射关系。
通过对比我们可以发现,ASM的API精确到字节码指令级别,所有的临时变量存储,压栈操作,静态/实例方法的调用都有对应的API操作。
javassist:(dubbo)
提供字节码级别的API,类似ASM,不再赘述。
提供源码级别的API,针对本文的案例,实现如下:
ByteBuddy
基于ASM的高级API,使我们对字节码的操作提升到更抽象层次。开发者只需要知道要实现什么目标,如何使用对应的API,不用关心底层的字节码指令排列,甚至可以不用了解字节码指令。
关于相关框架的API不详细说,有兴趣的同学可以自行查询相关资料。
小结一下
各种级别的API可以帮助开发者轻松实现字节码增强,实现特定逻辑。
不论什么奇技淫巧,都离不开Instrumentation机制。
四、增强的 byte[] 是如何影响 JVM 的?
Event --> CallBack
由前文总结,引入Instrumentation机制。
1、铺垫知识点:
(1)JVMTI
JVMTI 是基于事件驱动的,JVM 每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者扩展自己的逻辑。
JVMTIAgent 使用JVMTI来查询或控制JVM,JVMTIAgent与目标JVM运行在同一个进程中,通过JVMTI进行通信,最大化控制能力,最小化通信成本。
典型场景下,JVMTI代理会被实现的非常紧凑,其他的进程会与JVMTI代理进行通信。比如jdwp(IDEA远程调试)。
(2)JVMTIAgent
表现形式:
(1)linux: .so文件
windows: .dll文件
c/c++ 动态链接库
(2)JPLISAgent: .jar文件
命令行参数
(1)-agentlib:agent-lib-name=options
(2)-agentpath:path-to-agent=options
(3)-javaagent:/data/../../Agent.jar
可加载多个,通过options区分
实现接口
(1)JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved);
(2)JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM vm, char options, void* reserved);
(3)JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm);
JPLISAgent(Java Programming Language Instrumentation Services Agent)-- Instrumentation机制
(1)JavaSE1.5 启动时加载(本文重点)。
(2)JavaSE1.6 运行时加载。
2、简化了的核心流程逻辑
命令参数:-javaagent:/data/../../Agent.jar=optoions。
虚拟机创建-构建并初始化Agent-注册VMInit事件。
虚拟机初始化-触发VMInit事件-Agent start方法-注册回调函数并监听ClassFileLoadHook。
类加载-触发jvmtiEventClassFileLoadHook事件-替换byt[]-ClassLoader解析。
3、Java 虚拟机启动过程中 Agent 相关的流程:
(1)创建JVM的时候初始化agent
启动时读取jvm命令,-agentlib -agentpath -javaagent,并构建了Agent Library链表构建了Agent Library链表。
对agent链表中的每个agent,加载所指定的动态库(如instrument.so), 并调用里面的Agent_OnLoad方法。
创建并初始化 JPLISAgent,初始化了Premain class和包里的配置文件。
注册VMInit事件。
Agent_onLoad
|-----|createNewJPLISAgent
|-----|-----|initializeJPLISAgent
|-----|-----|-----|eventHandlerVMInit ---- > VMInit
(2)虚拟机初始化
实际上是调用 java 类 sun.instrument.InstrumentationImpl 类里的方法loadClassAndCallPremain。
(3)触发ClassFileLoadHook事件
|parseClassFile
|-----|post_class_file_load_hook
|-----|-----|post_to_env
|-----|-----|-----|eventHandlerClassFileLoadHook(jvmtiEventClassFileLoadHook回调函数)
|-----|-----|-----|-----|transformClassFile
|-----|-----|-----|-----|-----|CallObjectMethod
|-----|-----|-----|-----|-----|-----|sun.instrument.InstrumentationImpl.transform()
实际调用的java方法 Instrumentationimpl.transform。
debug过程中通过ClassFileTransformer的transform函数的执行堆栈印证。
到这里,增强的byte[]如何生效并影响运行时class的过程基本可以串起来。
小结一下
虚拟机创建阶段,初始化agent,解析,加载javaagent jar,注册回调函数监听VMInt事件。
虚拟机初始化阶段,触发VMInt回调函数,注册回调函数监听ClassFileHook事件,同时执行loadClassAndCallPremain函数,注册transformer。
ClassLoader加载类的时候触发tranform回调,判断是否目标类,进行对应字节码替换。
五、应用
监控
调试
混淆
AOP增强
日志记录
非常规应用:IDEA破解。
部分破解教程里面下载插件jar后,会要求你在IDEA的启动参数文件idea.vmoptions中添加一行,就是javaagent参数。
我们可以反编译这个插件jar包看看,发现很多class因为加了混淆,反编译后无法正常识别,但是核心入口Agent.class的主要工作就是注册Transformer,可以推测这些Transformer的功能就是在IDEA启动时之前修改某些鉴定Lisence的逻辑。
六、总结回顾
通过介绍字节码,字节码操作工具以及openJDK关于Instrumention机制的部分源码,探索了字节码增强的实现原理。
简单介绍了相关技术的应用场景。
七、附录
SpringMVC-Interceptor
IDEA 远程调试
JRE版本错误
作者: Neo
更多内容敬请关注vivo 互联网技术微信公众号
注:转载文章请先与微信号:Labs2020联系
字节码增强:原理与实战相关推荐
- java 字节码增强原理_深入浅出Java探针技术1--基于java agent的字节码增强案例
Java agent又叫做Java 探针,本文将从以下四个问题出发来深入浅出了解下Java agent 一.什么是java agent? Java agent是在JDK1.5引入的,是一种可以动态修改 ...
- python字节码解析_从底层入手,解析字节码增强和Btrace应用
这篇文章聊下字节码和相关的应用. 1.机器码和字节码 机器码(machine code),学名机器语言指令,有时也被称为原生码(Native Code),是电脑的CPU可直接解读的数据. 通常意义上来 ...
- 干货!Java字节码增强探秘
点击上方"朱小厮的博客",选择"设为星标" 后台回复"加群"获取公众号专属群聊入口 来源:美团技术团队 1. 字节码 1.1 什么是字节码? ...
- JVM插桩之一:JVM字节码增强技术介绍及入门示例
字节码增强技术:AOP技术其实就是字节码增强技术,JVM提供的动态代理追根究底也是字节码增强技术. 目的:在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修 ...
- idea如何反编译字节码指令_美团点评:Java字节码增强技术,线上问题诊断利器...
作者简介:泽恩,美团到店住宿业务研发团队工程师.文章转载于公众号:美团技术团队 1. 字节码 1.1 什么是字节码? Java之所以可以"一次编译,到处运行",一是因为JVM针对各 ...
- 【JVM】字节码与ASM字节码增强、Instrument实现类的动态重加载
目录 字节码与ASM字节码增强 什么是字节码? 字节码结构 操作数栈与字节码 字节码增强 ASM 运行时类加载 Instrument JPDA与JVMTI instrument实现热加载的过程 字节码 ...
- java探针 字节码增强_深入浅出Java探针技术1--基于java agent的字节码增强案例
Java agent又叫做Java 探针,本文将从以下四个问题出发来深入浅出了解下Java agent 一.什么是java agent? Java agent是在JDK1.5引入的,是一种可以动态修改 ...
- Hibernate字节码增强
介绍 既然您已经了解了Hibernate脏检查的基础知识 ,我们就可以深入研究增强的脏检查机制. 虽然默认的图遍历算法对于大多数用例可能已经足够,但有时您需要优化的脏检查算法,并且检测方法比构建自己的 ...
- object转class_从零并发框架(三)异步转同步注解+字节码增强代理实现
序言 上一节我们学习了异步查询转同步的 7 种实现方式,今天我们就来学习一下,如何对其进行封装,使其成为一个更加便于使用的工具. 思维导图如下: 异步转同步字节码增强 拓展阅读 java 手写并发框架 ...
最新文章
- Android Context activity实例使用
- 剑破冰山—Oracle开发艺术 前言
- python web开发项目 源码_真零基础Python开发web
- 『Linux基础 - 4 』linux常用命令(1)
- java advance_JavaAdvance
- 7-Zip CommondLine 使用记录
- SAP UI5应用debug级别的日志打印如何设置
- mysql跨服务器链表_MySQL 源码链表的实现
- 旷视COCO获奖团队亲述:我们是如何两年拿下7个冠军的
- JAVA→异常、异常类体系结构、try→catch→finally处理异常、throw new Exception(“重构异常“);、throws Exception{ }、自定义异常
- Visual Studio 2008 Designer.cs不能更新/自动添加控件声明的解决办法
- 冒泡排序c语言标准代码,C语言实现冒泡排序法和选择排序法代码参考
- MTF、Resolution、Contrast解读
- PTB-XL大型的心电图数据集
- 讯飞机器翻译质量评估挑战赛Baseline(PaddlePaddle)
- excel中文件合并F9键
- 亲爱的老狼-ctrl的快捷键用法大全
- mybatisplus 自增主键失效,自增主键超大
- Leetcode刷题总结(三)
- 绿皮书一些quant 题目 (1)