Instrumentation 应用简介

article/2025/9/21 1:14:02

引用: java-instrumentation
引用:Instrumentation 新功能

简介

java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。

场景

  • 一个ATM应用程序,允许我们取钱
  • 还有一个Java代理,它允许我们通过衡量投入的时间来衡量我们的ATM的性能

Java代理将修改ATM字节码,允许我们测量提取时间,而无需修改ATM应用程序。

什么是java agent

通常,java代理只是一个特制的jar文件。它利用JVM提供的Instrumentation API来更改JVM中加载的现有字节代码。
要使代理工作,我们需要定义两个方法:

  • premain: 将在JVM启动时使用-javaagent参数静态加载代理
  • agentmain: 将使用Java Attach API将代理动态加载到JVM中。JVM实现(如Oracle,OpenJDK等)都提供动态启动代理的机制。

首先,让我们看看我们如何使用现有的Java代理。
在那之后,我们将看看我们如何从头开始创建一个在字节码中添加所需的功能。

加载 agent

为了能够使用Java代理,我们必须首先加载它。我们有两种加载方式:

  • static: 使用premain来使用-javaagent选项加载代理
  • dynamic: 使用agentmain使用Java Attach API将代理加载到JVM中

接下来,我们将看看每种类型的负载并解释它是如何工作的。

静态加载

在应用程序启动时加载Java代理称为静态加载
静态加载可以在main方法执行前,修改任意代码的字节码。
静态加载使用premain方法,该方法将在任何应用程序代码运行之前运行,
通过如下命令,可以开启静态加载:

# 我们应该始终将-javaagent参数放在-jar参数之前。
# agent_param 可选参数,与main方法接收参数不同,它只能接收一个string类型的参数.
java -javaagent:agent.jar [agent_param] -jar application.jar

动态加载

将Java代理加载到已运行的JVM中的过程称为动态加载
代理程序通过Java Attach API附加到应用程序。

更为复杂的情况是,当我们的ATM应用程序已经在生产环境中运行,我们希望在不停机的情况下,动态的添加监控项,如:动态添加事务的总时间。
下面给出关键代码片段:

//pid 为需要监控的应用程序pid
VirtualMachine jvm = VirtualMachine.attach(jvmPid);
jvm.loadAgent(agentFile.getAbsolutePath());
jvm.detach();

具体的操作步骤是:

  1. 首先启动应用程序
  2. 在确保应用程序正常的启用一段时间后,获取该应用的pid,
  3. 启动agent程序,启动时需传递应用程序pid

coding

应用程序

ATM
ATM类,并提供withdrawMoney取款方法。

