Java核心: 使用instrumentation

2024-05-29 11:44

本文主要是介绍Java核心: 使用instrumentation,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在上一篇Java核心: 注解处理器我们提到,通过实现AbstractProcessor,并调用javac -processor能够生成代码来实现特殊逻辑。不过它存在两个明显的问题:

  1. 只能新增源文件来扩展逻辑,无法修改现有的类或方法
  2. 必须有一个单独的编译过程,调用javac -processor或者用Maven的annotationProcessor,不适用于已经编译好的jar

Java核心: 使用asm操作字节码讲了怎么使用asm修改类的字节码,解决了上面的第1个问题。不过即使用ASM,仍然需要自己创建ClassReader读取class文件,自定义ClassLoader加载字节码,虽然不需要单独编译,但是这些操作是需要额外做的。

instrumentation提供的能力是可以注册字节码转换器,在类加载时先将字节码传递给字节码转换器,转换之后才被真正的加载。使用者来并不感知这个过程,只是在启动程序的命令中添加一个-javaagent参数即可,应该说使用体验还是不错的。

java -javaagent:AsmToString.jar=hoho -cp ./swing-1.0-SNAPSHOT.jar  com.keyniu.asm.diagnostic.DiagnosticMain

1. 定义和使用

接下来我们要做的是定义一个最简单的instrumentation实现,仅仅是打印-javaagent后带的参数,以及执行过程加载的所有的类、它所属的ClassLoader。首先要做的是创建工程,后续用于打包成agent的jar,这里我们命名为AsmToString.jar。这里使用mvn archetype:generate创建工程

mvn archetype:generate  \-DarchetypeGroupId=org.apache.maven.archetypes \-DarchetypeArtifactId=maven-archetype-quickstart \-DarchetypeVersion=1.4 \-DgroupId=com.keyniu.agent \-DartifactId=AsmToString \-Dversion=0.1 \-Dpackage=com.keyniu.agent \-DinteractiveMode=false
1. 实现Premain

现在我们可以在工程下定义我们的Premain类ToStringGenerator,这个类的核心是实现public static void premain(final String arg, Instrumentation ins)方法,这里我们只打印参数arg,以及传递给字节码转换器的信息。使用Instrumentation.addTransformer注册字节码转换器ClassFileTransformer实现类。

package org.keyniu.agent;import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;public class ToStringGenerator {public static void premain(final String arg, Instrumentation ins) {System.out.println("arg: " + arg);ins.addTransformer(new ClassFileTransformer() {public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {System.out.println("ClassLoader: " + loader.getName());System.out.println("className: " + className);System.out.println("classBeingRedefined: " + classBeingRedefined);return classfileBuffer;}});}
}

在继续之前我们来仔细看看ClassFileTransformer.tranform的定义,它有5个参数

参数

说明

举例

loader

加载当前要进行字节码转换的类的类加载器

app

className

当前要进行字节码转换的类的类名

org/objectweb/asm/util/TraceRecordComponentVisitor

classBeingRefefined

被重定义(Instrumentation.redefineClasses)的类的Class对象,否则是null

null

protectionDomain

目标类的安全权限

null

classfileBuffer

转换之前的字节码

字节码数组

2. 定义MANIFEST.MF

为了让-javaagent能使用这个jar,需要在MANIFEST.MF里添加Premain-Class的定义。在Maven的build -> plugins -> plugin配置maven-jar-plugin。

<plugin><artifactId>maven-jar-plugin</artifactId><version>3.2.0</version><configuration><archive><manifest><addDefaultImplementationEntries>true</addDefaultImplementationEntries><addDefaultSpecificationEntries>true</addDefaultSpecificationEntries><addClasspath>true</addClasspath></manifest><manifestEntries><Premain-Class>org.keyniu.agent.ToStringGenerator</Premain-Class></manifestEntries></archive></configuration>
</plugin>

这里我们重点关注和的配置,我们看一下每个配置的作用。addDefaultImplementationEntries用于在MANIFEST.MF文件中生成

Implementation-Title: AsmToString
Implementation-Version: 1.0-SNAPSHOT

addDefaultSpecificationEntries会在MANIFEST.MF里记录Specification相关信息

Specification-Title: AsmToString
Specification-Version: 1.0

addClassPath则会在MANIFEST.MF文件中添加一行Class-Path

Class-Path: lib/dependency1.jar lib/dependency2.jar

manifestEntries用于添加我们自定义的属性,这里是添加Premain-Class属性以及它对应的类信息。现在可以通过mvn package打包了。

3. 使用javaagent

在拿到agent的jar包之后,就可以在另外一个工程里使用这个jar包了。复用在Java核心: 使用asm操作字节码里定义的类Account,代码如下,只是定义了一个remain字段、transfer方法。

package com.keyniu.asm;
import com.keyniu.asm.diagnostic.Diagnostic;
import com.keyniu.asm.lombok.ToString;
@ToString
public class Account {private int remain = 99;@Diagnosticpublic void transfer(String sb, int amount) {System.out.println("transfer to " + sb + " amount: " + amount);try {Thread.sleep((int) (Math.random() * 1000));} catch (InterruptedException e) {}}
}

用来测试的maif方法也极其简单,仅仅是创建Account实例,打印toString方法的输出,因为我们并没有定义toString方法,所以输出是Object默认的com.keyniu.asm.Account@3feba861

