乐趣区

关于java: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)

退出一个转换器 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

其中 TestControllerSimpleService 两个类的内容也很简略,间接贴代码

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 做架构 的程序猿吧~ 点个关注做个伴,让小菜不再孤独。咱们下文见!

明天的你多致力一点,今天的你就能少说一句求人的话!
我是小菜,一个和你一起变强的男人。 💋
微信公众号已开启,菜农曰,没关注的同学们记得关注哦!

退出移动版