public class ATM {private static Logger LOGGER = LoggerFactory.getLogger(ATM.class);private static int TOTAL_MONEY = 10000;/*** 取钱** @param amount* @throws InterruptedException*/public static void withdrawMoney(int amount) throws InterruptedException {//模拟一个取钱的动作Thread.sleep(ThreadLocalRandom.current().nextLong(1000));int rest = TOTAL_MONEY -= amount;LOGGER.info("[Application] 取款 [{}] 元,余额[{}]!", amount, rest);//当账户余额不足时,退出系统if (rest <= 0) {System.exit(1);}}
}

App
应用启动类App,用来启动应用程序.

public class App {private static Logger LOGGER = LoggerFactory.getLogger(App.class);/*** 应用主程序** @param args:* @throws Exception*/public static void main(String[] args) throws Exception {LOGGER.info("**************************************************");LOGGER.info("===========欢迎使用xx银行ATM无人取款机=============");LOGGER.info("**************************************************\n");while (true){ATM.withdrawMoney(new Random().nextInt(100));TimeUnit.SECONDS.sleep(2);}}
}

打包,执行
将上述两个应用程序打包成可执行jar包,执行命令java -jar application.jar,效果如下图:
在这里插入图片描述

Maven生成可以直接运行的jar包的多种方式

静态加载

在编写加载类前,需要先了解下如下几点知识:

premain函数
编写一个 Java 类,包含如下两个方法当中的任何一个:

//[1] 的优先级比 [2] 高,将会被优先执行([1] 和 [2] 同时存在时,[2] 被忽略)public static void premain(String agentArgs, Instrumentation inst);  //[1]
public static void premain(String agentArgs); //[2]

ClassFileTransformer-接口

public interface ClassFileTransformer {byte[]transform(  ClassLoader         loader,String              className,Class<?>            classBeingRedefined,ProtectionDomain    protectionDomain,byte[]              classfileBuffer)throws IllegalClassFormatException;
}

通过这个方法,代理可以得到虚拟机载入的类的字节码(通过 classfileBuffer 参数)。代理的各种功能一般是通过操作这一串字节码得以实现的。

字节码编辑器:AtmTransformer

使用javassist 来操作字节码

public class AtmTransformer implements ClassFileTransformer {private static Logger LOGGER = LoggerFactory.getLogger(AtmTransformer.class);private static final String WITHDRAW_MONEY_METHOD = "withdrawMoney";private String targetClassName;private ClassLoader targetClassLoader;public AtmTransformer(String targetClassName, ClassLoader targetClassLoader) {this.targetClassName = targetClassName;this.targetClassLoader = targetClassLoader;}@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {byte[] byteCode = classfileBuffer;String finalTargetClassName = this.targetClassName.replaceAll("\\.", "/"); //replace . with /if (!className.equals(finalTargetClassName)) {return byteCode;}if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) {LOGGER.info("[Agent] Transforming class ATM");try {ClassPool cp = ClassPool.getDefault();CtClass cc = cp.get(targetClassName);CtMethod m = cc.getDeclaredMethod(WITHDRAW_MONEY_METHOD);//添加局部变量 startTimem.addLocalVariable("startTime", CtClass.longType);//方法头部,为startTime赋值m.insertBefore("startTime = System.currentTimeMillis();");//添加局部变量 endTime , opTimem.addLocalVariable("endTime", CtClass.longType);m.addLocalVariable("opTime", CtClass.longType);StringBuilder endBlock = new StringBuilder();endBlock.append("endTime = System.currentTimeMillis();");endBlock.append("opTime = (endTime-startTime);");endBlock.append("LOGGER.info(\"[Application] 取款总计耗时:\" + opTime + \" ms!\");");//方法尾部,为endTime,optTime赋值,并打印日志m.insertAfter(endBlock.toString());//返回 更改后的字节码byteCode = cc.toBytecode();cc.detach();} catch (NotFoundException | CannotCompileException | IOException e) {LOGGER.error("Exception", e);}}return byteCode;}
}

计算取款耗时代理类:TimeInstrumentationAgent

public class TimeInstrumentationAgent {private static Logger LOGGER = LoggerFactory.getLogger(TimeInstrumentationAgent.class);/**** @param agentArgs 通过命令:java -javaagent:agent.jar [agent_param] -jar application.jar*                  传递的代理参数,只有一个以字符串形式接受* @param inst*/public static void premain(String agentArgs, Instrumentation inst) {LOGGER.info("[Agent] In premain method");String className = "cn.jhs.application.service.ATM";transformClass(className,inst);}private static void transformClass(String className, Instrumentation instrumentation) {Class<?> targetCls = null;ClassLoader targetClassLoader = null;// see if we can get the class using forNametry {targetCls = Class.forName(className);targetClassLoader = targetCls.getClassLoader();transform(targetCls, targetClassLoader, instrumentation);return;} catch (Exception ex) {LOGGER.error("Class [{}] not found with Class.forName");}// otherwise iterate all loaded classes and find what we wantfor(Class<?> clazz: instrumentation.getAllLoadedClasses()) {if(clazz.getName().equals(className)) {targetCls = clazz;targetClassLoader = targetCls.getClassLoader();transform(targetCls, targetClassLoader, instrumentation);return;}}throw new RuntimeException("Failed to find class [" + className + "]");}private static void transform(Class<?> clazz, ClassLoader classLoader, Instrumentation instrumentation) {AtmTransformer dt = new AtmTransformer(clazz.getName(), classLoader);instrumentation.addTransformer(dt, true);try {instrumentation.retransformClasses(clazz);} catch (Exception ex) {throw new RuntimeException("Transform failed for class: [" + clazz.getName() + "]", ex);}}}

打包:agent.jar
将agent程序打包,并在MANIFEST.MF指定Premain-Class,
如下图:
在这里插入图片描述

注意 : 经常有人会忘记指定Can-Retransform-ClassesCan-Redefine-Classes
而抛出类似java.lang.UnsupportedOperationException: adding retransformable transformers is not supported in this environment异常.

<!-- 添加任意 key-value -->
<manifestEntries><Premain-Class>cn.jhs.jvm.agent.instrument.TimeInstrumentationAgent</Premain-> ```Class><Can-Redefine-Classes>true</Can-Redefine-Classes><Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>

执行
使用命令:java -javaagent:agent.jar -jar application.jar执行,效果如下:
在这里插入图片描述

动态加载

在 Java SE 5 当中,开发者只能通过premain来修改完成代理工作。
但是必须在应用启动前指定Instrumentation,这样的方式存在一定的局限性。

在 Java SE 5 的基础上,Java SE 6 针对这种状况做出了改进,开发者可以在应用程序启动之后,启动自己的Instrumentation程序。

在 Java SE 6 的 Instrumentation 当中,有一个跟premain功能类似的agentmain方法,可以在 main 函数运行之后再运行。

agentmain函数

//[1] 的优先级比 [2] 高,将会被优先执行。
public static void agentmain (String agentArgs, Instrumentation inst // [1] 
public static void agentmain (String agentArgs)  // [2]

Premain-Class类似,开发者必须在manifest文件里面设置Agent-Class来指定包含 agentmain 函数的类。

修改:TimeInstrumentationAgent
修改字节码的逻辑不变,唯一的变化就是在TimeInstrumentationAgent添加agentmain方法:

public static void agentmain(String agentArgs, Instrumentation inst) {LOGGER.info("[Agent] In agentmain method");String className = "cn.jhs.application.service.ATM";transformClass(className,inst);}

打包:agent.jar
指定Agent-Class,打包后效果如下图:
在这里插入图片描述

编写监听程序

public class AttachTask implements Runnable{private static Logger LOGGER = LoggerFactory.getLogger(AttachTask.class);private String agentJarFullPath;private String applicationName;/**** @param agentJarFullPath agent.jar 完整路径* @param applicationName 需要添加代理的目标程序*/public AttachTask(String agentJarFullPath, String applicationName) {this.agentJarFullPath = agentJarFullPath;this.applicationName = applicationName;}@Overridepublic void run() {while(true) {Optional<String> jvmProcessOpt = Optional.ofNullable(VirtualMachine.list().stream().filter(jvm -> {LOGGER.info("jvm:{}", jvm.displayName());return jvm.displayName().contains(applicationName);}).findFirst().get().id());//如果没有找到目标应用程序,sleep ,然后从新监听if(!jvmProcessOpt.isPresent()) {try {LOGGER.info("未捕获到应用程序: " + applicationName);TimeUnit.SECONDS.sleep(5L);} catch (InterruptedException e) {//}break;}//File agentFile = new File(agentJarFullPath);try {String jvmPid = jvmProcessOpt.get();LOGGER.info("捕获到应用程序[{}], JVM PID:[{}] ",applicationName,jvmPid);VirtualMachine jvm = VirtualMachine.attach(jvmPid);jvm.loadAgent(agentFile.getAbsolutePath());jvm.detach();LOGGER.info("目标应用程序添加代理成功!");} catch (Exception e) {throw new RuntimeException(e);}//添加完代理之后,直接退出..return ;}}public static void main(String[] args) {new Thread(new AttachTask("D:/instrumentation/agent.jar","application.jar")).start();}
}

执行
1.启动应用 : java -jar application.jar
在这里插入图片描述

2.启动监听程序
在这里插入图片描述
3.监听启动后,应用程序日志变化
在这里插入图片描述


http://chatgpt.dhexx.cn/article/VC5gf6L1.shtml

相关文章

AndroidStudio单元测试——instrumentation

前言&#xff1a;这几天老大要我搞代码自动测试&#xff0c;eclispe的已经解决了&#xff0c;可他们都是用android studio&#xff0c;所以要在android studio 上重新试验&#xff0c;这个有难度啊&#xff0c;android studio国内资料极少&#xff0c;更不要说单元测试了。goog…

Android Instrumentation 简介

Instrumentation 简介 APIs && Source code 官方APIs地址(需要翻墙)Source code Instrumentation 特点 该框架基于JUnit&#xff0c;因此既可以直接使用Junit 进行测试&#xff0c;也可以使用Instrumentation 来测试Android 组件其为Android 应用的每种组件提供了测…

冲突域和广播域?

如何理解冲突域和广播域&#xff1f; 冲突域&#xff1a; 【定义】在同一个冲突域中的每一个节点都能收到所有被发送的帧。简单的说就是同一时间内只能有一台设备发送信息的范围。 【分层】基于OSI的第一层物理层 【设备】第二层设备能隔离冲突域&#xff0c;比如Switch。交…

广播域与冲突域

广播域与冲突域 一个集线器&#xff08;中继器&#xff09;连接的网络成为冲突域&#xff0c;因为每台主机都连接在了同一条线路上&#xff0c;所以传送信息时会冲突。 冲突域是基于第一层(物理层) 而交换机的本质是一个多借口网桥&#xff0c;就是说由交换机组成的网络中&…

冲突域广播域

网络互连设备可以将网络划分为不同的冲突域、广播域。但是&#xff0c;由于不同的网络互连设备可能工作在OSI模型的不同层次上。因此&#xff0c;它们划分冲突域、广播域的效果也就各不相同。如中继器工作在物理层&#xff0c;网桥和交换机工作在数据链路层&#xff0c;路由器工…

广播域和冲突域问题

该图中有几个冲突域几个广播域&#xff1f; 解答&#xff1a; 1、两个广播域&#xff0c;七个冲突域。 这样的&#xff1a;集线器属于物理层&#xff0c;所有接口同属于一个冲突域、一个广播域&#xff1b;交换机属于数据链路层&#xff0c;每个接口是一个单独的冲突域…

冲突域和碰撞域的理解

如何理解冲突域和广播域&#xff1f; 转载 冲突域&#xff1a; 【定义】在同一个冲突域中的每一个节点都能收到所有被发送的帧。简单的说就是同一时间内只能有一台设备发送信息的范围。 【分层】基于OSI的第一层物理层 【设备】第二层设备能隔离冲突域&#xff0c;比如Swi…

如何计算冲突域和广播域-图解分析

如何理解冲突域和广播域?冲突域:【定义】在同一个冲突域中的每一个节点都能收到所有被发送的帧。简单的说就是同一时间内只能有一台设备发送信息的范围。【分层】基于OSI的第一层(数据链路层)物理层【设备】第二层设备能隔离冲突域,比如Switch。交换机能缩小冲突域的范围,交…

有关冲突域的定义

一开始学习网络的时候&#xff0c;对于冲突域和广播域的理解仅仅是从设备上进行理解的&#xff0c;即集线器是一个冲突域&#xff0c;交换机能够分离冲突域&#xff0c;不能够分离广播域&#xff0c;路由器可以分离组播域。至于冲突域到底是什么&#xff0c;怎么样定义的&#…

如何辨别数清冲突域和广播域

1、首先&#xff0c;须知第一层不能隔离冲突域和广播域。例如集线器或者直接连PC 2、其次&#xff0c;第二层可以隔离冲突域&#xff0c;但不能隔离广播域。例如&#xff0c;二层交换机 3、接着&#xff0c;第三层可以隔离广播域&#xff0c;默认隔离冲突域&#xff0c;例如&…

详解广播域和冲突域的区别

总览 1、广播域可以跨网段&#xff0c;而冲突域只是发生的同一个网段的。以太网中&#xff0c;冲突域是由hub组织的。一个hub就是一个冲突域。交换机的每个端口都是一个冲突域。网段&#xff0c;又叫潜在冲突域。 2、冲突域在同一个冲突域中的每一个节点都能收到所有被发送的…

冲突域

一、冲突域 一个站点向另一个站点发出信号&#xff0c;除目的站点外&#xff0c;有多少站点能收到这个信息&#xff0c;这些站点就构成一个冲突域。在同一个冲突域中的每个节点都能收到所有被发送的帧&#xff0c;冲突域是基于第一层&#xff08;物理层&#xff09;。 传统共享…

collision domain - 冲突域

英文&#xff1a; Collision Domain 中文&#xff1a; 冲突域 介绍&#xff1a; 不同主机或设备同时发出的帧可能会互相冲突的网络区域。一条导线上所有工作站的集合&#xff0c;或一个物理网段上所有节点的集合&#xff0c;或以太网上竞争同一带宽的节点的集合都是一个冲突域…

Java 程序员开发常用的工具

1、常用开发工具 2、常用接口测试工具 3、常用远程连接工具 4、一些其他常用工具 5、总结 1、常用开发工具 作为一名Java程序开发人员&#xff0c;可以的选择集成开发环境IDE&#xff08;Integrated Development Environment&#xff09;非常多&#xff0c;得益于Java是一门开…

java编程软件(一) idea

前段时间&#xff0c;有个学弟问我OJ的题目&#xff0c;本来这是一件很正常的事情。 主要是关注点不是那道OJ题&#xff0c;而是他编程所使用的软件———blueJ。既熟悉又陌生的名字。 BlueJ在编写的过程是很不方便的&#xff0c;当然&#xff0c;在类与类之间的关系倒是能很好…

学java编程需要安装什么软件?

近几年学编程的同学或者想学习编程的同学越来越多&#xff0c;提到编程java语言肯定是里面的佼佼者&#xff0c;今天小千就来给大家介绍一下学Java编程都需要安装什么软件&#xff0c;自学的同学有福了。 1.java环境 想要从事Java开发&#xff0c;那么Java运行环境肯定是你首先…

Java编写软件

资源&#xff1a;UIControlsJavaFX.exe 1. 测试环境 JRE7U21 Win7 2. 如果JRE 6运行不成功则说明不支持JavaFX runtime 3. 双击UIControlsJavaFX.jar可以直接运行 4. 右键用rar软件打开UIControlsJavaFX.jar可以查看源码和JavaDoc UIControlsJavaFX_V2.exe 版本2&#xff…

5款新手常用的java编程工具,有你正在用的吗?

新手如果想成为一名合格的java程序员工程师&#xff0c;不但要熟练使用各种框架&#xff0c;而且还有明白框架是如何实现的各种原理&#xff0c;例如像jvm虚拟机的原理&#xff0c;优化&#xff0c;熟练掌握jvm能让你写出性能更好的代码&#xff0c;还有池技术&#xff0c;对象…

推荐5款好用的Java软件,初学者必看

“工欲善其事必先利其器”&#xff0c;想要学好Java&#xff0c;除了要有好的学习资源之外&#xff0c;还要有一套适合自己的Java软件&#xff0c;好的软件能极大提高你的学习和工作效率。那么&#xff0c;学Java用什么软件好呢&#xff1f;下面小千为大家推荐5款好用的Java软件…

恶意流量监测开源系统:Maltrail

20210309 - 0. 引言 一般来说&#xff0c;通过IDS来监测一些攻击流量&#xff0c;或者说恶意流量也是可以的&#xff1b;但是现在看到的这个开源软件[1]是专门利用IOC来识别恶意流量&#xff1b;具体细节我没有深入去研究&#xff0c;例如流量捕获部分到底是什么引擎来启动的…