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


目录
相关文章
|
2月前
|
Java
Java中的equals()与==的区别与用法
【7月更文挑战第28天】
52 12
|
2月前
|
Java 程序员 API
Java中的异常处理:从基础到高级
【7月更文挑战第28天】在Java编程的世界中,异常处理是一块基石,它确保了程序的健壮性和可靠性。本文将带领读者深入理解Java的异常处理机制,从基本的try-catch语句开始,逐步探索更复杂的异常处理策略,如finally块、自定义异常以及异常链。我们还会讨论如何在设计良好的API时利用异常处理来提高用户体验。通过这篇文章,读者将能够更加自信地处理各种异常情况,编写出更加稳定和用户友好的Java应用程序。
|
24天前
|
缓存 负载均衡 安全
|
21天前
|
Java 数据处理
Java IO 接口(Input)究竟隐藏着怎样的神秘用法?快来一探究竟,解锁高效编程新境界!
【8月更文挑战第22天】Java的输入输出(IO)操作至关重要,它支持从多种来源读取数据,如文件、网络等。常用输入流包括`FileInputStream`,适用于按字节读取文件;结合`BufferedInputStream`可提升读取效率。此外,通过`Socket`和相关输入流,还能实现网络数据读取。合理选用这些流能有效支持程序的数据处理需求。
23 2
|
2月前
|
Java 开发者
Java中的并发编程:从基础到高级
在Java世界中,并发编程是一项至关重要的技能。本文将深入探讨Java并发编程的核心概念、实用工具和高级技术。我们将从线程基础出发,逐步过渡到线程池的使用,最后探索Java并发包中的强大工具,如CyclicBarrier、Semaphore和CountDownLatch。无论你是Java新手还是资深开发者,这篇文章都将为你提供有价值的见解和技巧,帮助你在多线程环境中编写出更加高效、稳定的代码。 【7月更文挑战第30天】
31 7
|
2月前
|
Java 开发者
Java中的异常处理:从基础到高级
在Java的世界中,异常处理是一项不可或缺的技能。它不仅关乎程序的健壮性,更是开发者调试和解决问题的利器。本文将深入探讨Java异常处理的各个方面,从基本的try-catch语句到自定义异常类,再到异常链追踪和finally块的使用,我们将一一剖析。无论你是Java新手还是资深开发者,这篇文章都将为你提供新的视角和深度理解。
|
2月前
|
数据采集 安全 Java
Java Selenium WebDriver:代理设置与图像捕获
Java Selenium WebDriver:代理设置与图像捕获
|
2月前
|
设计模式 Java
Java进阶之代理
Java进阶之代理
20 3
|
2月前
|
设计模式 Java
Java进阶之代理
【7月更文挑战第16天】Java动态代理通过`java.lang.reflect.Proxy`和`InvocationHandler`实现,无需编译期定义代理类。与静态代理相比,它更灵活,代码更简洁,适用于方法数量变化或未知接口代理。
19 2
|
2月前
|
安全 Java 调度
Java中的并发编程:从基础到高级
【7月更文挑战第20天】在Java的世界中,并发编程是一块重要的领域,它允许多个操作同时执行,极大地提高了程序的效率和性能。本文将深入探讨Java并发编程的核心概念,从线程的基础使用到高级的并发工具类,再到实际案例分析,旨在为读者提供一个全面而深入的视角来理解和掌握Java并发编程的艺术。