我们好,我是小菜。一个期望能够成为 吹着牛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
publicstaticvoidpremain(StringagentArgs,Instrumentationinst);
//函数2
publicstaticvoidpremain(StringagentArgs);
JVM 首要寻觅函数1
,假如没有发现函数1,则会寻觅函数2
2、JVM 运转时加载
进口函数如下所示:
//函数1
publicstaticvoidagentmain(StringagentArgs,Instrumentationinst);
//函数2
publicstaticvoidagentmain(StringagentArgs);
与上述共同,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文件
javacTargetJvm.java
#指定agent程序并运转该类
java-javaagent:./java-agent.jarTargetJvm
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、启动时加载
nohupjava-javaagent:./java-agent-jar-with-dependencies.jar-jartarget-jvm.jar&
我们直接启动时添加参数,带上我们的 Agent jar包
结果并没有让小菜太为难,成功的完成我们想要的功用,可是这只是启动时加载,显着不是我们想要的~ 我们来试下运转时怎么加载
2、运转时加载
正常运转下,办法并没有做耗时计算,我们的需求就来了,我们想要计算该办法的耗时,首要获取该进程ID
然后经过 Attach 办法(调用controller 的 active() 办法)附着 Agent,我们能够实时检查控制台
现已能够看到 Agent 好像现已成功附着了,然后我们继续恳求 test 接口
能够发现 resolve 办法现已被我们增强了!
四、题外话
上面我们现已简略的完成了动态操作方针类文件,文章开头就阐明晰给奔驰中的轿车替换轮胎
是一个匪夷所思却又百般无奈的需求,可是这个需求能不能让别人完成,其实是能够的,而这个便是小菜的首要意图,我们了解了怎么完成动态换轮胎的原理后,当我们运用其成熟的中间件也能愈加应手而不会不知所措,知识不能让我们只学会卧槽
两个字,而是当别人完成的时分我们能静静思考,思考后再说出牛逼
~!感兴趣的同学无妨拉取一下源码演练一番:Arthas gitee,现已运用过类似 Arthas 或 BTrace 的同学,看完相信会愈加了解其工作运转原理,没运用过的同学下次用到的时分也不会那么战战兢兢!
不要空谈,不要贪懒,和小菜一起做个吹着牛X做架构
的程序猿吧~点个重视做个伴,让小菜不再孑立。我们下文见!
今日的你多尽力一点,明日的你就能少说一句求人的话!我是小菜,一个和你一起变强的男人。
微信大众号已敞开,菜农曰,没重视的同学们记住重视哦!