大家好,我是小菜。
一个心愿可能成为 吹着牛 X 谈架构 的男人!如果你也想成为我想成为的人,不然点个关注做个伴,让小菜不再孤独!
本文次要介绍
Java 高级用法之 Insrument
如有须要,能够参考
如有帮忙,不忘 点赞 ❥
微信公众号已开启,菜农曰,没关注的同学们记得关注哦!
小王是一个刚来不久的妹子,啊呸,是一个刚来不久的程序媛,常常没精打采的~ 让我很是不解,终于有一天我怕小王哪天想不开到职了岂不是会减少我的工作量(部门为数不多的妹子 - 1
)?于是乎,我被动找小王进行了谈心找到了问题所在,原来是小王编程经验不足,不晓得如何奇妙的进行日志打印,那么因果关系就总结进去了:经验不足导致编码常常出错,编码出错因为日志未打印导致排查艰难,排查艰难导致开发抑郁
。查到问题的起因,那么进行隔靴搔痒即可~
其实以上问题我置信很多小伙伴都遇到过,开发过程中未呈现的谬误在上线后就频频呈现,那么只能一直的进行增加日志打印而后再打包上传进行问题跟踪,一天的工夫绝大部分都节约在了打包上传的下面。那么能不能间接进行 bug 跟踪,而后查看到问题出错的所在?这种需要不亚于 给奔跑中的汽车更换轮胎
,匪夷所思却又无可奈何~ 其实有开发教训的小伙伴曾经想进去一个中间件,那就是 Arthas
!然而这篇文章不是介绍如何应用 Archas
,而是咱们本人能不能实现这种动静调试的技能?那么就进入咱们明天的整体 — Java Agent 技术
Java Instrument
这个玩意并不是什么 Java 的新个性,早在 JDK 1.5 的时候就诞生了,位于 java.lang.instrument.Instrumentation
中,它的作用就是用来在运行的时候从新加载某个类的 calss 文件的 api
。
这品种的实现形式其实是一种 Java Agent
技术,咱们这里能够顺带理解一下什么是 Java Agent。
一、Java Agent
代理这个词对于咱们开发人员来说并不默认,咱们常常用到的 AOP 面向切面编程用到的就是代理形式。它能够动静切入某个面,进行代码加强。这种不必反复补充轮子的形式大大增加了咱们开发效率,那么这里捕捉到了一个关键词 动静
。那么 Java Agent 如何实现?那就能够说到 JVMTI(JVM Tool Interface),这是 Java 虚拟机对外提供的 Native 编程接口,通过它咱们能够获取运行时 JVM 的诸多信息,而 Agent 是一个运行在指标 JVM 的特定程序,它能够从指标 JVM 获取数据,而后将数据传递给内部过程,而后内部过程能够依据获取到的数据进行动静 Enhance。
那么 Java Agent 什么时候可能加载?
- 指标 JVM
启动时
- 指标 JVM
运行时
那么咱们关注的是 运行时
,这样子就能满足咱们动静加载的需要。
而 Java Agent 看上去这么高大上,咱们要如何编写?当然在 JDK 1.5 之前,实现起来是具备困难性的,咱们须要编写 Native 代码来实现,那么 JDK 1.5 之后咱们就能够利用下面说到的 Java Instrument
来实现了!
首先咱们先理解一下 Instrumentation 这个接口,其中有几个办法:
addTransformer(ClassFileTransformer transformer, boolean canRetransform)
退出一个转换器 Transformer,之后所有的指标类加载都会被 Transformer 拦挡,可自定义实现 ClassFileTransformer 接口,重写该接口的惟一办法 transform()
办法,返回值是转换后的类字节码文件
retransformClasses(Class<?>... classes)
对 JVM 曾经加载的类从新触发类加载,应用下面自定义的转换器进行解决。该办法能够批改办法体,常量池和属性值,但不能新增、删除、重命名属性或办法,也不能批改办法的签名
redefineClasses(ClassDefinition... definitions)
此办法用于替换类的定义,而不援用现有类文件字节。
getObjectSize(Object objectToSize)
获取一个对象的大小
appendToBootstrapClassLoaderSearch(JarFile jarfile)
将一个 jar 文件增加到 bootstrap classload 的 classPath 中
getAllLoadedClasses()
获取以后被 JVM 加载的所有类对象
redefineClasses 和 retransformClasses 补充阐明
- 两者区别:
redefineClasses 是本人提供字节码文件替换掉已存在的 class 文件
retransformClasses 是在已存在的字节码文件上批改后再进行替换
- 替换后失效的机会
如果一个被批改的办法曾经在栈帧中存在,则栈帧中的办法会持续应用旧字节码运行,新字节码会在新栈帧中运行
- 留神点
两个办法都是只能扭转类的办法体、常量池和属性值,但不能新增、删除、重命名属性或办法,也不能批改办法的签名
二、实现 Agent
1、编写办法
下面咱们曾经说到了有两处中央能够进行 Java Agent 的加载,别离是 指标 JVM 启动时加载
和 指标 JVM 运行时加载
,这两种不同的加载模式应用不同的入口函数:
1、JVM 启动时加载
入口函数如下所示:
// 函数 1
public static void premain(String agentArgs, Instrumentation inst);
// 函数 2
public static void premain(String agentArgs);
JVM 首先寻找 函数 1
,如果没有发现函数 1,则会寻找函数 2
2、JVM 运行时加载
入口函数如下所示:
// 函数 1
public static void agentmain(String agentArgs, Instrumentation inst);
// 函数 2
public static void agentmain(String agentArgs);
与上述统一,JVM 首先寻找 函数 1
,如果没有发现函数 1,则会寻找函数 2
这两组办法的第一个参数 agentArgs
是伴随“-javaagent”一起传入的程序参数,如果这个字符串代表了多个参数,就须要本人解析这参数,inst
是 Instrumentation 类型的对象,是 JVM 本人传入的,咱们能够那这个参数进行参数的加强操作。
2、申明办法
当定义完这两组办法后,要使之失效还须要手动申明,申明形式有两种:
1、应用 MANIFEST.MF 文件
咱们须要创立resources/META-INF.MANIFEST.MF
文件,当 jar 包打包时将文件一并打包,文件内容如下:
Manifest-Version: 1.0
Can-Redefine-Classes: true # true 示意能重定义此代理所需的类,默认值为 false(可选)Can-Retransform-Classes: true # true 示意能重转换此代理所需的类,默认值为 false(可选)Premain-Class: cbuc.life.agent.MainAgentDemo #premain 办法所在类的地位
Agentmain-Class: cbuc.life.agent.MainAgentDemo #agentmain 办法所在类的地位
2、如果是 maven 我的项目,在 pom.xml 退出
3、指定 agent
要让指标 JVM 认你这个 Agent,你就要给指标 JVM 介绍这个 Agent
1、JVM 启动时加载
咱们间接在 JVM 启动参数中退出 -javaagent 参数并指定 jar 文件的地位
# 将该类编译成 class 文件
javac TargetJvm.java
# 指定 agent 程序并运行该类
java -javaagent:./java-agent.jar TargetJvm
2、JVM 运行时加载
要实现动静调试,咱们就不能将指标 JVM 停机后再重新启动,这不合乎咱们的初衷,因而咱们能够应用 JDK 的 Attach Api 来实现运行时挂载 Agent。
Attach Api 是 SUN 公司提供的一套扩大 API,用来向指标 JVM 附着(attach)在目标程序上,有了它咱们能够很不便地监控一个 JVM。Attach Api 对应的代码位于 com.sun.tools.attach
包下,提供的性能也非常简单:
- 列出以后所有的 JVM 实例形容
- Attach 到其中一个 JVM 上,建设通信管道
- 让指标 JVM 加载 Agent
该包下有一个类 VirtualMachine
,它提供了两个重要的办法:
VirtualMachine attach(String var0)
传递一个过程号,返回指标 JVM 过程的 vm 对象,该办法是 JVM 过程之间指令传递的桥梁,底层是通过 socket 进行通信
void loadAgent(String var1)
该办法容许咱们将 agent 对应的 jar 文件地址作为参数传递给指标 JVM,指标 JVM 收到该命令后会加载这个 Agent
有了 Attach Api,咱们就能够创立一个 java 过程,用它 attach 到对应的 jvm,并加载 agent。
以下是简略的 Attach 代码实现:
留神:在 mac 上装置了的 jdk 是能间接找到 VirtualMachine 类的,然而在 windows 中装置的 jdk 无奈找到,如果你遇到这种状况,请手动将你 jdk 装置目录下:lib 目录中的 tools.jar 增加进以后工程的 Libraries 中。
下面代码非常繁难的实现了 Attach 的形式,通过寻找以后零碎中所有运行的 JVM 过程,而后通过比对 PID 来筛选出指标 JVM,而后让 Agent 附着在指标 JVM 上。当然这边曾经繁难到间接在代码中指定指标 JVM 的 PID,这种形式在理论生产中是非常不可取的,咱们能够通过动静参数的形式传入 PID~!而 Attach 的执行原理也不简单,简略流程如下:
三、案例阐明
咱们上述简略聊了下 Java Agent 的实现过程,那咱们上面也简略写个案例来了解一下 Java Agent 的实现过程~
咱们下面说到能够应用 Java Instrumentation 来实现动静类批改的性能,并且在 Instrumentation 接口中咱们能够通过 addTransformer()
办法来减少一个类转换器,类转换器由类 ClassFileTransformer 接口实现。该接口中有一个惟一的办法 transform()
用于实现类的转换,也就是咱们能够加强类解决的中央!当类被加载的时候就会调用 transform()
办法,实现对类加载的事件进行拦挡并返回转换后新的字节码,通过 redefineClasses()
或 retransformClasses()
都能够触发类的从新加载事件。
实际操作
1)筹备指标 JVM
咱们这里间接应用一个 SpringBoot 我的项目来试验,不便大家加强革新~ 我的项目构造如下:
target-jvm
├─src
├─main
├─java
└─cbuc
└─life
└─targetjvm
├─controller
| └─TestController.java
└─service
| └─SimpleService.java
└─TargetJvmApplication.java
其中 TestController
和 SimpleService
两个类的内容也很简略,间接贴代码
2)筹备 Agent
1、编写办法
而后编写咱们的 Agent
jar 包。因为懈怠,所以我这边将 premain 和 agentmain 两个办法写在同一个 jar 包中,而后别离以 启动时
和 运行时
来模仿场景~
很简略,一个类中蕴含了咱们须要的所有性能~ 避免图片内容过于拥挤,小菜贴心地别离粘贴出外围代码:
premain
agentmain
ClassFileTransformer
2)申明办法
而后将 Agent 打包,打包的时候须要在 pom.xml 文件中增加以下内容
而后运行mvn assembly:assembly
既可
3)启动 Agent
当咱们曾经筹备好了两个 jar 包便能够开始测试了!
1、启动时加载
nohup java -javaagent:./java-agent-jar-with-dependencies.jar -jar target-jvm.jar &
咱们间接启动时增加参数,带上咱们的 Agent jar 包
后果并没有让小菜太难堪,胜利的实现咱们想要的性能,然而这只是启动时加载,显著不是咱们想要的~ 咱们来试下运行时如何加载
2、运行时加载
失常运行下,办法并没有做耗时统计,咱们的需要就来了,咱们想要统计该办法的耗时,首先获取该过程 ID
而后通过 Attach 形式(调用 controller 的 active() 办法)附着 Agent,咱们能够实时查看控制台
曾经能够看到 Agent 仿佛曾经胜利附着了,而后咱们持续申请 test 接口
能够发现 resolve 办法曾经被咱们加强了!
四、题外话
下面咱们曾经简略的实现了动静操作指标类文件,文章结尾就阐明了 给奔跑中的汽车更换轮胎
是一个匪夷所思却又无可奈何的需要,然而这个需要能不能让他人实现,其实是能够的,而这个就是小菜的次要目标,咱们理解了如何实现动静换轮胎的原理后,当咱们使用其成熟的中间件也能更加应手而不会手足无措,常识不能让咱们只学会 卧槽
两个字,而是当他人实现的时候咱们能默默思考,思考后再说出 牛逼
~!感兴趣的同学无妨拉取一下源码演练一番:Arthas gitee,曾经应用过相似 Arthas 或 BTrace 的同学,看完置信会更加理解其工作运行原理,没应用过的同学下次用到的时候也不会那么战战兢兢!
不要空谈,不要贪懒,和小菜一起做个 吹着牛 X 做架构
的程序猿吧~ 点个关注做个伴,让小菜不再孤独。咱们下文见!
明天的你多致力一点,今天的你就能少说一句求人的话!
我是小菜,一个和你一起变强的男人。💋
微信公众号已开启,菜农曰,没关注的同学们记得关注哦!