Java 中的 Agent 技术可以让我们无侵入性的去进行代理,最常用于程序调试、热部署、性能诊断分析等场景,现如今比较火热的分布式链路追踪项目Skywalking,就是通过探针技术去捕获日志,将数据上报OAP观察分析平台。

Java Agent 技术简介

Java Agent 直译为 Java 代理,也常常被称为 Java 探针技术。

Java Agent 是在 JDK1.5 引入的,是一种可以动态修改 Java 字节码的技术。Java 中的类编译后形成字节码被 JVM 执行,在 JVM 在执行这些字节码之前获取这些字节码的信息,并且通过字节码转换器对这些字节码进行修改,以此来完成一些额外的功能。

Java Agent 是一个不能独立运行 jar 包,它通过依附于目标程序的 JVM 进程,进行工作。启动时只需要在目标程序的启动参数中添加-javaagent 参数添加 ClassFileTransformer 字节码转换器,相当于在main方法前加了一个拦截器。

Java Agent 功能介绍

Java Agent 主要有以下功能

  • Java Agent 能够在加载 Java 字节码之前拦截并对字节码进行修改;
  • Java Agent 能够在 Jvm 运行期间修改已经加载的字节码;

Java Agent 的应用场景

  • IDE 的调试功能,例如 Eclipse、IntelliJ IDEA ;
  • 热部署功能,例如 JRebel、XRebel、spring-loaded;
  • 各种线上诊断工具,例如 Btrace、Greys,还有阿里的 Arthas;
  • 各种性能分析工具,例如 Visual VM、JConsole 等;
  • 全链路性能检测工具,例如 Skywalking、Pinpoint等;

Java Agent 实现原理

在了解Java Agent的实现原理之前,需要对Java类加载机制有一个较为清晰的认知。一种是在man方法执行之前,通过premain来执行,另一种是程序运行中修改,需通过JVM中的Attach实现,Attach的实现原理是基于JVMTI。

主要是在类加载之前,进行拦截,对字节码修改

下面我们分别介绍一下这些关键术语:

Java Agent 案例

  • JVMTI 就是JVM Tool Interface,是 JVM 暴露出来给用户扩展使用的接口集合,JVMTI 是基于事件驱动的,JVM每执行一定的逻辑就会触发一些事件的回调接口,通过这些回调接口,用户可以自行扩展

    JVMTI是实现 Debugger、Profiler、Monitor、Thread Analyser 等工具的统一基础,在主流 Java 虚拟机中都有实现

  • JVMTIAgent是一个动态库,利用JVMTI暴露出来的一些接口来干一些我们想做、但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:

    • Agent_OnLoad函数,如果agent是在启动时加载的,通过JVM参数设置
    • Agent_OnAttach函数,如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用Agent_OnAttach函数
    • Agent_OnUnload函数,在agent卸载时调用
  • javaagent 依赖于instrument的JVMTIAgent(Linux下对应的动态库是libinstrument.so),还有个别名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),专门为Java语言编写的插桩服务提供支持的

  • instrument 实现了Agent_OnLoad和Agent_OnAttach两方法,也就是说在使用时,agent既可以在启动时加载,也可以在运行时动态加载。其中启动时加载还可以通过类似-javaagent:jar包路径的方式来间接加载instrument agent,运行时动态加载依赖的是JVM的attach机制,通过发送load命令来加载agent

  • JVM Attach 是指 JVM 提供的一种进程间通信的功能,能让一个进程传命令给另一个进程,并进行一些内部的操作,比如进行线程 dump,那么就需要执行 jstack 进行,然后把 pid 等参数传递给需要 dump 的线程来执行

一、加载 Java 字节码之前拦截

我们就以打印方法的执行时间为例,通过Java Agent 来实现。

首先我们需要构建一个精简的Maven项目,在其中构建两个Maven的子项目,一个用于实现外挂的Agent,一个用于实现测试目标程序。

我们在父应用中导入两个项目公共依赖的包

    <dependencies><dependency><groupId>org.javassist</groupId><artifactId>javassist</artifactId><version>3.28.0-GA</version></dependency></dependencies>

