JavaAgent简介和使用

JavaAgent是在JDK5之后提供的新特性,也可以叫java代理。

开发者通过这种机制(Instrumentation)可以在类加载class文件之前修改方法的字节码(此时字节码尚未加入JVM),动态更改类方法实现AOP,提供监控服务如;方法调用时长、可用率、内存等。

1、Java字节码简介

Java字节码是众多字节码增强技术的知识基础。

Java语言写出的源代码首先需要编译成class文件,即字节码文件,然后被JVM加载并运行,每个 class文件 具有如下固定的数据格式

ClassFile {
    u4             magic;           // 魔数,固定为0xCAFEBABE
    u2             minor_version;   // 次版本
    u2             major_version;   // 主版本,常见版本:52对应1.8,51对应1.7,其他依次类推
    u2             constant_pool_count;                     // 常量池个数
    cp_info        constant_pool[constant_pool_count-1];    // 常量池定义
    u2             access_flags;    // 访问标志:ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT等
    u2             this_class;      // 类索引
    u2             super_class;     // 父类索引
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

可以看到, class文件总是一个魔数开头,后面跟着版本号,然后就是常量定义、访问标志、类索引、父类索引、接口个数和索引表、字段个数和索引表、方法个数和索引表、属性个数和索引表

class文件本质上是一个字节码流,每个字节码所处的位置代表着一定的指令和含义。如何对class文件中定义的指令和字节码进行解读、增强定义、编排,这是字节码增强技术所要完成的事情。

了解Java字节码有助于字节码增强的开发,但并不是实现字节码增强开发的必要条件,最新主流的众多字节码增强工具框架类库都将字节码的编排进行了不同程度封装,在可读性、易编排性、排错性上提供开发便利性,学习曲线和开发难度得到了较好的改善。

2、Java字节码增强

对于字节码增强的开发来说,JVMTI是一个在实践中应该被熟悉的工具技术。

JVM从1.5版本开始提供 JVM Tool Interface ,这是JVM对外的、用于Java应用监控和调试的一系列工具接口,是JVM平台调试架构的重要组成部分。

The JavaPlatform Debugger Architecture is structured as follows:
           Components                          Debugger Interfaces

                /    |--------------|
               /     |     VM       |
 debuggee ----(      |--------------|  <------- JVM TI - Java VM Tool InterfaceJvm服务端调试接口)
               \     |   back-end   |
                \    |--------------|
                /           |
 comm channel -(            |  <--------------- JDWP - Java Debug Wire ProtocolJava调试通信协议)
                \           |
                     |--------------|
                     | front-end    |
                     |--------------|  <------- JDI - Java Debug Interface (客户端调试接口和调试应用)
                     |      UI      |
                     |--------------|

JVM启动支持加载agent代理,而agent代理本身就是一个JVM TI的客户端,其通过监听事件的方式获取Java应用运行状态,调用JVM TI提供的接口对应用进行控制。

我们可以看下Java agent代理的两个入口函数定义

// 用于JVM刚启动时调用,其执行时应用类文件还未加载到JVM
public static void premain(String agentArgs, Instrumentation inst);

// 用于JVM启动后,在运行时刻加载
public static void agentmain(String agentArgs, Instrumentation inst);

这两个入口函数定义分别对应于JVM TI专门提供了执行 字节码增强(bytecode instrumentation) 的两个接口。

premain加载时刻增强(JVM 启动时加载),类字节码文件在JVM加载的时候进行增强,。

agentMain动态增强(JVM 运行时加载),已经被JVM加载的class字节码文件,当被修改或更新时进行增强

这两个接口都是从JDK 1.6开始支持。

我们无需对上面JVM TI提供的两个接口规范了解太多,Java Agent和 Java Instrument类包 封装好了字节码增强的上述接口通信。

上面我们已经说到了, 有两处地方可以进行 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、premain演示

1、定义premain-class 定义两个接口方法

public class AgentDemo {
    /**
     * JVM 首先尝试在代理类上调用以下方法
     * 该方法在main方法之前运行,
     * 与main方法运行在同一个JVM中
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("=========premain方法执行 1========");
        System.out.println("agentArgs:="+agentArgs);
    }
 
    /**
     * 候选的、兜底 方法:
     * 如果不存在 premain(String agentArgs, Instrumentation inst)
     * 则会执行 premain(String agentArgs)
     *
     */
    public static void premain(String agentArgs) {
        System.out.println("=========premain 方法执行 2========");
        System.out.println("agentArgs:="+agentArgs);
    }

}

MANIFEST.MF 文件可以自己定义,也可以自动生成

2、编写MANIFEST.MF 文件

注意:最后需要有一个空行

Manifest-Version: 1.0
Can-Redefine-Classes: true   # true表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes: true    # true 表示能重转换此代理所需的类,默认值为 false (可选)
Premain-Class:  com.xiaoxu.javaagent.AgentDemo   #premain方法所在类的位置

2、或者maven项目,在pom.xml加入

      <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.xiaoxu</groupId>
    <artifactId>java-agent</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>java-agent</name>

    <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>

    </dependencies>

    <build>
        <plugins>
<!--            <plugin>-->
<!--                <groupId>org.apache.maven.plugins</groupId>-->
<!--                <artifactId>maven-compiler-plugin</artifactId>-->
<!--                <version>3.6.1</version>-->
<!--                <configuration>-->
<!--                    <source>${java.version}</source>-->
<!--                    <target>${java.version}</target>-->
<!--                </configuration>-->
<!--            </plugin>-->
            <plugin>
                <artifactId>maven-shade-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <shadedArtifactAttached>false</shadedArtifactAttached>
                            <createDependencyReducedPom>true</createDependencyReducedPom>
                            <createSourcesJar>true</createSourcesJar>
                            <shadeSourcesContent>true</shadeSourcesContent>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <manifestEntries>
                                        <Premain-Class>com.xiaoxu.javaagent.AgentDemo</Premain-Class>
                                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                                    </manifestEntries>
                                </transformer>
                            </transformers>
                            <artifactSet>
                                <excludes>
                                    <exclude>*:gson</exclude>
                                    <exclude>io.netty:*</exclude>
                                    <exclude>io.opencensus:*</exclude>
                                    <exclude>com.google.*:*</exclude>
                                    <exclude>com.google.guava:guava</exclude>
                                    <exclude>org.checkerframework:checker-compat-qual</exclude>
                                    <exclude>org.codehaus.mojo:animal-sniffer-annotations</exclude>
                                    <exclude>io.perfmark:*</exclude>
                                    <exclude>org.slf4j:*</exclude>
                                </excludes>

                                <!-- 将javassist包打包到Agent-->

                                <includes>
                                    <include>javassist:javassist:jar:</include>
                                </includes>
                            </artifactSet>
                            <filters>
                                <filter>
                                    <artifact>net.bytebuddy:byte-buddy</artifact>
                                    <excludes>
                                        <exclude>META-INF/versions/9/module-info.class</exclude>
                                    </excludes>
                                </filter>
                            </filters>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

3、Agent的简单使用

1、JVM 启动时加载

我们直接在 JVM 启动参数中加入 -javaagent 参数并指定 jar 文件的位置

# 指定agent程序并运行该类
java -javaagent:-javaagent:E:\IdeaProject\FrameworkSources\JavaAgent\target\java-agent-0.0.1-SNAPSHOT.jar  PremainTest

或者

VM options: -javaagent:E:\IdeaProject\FrameworkSources\JavaAgent\target\java-agent-0.0.1-SNAPSHOT.jar

定义一个测试类执行
在这里插入图片描述

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。

4、ClassFileTransformer字节码转换器

我们需要了解的是,上述入口函数传入的第二个参数Instrumentation实例,即Java Instrument类 java.lang.instrument.Instrumentation ,查看其类定义,可以看到其提供的核心方法只有一个addTransformer,用于添加多个ClassFileTransformer

// 说明:添加ClassFileTransformer
// 第一个参数:transformer,类转换器
// 第二个参数:canRetransform,经过transformer转换过的类是否允许再次转换
void Instrumentation.addTransformer(ClassFileTransformer transformer, boolean canRetransform)

而 ClassFileTransformer 则提供了tranform()方法,用于对加载的类进行增强重定义,返回新的类字节码流。

需要特别注意的是,若不进行任何增强,当前方法返回null即可,若需要增强转换,则需要先拷贝一份classfileBuffer,在拷贝上进行增强转换,然后返回拷贝。

// 说明:对类字节码进行增强,返回新的类字节码定义
// 第一个参数:loader,类加载器
// 第二个参数:className,内部定义的类全路径
// 第三个参数:classBeingRedefined,待重定义/转换的类
// 第四个参数:protectionDomain,保护域
// 第五个参数:classfileBuffer,待重定义/转换的类字节码(不要直接在这个classfileBuffer对象上修改,需拷贝后进行)
// 注:若不进行任何增强,当前方法返回null即可,若需要增强转换,则需要先拷贝一份classfileBuffer,在拷贝上进行增强转换,然后返回拷贝。
byte[] ClassFileTransformer.transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte classfileBuffer)

