Java高级用法,写个代理侵入你 ?

简介: 本文主要介绍 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 的同学,看完相信会更加了解其工作运行原理,没使用过的同学下次用到的时候也不会那么战战兢兢!


目录
相关文章
|
5月前
|
Java
Java中的equals()与==的区别与用法
【7月更文挑战第28天】
78 12
|
2月前
|
存储 安全 Java
深入理解Java中的FutureTask:用法和原理
【10月更文挑战第28天】`FutureTask` 是 Java 中 `java.util.concurrent` 包下的一个类,实现了 `RunnableFuture` 接口,支持异步计算和结果获取。它可以作为 `Runnable` 被线程执行,同时通过 `Future` 接口获取计算结果。`FutureTask` 可以基于 `Callable` 或 `Runnable` 创建,常用于多线程环境中执行耗时任务,避免阻塞主线程。任务结果可通过 `get` 方法获取,支持阻塞和非阻塞方式。内部使用 AQS 实现同步机制,确保线程安全。
|
21天前
|
Java
JAVA 静态代理 & 动态代理
【11月更文挑战第14天】静态代理是一种简单的代理模式实现,其中代理类和被代理类的关系在编译时已确定。代理类实现与被代理类相同的接口,并持有被代理类的实例,通过调用其方法实现功能增强。优点包括代码结构清晰,易于理解和实现;缺点是对于多个被代理类,需为每个类编写相应的代理类,导致代码量大增,维护成本高。动态代理则在运行时动态生成代理类,更加灵活,减少了代码冗余,但可能引入性能损耗和兼容性问题。
|
2月前
|
设计模式 Java API
[Java]静态代理与动态代理(基于JDK1.8)
本文介绍了代理模式及其分类,包括静态代理和动态代理。静态代理分为面向接口和面向继承两种形式,分别通过手动创建代理类实现;动态代理则利用反射技术,在运行时动态创建代理对象,分为JDK动态代理和Cglib动态代理。文中通过具体代码示例详细讲解了各种代理模式的实现方式和应用场景。
30 0
[Java]静态代理与动态代理(基于JDK1.8)
|
2月前
|
Java
Java访问外网图片地址时,如何添加代理?
【10月更文挑战第14天】Java访问外网图片地址时,如何添加代理?
39 2
|
2月前
|
Java
Java代码解释静态代理和动态代理的区别
### 静态代理与动态代理简介 **静态代理**:代理类在编译时已确定,目标对象和代理对象都实现同一接口。代理类包含对目标对象的引用,并在调用方法时添加额外操作。 **动态代理**:利用Java反射机制在运行时生成代理类,更加灵活。通过`Proxy`类和`InvocationHandler`接口实现,无需提前知道接口的具体实现细节。 示例代码展示了两种代理方式的实现,静态代理需要手动创建代理对象,而动态代理通过反射机制自动创建。
|
3月前
|
Java
Java 正则表达式高级用法
Java 中的正则表达式是强大的文本处理工具,用于搜索、匹配、替换和分割字符串。`java.util.regex` 包提供了 `Pattern` 和 `Matcher` 类来高效处理正则表达式。本文介绍了高级用法,包括使用 `Pattern` 和 `Matcher` 进行匹配、断言(如正向和负向前瞻/后顾)、捕获组与命名组、替换操作、分割字符串、修饰符(如忽略大小写和多行模式)及 Unicode 支持。通过这些功能,可以高效地处理复杂文本数据。
57 10
|
3月前
|
存储 Java 数据处理
Java 数组的高级用法
在 Java 中,数组不仅可以存储同类型的数据,还支持多种高级用法,如多维数组(常用于矩阵)、动态创建数组、克隆数组、使用 `java.util.Arrays` 进行排序和搜索、与集合相互转换、增强 for 循环遍历、匿名数组传递以及利用 `Arrays.equals()` 比较数组内容。这些技巧能提升代码的灵活性和可读性,适用于更复杂的数据处理场景。
42 10
|
5月前
|
Java 程序员 API
Java中的异常处理:从基础到高级
【7月更文挑战第28天】在Java编程的世界中,异常处理是一块基石,它确保了程序的健壮性和可靠性。本文将带领读者深入理解Java的异常处理机制,从基本的try-catch语句开始,逐步探索更复杂的异常处理策略,如finally块、自定义异常以及异常链。我们还会讨论如何在设计良好的API时利用异常处理来提高用户体验。通过这篇文章,读者将能够更加自信地处理各种异常情况,编写出更加稳定和用户友好的Java应用程序。
|
4月前
|
缓存 负载均衡 安全