首先我们去构建测试的目标程序

// 启动类
public class APPMain {public static void main(String[] args) {System.out.println("APP 启动!!!");AppInit.init();}
}
// 模拟的应用初始化的类
public class AppInit {public static void init() {try {System.out.println("APP初始化中...");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}
}

然后我们启动程序,测试是否能正常执行,程序正常执行之后,我们开始构建探针程序

探针程序中我们需要编写,改变原有class的Transformer,通过自定义的Transformer类完成输出方法执行时间的功能

首先构检Agent程序的入口

public class RunTimeAgent {public static void premain(String arg, Instrumentation instrumentation) {System.out.println("探针启动!!!");System.out.println("探针传入参数:" + arg);instrumentation.addTransformer(new RunTimeTransformer());}
}

这里每个类加载的时候都会走这个方法,我们可以通过className进行指定类的拦截,然后借助javassist这个工具,进行对Class的处理,这里的思想和反射类似,但是要比反射功能更加强大,可以动态修改字节码。

javassist是一个开源的分析、编辑和创建Java字节码的类库。

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;public class RunTimeTransformer implements ClassFileTransformer {private static final String INJECTED_CLASS = "com.zhj.test.init.AppInit";@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {String realClassName = className.replace("/", ".");if (realClassName.equals(INJECTED_CLASS)) {System.out.println("拦截到的类名:" + realClassName);CtClass ctClass;try {// 使用javassist,获取字节码类ClassPool classPool = ClassPool.getDefault();ctClass = classPool.get(realClassName);// 得到该类所有的方法实例,也可选择方法,进行增强CtMethod[] declaredMethods = ctClass.getDeclaredMethods();for (CtMethod method : declaredMethods) {System.out.println(method.getName() + "方法被拦截");method.addLocalVariable("time", CtClass.longType);method.insertBefore("System.out.println(\"---开始执行---\");");method.insertBefore("time = System.currentTimeMillis();");method.insertAfter("System.out.println(\"---结束执行---\");");method.insertAfter("System.out.println(\"运行耗时: \" + (System.currentTimeMillis() - time));");}return ctClass.toBytecode();} catch (Throwable e) { //这里要用Throwable,不要用ExceptionSystem.out.println(e.getMessage());e.printStackTrace();}}return classfileBuffer;}
}

我们需要在Maven中配置,编译打包的插件,这样我们就可以很轻松的借助Maven生成Agent的jar包

<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.5.1</version><!-- 指定maven编译的jdk版本。若不指定,maven3默认用jdk 1.5 maven2默认用jdk1.3 --><configuration><source>8</source><target>8</target></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.2.0</version><configuration><archive><!--自动添加META-INF/MANIFEST.MF --><manifest><addClasspath>true</addClasspath></manifest><manifestEntries><Menifest-Version>1.0</Menifest-Version><Premain-Class>com.zhj.agent.RunTimeAgent</Premain-Class><Can-Redefine-Classes>true</Can-Redefine-Classes><Can-Retransform-Classes>true</Can-Retransform-Classes></manifestEntries></archive></configuration></plugin></plugins></build>

否则我们需要在resources下创建META-INF/MANIFEST.MF文件,文件内容如下,我们可以看出这个与Maven中的配置是一致的,然后通过配置编译器,借助编译器打包成jar包,需指定该文件

Manifest-Version: 1.0
Premain-Class: com.zhj.agent.RunTimeAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

告示文件MANIFEST.MF参数说明:

  • Manifest-Version

    文件版本

  • Premain-Class

    包含 premain 方法的类(类的全路径名)main方法运行前代理

  • Agent-Class

    包含 agentmain 方法的类(类的全路径名)main开始后可以修改类结构

  • Boot-Class-Path

    设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。(可选)

  • Can-Redefine-Classes true

    表示能重定义此代理所需的类,默认值为 false(可选)

  • Can-Retransform-Classes true

    表示能重转换此代理所需的类,默认值为 false (可选)

  • Can-Set-Native-Method-Prefix true

    表示能设置此代理所需的本机方法前缀,默认值为 false(可选)

  • ...

最后通过Maven生成Agent的jar包,然后修改测试目标程序的启动器,添加JVM参数即可

参数示例:-javaagent:F:\code\myCode\agent-test\runtime-agent\target\runtime-agent-1.0-SNAPSHOT.jar=hello

最终效果:

这样就完成了无侵入的代理。

二、运行时拦截(JDK1.6以上提供)

在案例一的基础上,我们如何实现在程序运行时去完成动态修改字节码呢?

动态修改字节码需要依赖于JDK为我们提供的JVM工具,也就是上边我们提到的Attach,通过它去加载我们的代理程序。

首先我们在代理程序中需要定义一个名字为agentmain的方法,它可以和上边我们提到的premain是一样的内容,也可根据agentmain的特性进行自己逻辑的开发。

/*** agentmain 在 main 函数开始运行后才启动(依赖于Attach机制)*/
public class RunTimeAgent {public static void agentmain(String arg, Instrumentation instrumentation) {System.out.println("agentmain探针启动!!!");System.out.println("agentmain探针传入参数:" + arg);instrumentation.addTransformer(new RunTimeTransformer());}
}

然后就是我们需要将配置中设置,让其知道我们的探针需要加载这个类,在maven中设置如下,如果是META-INF/MANIFEST.MF文件同理。

<!--<Premain-Class>com.zhj.agent.agentmain.RunTimeAgent</Premain-Class>-->
<Agent-Class>com.zhj.agent.agentmain.RunTimeAgent</Agent-Class>

这样其实我们的探针就已经改造好了,然后我们需要在目标程序的main方法中植入一些代码,使其可以读取到我们的代理程序,这样我们也无需去配置JVM的参数,就可以加载探针程序。

public class APPMain {public static void main(String[] args) {System.out.println("APP 启动!!!");for (VirtualMachineDescriptor vmd : VirtualMachine.list()) {// 指定的VM才可以被代理if (true) {System.out.println("该VM为指定代理的VM");System.out.println(vmd.displayName());try {VirtualMachine vm = VirtualMachine.attach(vmd.id());vm.loadAgent("D:/Code/java/idea_project/agent-test/runtime-agent/target/runtime-agent-1.0-SNAPSHOT.jar=hello");vm.detach();} catch (Exception e) {e.printStackTrace();}}}AppInit.init();}
}

其中VirtualMachine是JDK工具包下的类,如果系统环境变量没有配置,需要自己在Maven中引入本地文件。

<dependency><groupId>com.sun</groupId><artifactId>tools</artifactId><version>1.8</version><scope>system</scope><systemPath>D:/Software/java_dev/java_jdk/lib/tools.jar</systemPath>
</dependency>

这样我们在程序启动后再去动态修改字节码文件的简单案例就完成了。

Java Agent 探针技术相关推荐

  1. Java agent 探针技术(1)-JVM 启动时 premain 进行类加载期增强

    文章目录 1. 简介 2. 使用 Java agent 的步骤 3. 使用示例 3.1 创建实现 ClassFileTransformer 接口的类 3.2 创建使用 ClassFileTransfo ...

  2. Java Agent探针技术

    1.基本概念 Java Agent 是 jdk1.5 引入的特征,此特征为用户提供了在 jvm 将字节码文件读入内存后,jvm 使用对应的字节流在 java 堆中生成 Class 对象之前,用户可以对 ...

  3. 深入Java自动化探针技术的原理和实践

    转至作者 蒋志伟:深入Java自动化探针技术的原理和实践 前言 建议阅读时间 30~40分钟 读者需要对Java JVM 一定了解,文章会系统的介绍Java 探针核心原理和技术实现,总结目前一些主流的 ...

  4. 【Java综合专栏】「引领序幕」全链路追踪原理之Java Agent探针的技术介绍(上篇)

    前提概要 Java调式.热部署.JVM背后的支持者Java Agent: 各个 Java IDE 的调试功能,例如 eclipse.IntelliJ : 热部署功能,例如 JRebel.XRebel. ...

  5. 【JAVA基础☞探针技术】Java探针-Java Agent技术

    个人博客导航页(点击右侧链接即可打开个人博客):大牛带你入门技术栈 1.原理:基于javaAgent和Java字节码注入技术的java探针工具技术原理 2.原理分析 动态代理功能实现说明,我们利用ja ...

  6. Java探针技术详解

    简介 在JVM中运行中,类是通过classLoader加载.class文件进行生成的.在类加载器加载.class文件生成对应的类对象之前时,我们可以通过修改.class文件内容(就是字节码修改技术), ...

  7. Java Agent与ASM字节码介绍

    Java Agent Java Agent是jdk1.5以后引入的,也叫做Java代理. javaAgent是运行方法之前的拦截器.我们利用javaAgent和ASM字节码技术,在JVM加载class ...

  8. 探针技术-JavaAgent 和字节码增强技术-Byte Buddy

    能够基于Java Agent编写出普通类的代理 理解Byte Buddy的作用 能够基于Byte Buddy编写动态代理 1 Byte Buddy Byte Buddy 是一个代码生成和操作库,用于在 ...

  9. 一文带你了解Java Agent

    转载自  一文带你了解Java Agent Java Agent这个技术,对于大多数同学来说都比较陌生,像个黑盒子.但是多多少少又接触过,实际上,我们平时用的很多工具,都是基于Java Agent实现 ...

最新文章

  1. R语言使用ggplot2包使用geom_dotplot函数绘制分组点图(添加均值、中位数)实战(dot plot)
  2. mysql中迅速插入百万条测试数据的方法
  3. ​Unity资源Assetmport New Asset对话框
  4. gdb调试多进程和多线程命令 .
  5. PHP成为首个在内核中嵌入加密库的编程语言
  6. 为什么可能导致睡眠的函数都不能在中断上下文中使用呢?【转】
  7. 一目了然“之IT运维可视化,让运维不再说难!
  8. redis中执行lua脚本命令
  9. boost::detail::sp_typeinfo_相关的测试程序
  10. UICollectionView实现的图片的多选效果(本人已封装好,简单操作)
  11. plsql中文乱码问题(显示问号)
  12. matlab平面问题编程,有限元平面矩形单元MATLAB程序设计.docx
  13. ef oracle 批量更新慢_详解Oracle中多表关联批量插入、批量更新与批量删除
  14. 抖音一个老人和一个机器人歌曲_一个老人孤独去世,一个老人安然离世
  15. ab plc软件_【原创】AB上位机FactoryTalk View的使用教程(上)
  16. PostgreSQL大版本升级(pg_upgrade)11.7升12.2
  17. 使用JWT保护你的Spring Boot应用 - Spring Security实战
  18. 【python】编程语言入门经典100例--30
  19. android edittext不可复制_【EditText】Android设置EditText不可编辑 | 学步园
  20. c语言c9考试软件下载,二级c语言考试系统-无忧考吧二级c语言考试系统下载 v2018.09官方版--pc6下载站...

热门文章

  1. Android系统启动,用过安卓手机的都知道,你还不知道吗?
  2. phper:敢问路在何方
  3. 算法题解_哥德巴赫曾猜测
  4. 【vue】时间戳、中国标准时间等格式转换时间样式(yyyy-MM-dd)
  5. 全球肝脏肿瘤病灶区CT图像分割挑战大赛,联想E-Health夺得冠军
  6. bwapp靶场笔记 -SQL注入篇
  7. 海淀区中小学生知识技能计算机竞赛,海淀区中小学生计算机知识技能竞赛测.doc...
  8. 4rx4 服务器内存2rx4_836220-B21 809081-081 16G 2Rx4 PC4-2400T-R HP服务器内存
  9. 一、SpringCloud 微服务架构
  10. Windows下如何用CMD命令跳转到指定的目录下