4.1、Javassist 修改字节码

重写ClassFileTransformer 接口的唯一方法 transform() 方法,返回值是转换后的类字节码文件

在 transform 方法中,通过 Javassist 修改字节码

1、自定义字节码转换器

/**
 * @author xiaoxu
 * @description 自定义字节码转换器
 * @date 2022/10/8
 */
public class CustomTransformer implements ClassFileTransformer {

	/**
	 * byte[]: 类字节码文件数组
	 * className: 类文件路径
	 */
	public byte[] transform(ClassLoader loader,
	                        String className,
	                        Class<?> classBeingRedefined,
	                        ProtectionDomain protectionDomain,
	                        byte[] classfileBuffer) throws IllegalClassFormatException {
		byte[] bytes = null;

		if ("com/xiaoxu/javaagent/transformer/HelloWorld".equals(className)) {
			try {
				// 获得一个ClassPool对象
				ClassPool cp = ClassPool.getDefault();
				// classPool根据需要读取类文件以构造CtClass对象
				CtClass cc = cp.get("com.xiaoxu.javaagent.transformer.HelloWorld");
				CtMethod method = cc.getDeclaredMethod("say");

				// 方法增强
				method.insertBefore("{System.out.println(\"before say()\");}");
				method.insertAfter("{System.out.println(\"after say()\");}");

				// 将增强类 转字节码返回
				bytes = cc.toBytecode();
			} catch (NotFoundException e) {
				e.printStackTrace();
			} catch (CannotCompileException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}else {
			System.out.println(className +" 字节码转换器略过,不是目标字节码");
		}
		return bytes;
	}

}

2、定义premain-class

public class AgentClassFileTransformerPreMain {


