Java高级用法,写个代理侵入你 ?
大家好,我是小菜。
一个希望能够成为 吹着牛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)
transform()
方法,返回值是转换后的类字节码文件retransformClasses(Class>... classes)
redefineClasses(ClassDefinition... definitions)
getObjectSize(Object objectToSize)
appendToBootstrapClassLoaderSearch(JarFile jarfile)
getAllLoadedClasses()
redefineClasses 和 retransformClasses 补充说明二、实现 Agent 1、编写方法 上面我们已经说到了有两处地方可以进行 Java Agent 的加载,分别是
redefineClasses 是自己提供字节码文件替换掉已存在的 class 文件
- 两者区别:
retransformClasses 是在已存在的字节码文件上修改后再进行替换
如果一个被修改的方法已经在栈帧中存在,则栈帧中的方法会继续使用旧字节码运行,新字节码会在新栈帧中运行
- 替换后生效的时机
两个方法都是只能改变类的方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
- 注意点
目标JVM启动时加载
和 目标JVM运行时加载
,这两种不同的加载模式使用不同的入口函数:1、JVM 启动时加载
入口函数如下所示:
// 函数1
public static void premain(String agentArgs, Instrumentation inst);
// 函数2
public static void premain(String agentArgs);
JVM 首先寻找
函数1
,如果没有发现函数1,则会寻找函数22、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)
void loadAgent(String var1)
有了 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 文件中添加以下内容
【Java高级用法,写个代理侵入你 ?】
文章图片
然后运行
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做架构
的程序猿吧~点个关注做个伴,让小菜不再孤单。咱们下文见!今天的你多努力一点,明天的你就能少说一句求人的话!
我是小菜,一个和你一起变强的男人。
微信公众号已开启, 菜农曰,没关注的同学们记得关注哦!
推荐阅读
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 事件代理
- Java|Java OpenCV图像处理之SIFT角点检测详解
- java中如何实现重建二叉树
- 数组常用方法一
- 【Hadoop踩雷】Mac下安装Hadoop3以及Java版本问题
- 唐嫣可真会穿,西装搭牛仔裤都能穿出高级感,一双大长腿太抢镜
- Java|Java基础——数组
- RxJava|RxJava 在Android项目中的使用(一)
- java之static、static|java之static、static final、final的区别与应用