package com.keyniu.testagent;
import com.keyniu.asm.Account;
public class TestAgent {public static void main(String[] args) {Account account = new Account();System.out.println(account.toString());}
}

现在可以使用-javaagent来测试执行了,我们看看执行的命令以及返回的输出

java -javaagent:D:/Workspace/HelloJava17/AsmToString-1.0-SNAPSHOT.jar=hoho -classpath ./swing-1.0-SNAPSHOT.jar  com.keyniu.testagent.TestAgent

2. 实战: 生成toString方法

上一节中我们创建了一个javaagent的jar,整个流程已经跑通了,只是这个agent本身并没有什么实际意义,仅仅是打印我们加载的所有类而已。现在我们让他变得有用一点,为注解了@ToString的类生成toString方法,案例中Account类标注了@ToString,关于ToString的代码,asm的更详细介绍,可以看Java核心: 使用asm操作字节码。这里我们默认读者是会asm的基本操作的。

1.引入asm依赖

首先要做的是在AsmToString工程里引入asm类库的依赖。

<dependency><groupId>org.ow2.asm</groupId><artifactId>asm</artifactId><version>9.7</version> <!-- 使用时请检查是否有更新的版本 -->
</dependency><dependency><groupId>org.ow2.asm</groupId><artifactId>asm-util</artifactId><version>9.7</version> <!-- 使用时请检查是否有更新的版本 -->
</dependency>
2. 定义ToStringClassVisitor

代码完全复用Java核心: 使用asm操作字节码里的ToStringClassVisitor类,代码如下

package org.keyniu.agent;import org.objectweb.asm.*;import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;public class ToStringClassVisitor extends ClassVisitor {private boolean isAnnotated = false;private Map<String, String> fields = new LinkedHashMap<>();private String className;@Overridepublic void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {this.className = name;super.visit(version, access, name, signature, superName, interfaces);}protected ToStringClassVisitor(ClassVisitor classVisitor) { // classVisitor接收一个ClassWriter用于生成字节码super(Opcodes.ASM9, classVisitor);}@Overridepublic AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {if ("Lcom/keyniu/asm/lombok/ToString;".equals(descriptor)) {isAnnotated = true;}return super.visitAnnotation(descriptor, visible);}@Overridepublic FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {fields.put(name, descriptor);return super.visitField(access, name, descriptor, signature, value);}@Overridepublic void visitEnd() {if (isAnnotated && fields.size() > 0) {MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "toString", "()Ljava/lang/String;", null, null);mv.visitCode();// 创建StringBuilder并调用构造函数mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");mv.visitInsn(Opcodes.DUP);mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);// 拼接类名,比如 Account(mv.visitLdcInsn(className.substring(className.lastIndexOf("/") + 1) + "(");mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);Iterator<Map.Entry<String, String>> it = fields.entrySet().iterator();while (it.hasNext()) {Map.Entry<String, String> kv = it.next();String fieldName = kv.getKey();String fieldDesc = kv.getValue();mv.visitVarInsn(Opcodes.ALOAD, 0); // 载入thismv.visitFieldInsn(Opcodes.GETFIELD, className, fieldName, fieldDesc);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(" + fieldDesc + ")Ljava/lang/StringBuilder;", false);if (it.hasNext()) {mv.visitLdcInsn(",");mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);}}// 拼接右括号,Account(字段=值)mv.visitLdcInsn(")");mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);mv.visitInsn(Opcodes.ARETURN);mv.visitMaxs(0, 0);mv.visitEnd();}super.visitEnd();}
}
3.修改ToStringGenerator实现
package org.keyniu.agent;import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;public class ToStringGenerator {public static void premain(final String arg, Instrumentation ins) {System.out.println("arg: " + arg);ins.addTransformer(new ClassFileTransformer() {public byte[] transform(ClassLoader load, String clazz, Class<?> redefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {ClassReader cr = new ClassReader(classfileBuffer);// 写字节码ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);ClassVisitor cv = new ToStringClassVisitor(cw);// 跑事件流cr.accept(cv, ClassWriter.COMPUTE_FRAMES); // 0表示不计算最大堆栈大小和最大局部变量表大小// 获取修改后的字节码并写入到文件byte[] transformedBytes = cw.toByteArray();return transformedBytes;}});}
}
4. 查看运行输出

我们将AsmToString的jar拷到D:盘下,使用如下命令执行TestAgent,我们看到Accout的toString已经生成了并打印Account类中的字段。

java -javaagent:D:/AsmToString-1.0-SNAPSHOT.jar=hoho -classpath ./swing-1.0-SNAPSHOT.jar  com.keyniu.testagent.TestAgent

这篇关于Java核心: 使用instrumentation的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1013576

相关文章

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

Python使用国内镜像加速pip安装的方法讲解

《Python使用国内镜像加速pip安装的方法讲解》在Python开发中,pip是一个非常重要的工具,用于安装和管理Python的第三方库,然而,在国内使用pip安装依赖时,往往会因为网络问题而导致速... 目录一、pip 工具简介1. 什么是 pip?2. 什么是 -i 参数?二、国内镜像源的选择三、如何

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Linux使用nload监控网络流量的方法

《Linux使用nload监控网络流量的方法》Linux中的nload命令是一个用于实时监控网络流量的工具,它提供了传入和传出流量的可视化表示,帮助用户一目了然地了解网络活动,本文给大家介绍了Linu... 目录简介安装示例用法基础用法指定网络接口限制显示特定流量类型指定刷新率设置流量速率的显示单位监控多个

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程