	public static void premain(String args, Instrumentation inst) throws UnmodifiableClassException {
		System.out.println("premian: 执行啦!");

		// 增加字节码增强的转换器
		inst.addTransformer(new CustomTransformer(),true);

		// main方法执行前 获取全部已加载的class
		Class[] allLoadedClasses = inst.getAllLoadedClasses();
		for (Class loadedClass : allLoadedClasses) {
			System.out.println(loadedClass.getSimpleName());
		}

	}
}

打成jar包

3、定义目标类

public class HelloWorld {

	public void say(){
		System.out.println("xiaoxu hello world!");
	}
}

4、使用javaagent参数 测试

public class PremainTest {

	public static void main(String[] args) {
		new HelloWorld().say();
	}

}

在这里插入图片描述

动态修改字节码成功

Javassist 是一个非常早的字节码操作类库,开始于1999年,

它能够支持两种编辑方式:

  • 源码级别
  • 字节码指令级别

相比于晦涩的字节码级别,源码级别更加人性化,代码编写起来更加易懂。

5、Java字节码增强工具关系图

在这里插入图片描述

6、字节码增强组件对比

在这里插入图片描述

综上:引出ByteBuddy

参考文章
https://blog.csdn.net/crazymakercircle/article/details/126579528?spm=1001.2014.3001.5502

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Java Agent是Arthas使用的技术,是Skywalking使用的技术,是一份十分重要的技术。 课程的稀缺性在此之前,市面上并没有针对Java Agent进行系统介绍的课程。 通过搜索引擎查找,会发现与Java Agent相关的内容大多是个人知识总结分享的内容。这些内容有如下特点:内容质量不一详略程度不一学习难度千差万别总体上来说,学习者很难有一个整体认知、系统学习的过程。 课程的设计目标 在构思课程内容时,本课程带有以下目标:课程学习梯度:从简单到复杂,让学习者有一个循序渐进的理解过程。构造完整、统一的知识体系:不是零散的知识点堆砌,而是有一个统一的贯穿始终的知识框架。具有可操作性的代码示例,不只是讲概念,更注意于实践。课程内容安排 本课程通过四章内容对Java Agent相关知识进行讲解:第一章,介绍Agent Jar的三个组成部分:Manifest、Agent Class和ClassFileTransformer。第二章,介绍Agent Jar的两种启动方式:从命令行启动和使用Attach机制启动。第三章,介绍如何利用Instrumentation API来实现Agent Jar的功能。第四章,Java Agent的应用与技巧。 通过本课程的学习,让同学们更好地建立起一个完整的知识体系:  讲师介绍我叫刘森,南京师范大学研究生毕业,2015年获得信息系统项目管理师(高级),2014年获得系统集成项目管理工程师(中级)。 目前,我的课程都是围绕着“Java字节码”技术展开: 《Java Agent基础篇》是在一个运行JVM当中提供修改字节码的机会《Java ASM系列》(免费课程)是一个操作字节码的类库《Java 8 ClassFile》专注于字节码的理论知识,入选为“51CTO数字化人才证书项目认证课程” 因此,我对字节码技术有较为深入的研究和理解,大家想学习字节码的技术可以找我:字节码技术找刘森,轻松学习又省心~~~ 

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白鸽呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值