javaagent
- 2022 年 1 月 20 日
本文字数:7960 字
阅读完需:约 26 分钟
在 java
的世界里,有时候你会听到这样一些名词 - 探针
、字节码插桩
,这背后用到的技术其实就叫做javaagent
,今天我就来揭秘一下 javaagent
背后的原理。
概念
javaagent 是什么?
javaagent 是 java 命令的一个参数,参数 javaagent
可以指定一个 jar
包,并且对该 jar
包有两个要求:
这个
jar
包的MANIFEST.MF
文件必须指定Premain-Class
项;Premain-Class
指定的类必须实现premain()
方法。
premain()
方法,从字面上理解,就是运行在 main 函数之前的类。当 Java 虚拟机启动时,在执行 main 函数之前,JVM 会先执行 -javaagent
所指定 jar 包内 Premain-Class
指定类的 premain()
方法。
从本质上讲,Java Agent 是一个遵循一组严格约定的 Java 类。 上面说到 javaagent
命令要求指定的类中必须要有 premain()
方法,并且对 premain()
方法的签名也有要求,签名必须满足以下两种格式:
public static void premain(String agentArgs, Instrumentation instrumentation);
public static void premain(String agentArgs);
JVM 会优先加载 带 Instrumentation
签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法。
Instrumentation
类定义如下:
public interface Instrumentation {
// 添加一个 Calss 文件转换器,转换器用于改变 Class 二进制流的数据
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void addTransformer(ClassFileTransformer transformer);
boolean removeTransformer(ClassFileTransformer transformer);
boolean isRetransformClassesSupported();
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
boolean isRedefineClassesSupported();
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;
boolean isModifiableClass(Class<?> theClass);
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);
long getObjectSize(Object objectToSize);
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
void appendToSystemClassLoaderSearch(JarFile jarfile);
boolean isNativeMethodPrefixSupported();
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}
如何使用 javaagent?
使用 javaagent 需要几个步骤:
定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入 Can-Redefine-Classes 和 Can-Retransform-Classes 选项;
创建一个 Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己实现;
将上述文件打成 jar 包;
使用 -javaagent:/path/to/agent.jar 启动要代理的方法。
在执行以上步骤之后,JVM 会先执行 premain() 方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。当然,遗漏的主要是系统类,因为很多系统类先于 Agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main() 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,那么就可以去做重写类这样的操作,结合第三方的字节码编译工具,比如 ASM,javassist,ByteBuddy 等来重写字节码。
Demo
实现 javaagent 我们需要搭建两个工程,一个工程是用来承载 javaagent 类,单独的打成 jar 包;一个工程是 javaagent 需要去代理的类。即 javaagent 会在这个工程中的 main 方法启动之前去做一些事情。
javaagent 工程
创建一个包含 premain() 方法的类:
package com.zhufan.javaagent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
/**
* @author zhufan
* @date 2022/1/20 16:10
*/
public class DemoAgent {
public static void premain(String agentArgs, Instrumentation instrumentation) {
final Class[] allLoadedClasses = instrumentation.getAllLoadedClasses();
instrumentation.addTransformer(new DemoClassFileTransformer(), true);
}
private static class DemoClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("transform class:" + className);
return new byte[0];
}
}
}
使用 maven 插件生成 MANIFREST.MF 文件:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.javaagent.example</groupId>
<artifactId>javaagent</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<premain-class>com.zhufan.javaagent.DemoAgent</premain-class>
<can.redefine.classes>true</can.redefine.classes>
<can.retransform.classes>true</can.retransform.classes>
</properties>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.22.0-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<!--打包时,设置MANIFEST.MF的信息-->
<manifestEntries>
<Premain-Class>${premain-class}</Premain-Class>
<Can-Redefine-Classes>${can.redefine.classes}</Can-Redefine-Classes>
<Can-Retransform-Classes>${can.retransform.classes}</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<!-- 防止引用该jar的项目也使用javassist,与jar中javassist冲突-->
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createSourcesJar>true</createSourcesJar>
<relocations>
<relocation>
<pattern>javassist</pattern>
<shadedPattern>com.zhufan.javaagent.javassist</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
应用工程
新建一个工程,编写一个包含 main() 方法的类:
public class TestMain {
public static void main(String[] args) {
System.out.println("main start");
}
}
使用
java -javaagent:/path/to/DemoAgent.jar TestMain
启动即可,会看到如下输出:
/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/bin/java -javaagent:/Users/zhufan/GitRepository/javaagent-demo/target/javaagent-1.0-SNAPSHOT.jar -javaagent:/Applications/IntelliJ IDEA for M1 2021.1.1.app/Contents/lib/idea_rt.jar=64371:/Applications/IntelliJ IDEA for M1 2021.1.1.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/lib/tools.jar:/Users/zhufan/GitRepository/zhufan/target/classes:/Users/zhufan/.m2/repository/io/netty/netty-all/4.1.48.Final/netty-all-4.1.48.Final.jar:/Users/zhufan/.m2/repository/org/javassist/javassist/3.27.0-GA/javassist-3.27.0-GA.jar:/Users/zhufan/.m2/repository/org/apache/commons/commons-lang3/3.10/commons-lang3-3.10.jar:/Users/zhufan/.m2/repository/com/google/guava/guava/29.0-jre/guava-29.0-jre.jar:/Users/zhufan/.m2/repository/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar:/Users/zhufan/.m2/repository/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar:/Users/zhufan/.m2/repository/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar:/Users/zhufan/.m2/repository/org/checkerframework/checker-qual/2.11.1/checker-qual-2.11.1.jar:/Users/zhufan/.m2/repository/com/google/errorprone/error_prone_annotations/2.3.4/error_prone_annotations-2.3.4.jar:/Users/zhufan/.m2/repository/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar:/Users/zhufan/.m2/repository/org/openjdk/jol/jol-core/0.10/jol-core-0.10.jar:/Users/zhufan/.m2/repository/org/apache/kafka/kafka-clients/0.11.0.3/kafka-clients-0.11.0.3.jar:/Users/zhufan/.m2/repository/net/jpountz/lz4/lz4/1.3.0/lz4-1.3.0.jar:/Users/zhufan/.m2/repository/org/xerial/snappy/snappy-java/1.1.2.6/snappy-java-1.1.2.6.jar:/Users/zhufan/.m2/repository/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar com.fanzhu.adapter.AdapterTest
transform class:sun/nio/cs/ThreadLocalCoders
transform class:sun/nio/cs/ThreadLocalCoders$1
transform class:sun/nio/cs/ThreadLocalCoders$Cache
transform class:sun/nio/cs/ThreadLocalCoders$2
transform class:com/intellij/rt/execution/application/AppMainV2$Agent
transform class:com/intellij/rt/execution/application/AppMainV2
transform class:java/lang/NoSuchMethodException
transform class:java/lang/reflect/InvocationTargetException
transform class:com/intellij/rt/execution/application/AppMainV2$1
transform class:java/net/Socket
transform class:java/lang/invoke/MethodHandleImpl
transform class:java/net/InetSocketAddress
transform class:java/net/SocketAddress
transform class:java/net/InetAddress
transform class:java/lang/invoke/MethodHandleImpl$1
transform class:java/net/InetSocketAddress$InetSocketAddressHolder
transform class:java/lang/invoke/MethodHandleImpl$2
transform class:java/util/function/Function
transform class:java/lang/invoke/MethodHandleImpl$3
transform class:java/lang/invoke/MethodHandleImpl$4
transform class:java/lang/ClassValue
transform class:java/lang/ClassValue$Entry
transform class:sun/security/action/GetBooleanAction
transform class:java/net/InetAddress$1
transform class:java/lang/ClassValue$Identity
transform class:java/lang/ClassValue$Version
transform class:java/lang/invoke/MemberName$Factory
transform class:java/lang/invoke/MethodHandleStatics
transform class:java/lang/invoke/MethodHandleStatics$1
transform class:sun/misc/PostVMInitHook
transform class:sun/misc/PostVMInitHook$1
transform class:java/net/InetAddress$InetAddressHolder
transform class:java/net/InetAddress$Cache
transform class:java/net/InetAddress$Cache$Type
transform class:java/net/InetAddressImplFactory
transform class:sun/usagetracker/UsageTrackerClient
transform class:java/util/concurrent/atomic/AtomicBoolean
transform class:sun/usagetracker/UsageTrackerClient$1
transform class:sun/usagetracker/UsageTrackerClient$4
transform class:java/net/Inet6AddressImpl
transform class:java/net/InetAddressImpl
transform class:sun/usagetracker/UsageTrackerClient$3
transform class:java/net/InetAddress$2
transform class:sun/net/spi/nameservice/NameService
transform class:java/net/Inet4Address
transform class:java/io/FileOutputStream$1
transform class:java/net/SocksSocketImpl
transform class:java/net/SocksConsts
transform class:java/net/PlainSocketImpl
transform class:java/net/AbstractPlainSocketImpl
transform class:java/net/SocketImpl
transform class:java/net/SocketOptions
transform class:java/net/AbstractPlainSocketImpl$1
transform class:java/net/Inet6Address
transform class:sun/launcher/LauncherHelper
transform class:java/net/Inet6Address$Inet6AddressHolder
transform class:sun/misc/URLClassPath$FileLoader$1
transform class:java/net/SocketException
transform class:java/io/IOException
transform class:java/net/SocksSocketImpl$3
transform class:java/net/ProxySelector
transform class:sun/net/spi/DefaultProxySelector
transform class:sun/net/spi/DefaultProxySelector$1
transform class:sun/net/NetProperties
transform class:sun/net/NetProperties$1
transform class:java/util/Properties$LineReader
transform class:com/fanzhu/adapter/AdapterTest
transform class:sun/launcher/LauncherHelper$FXHelper
transform class:java/lang/Class$MethodArray
transform class:java/net/URI
transform class:com/fanzhu/adapter/Cock
transform class:java/net/URI$Parser
transform class:com/fanzhu/adapter/Duck
transform class:java/lang/Void
transform class:sun/net/spi/DefaultProxySelector$NonProxyInfo
transform class:sun/net/spi/DefaultProxySelector$3
transform class:java/net/Proxy
transform class:java/net/Proxy$Type
transform class:java/util/ArrayList$Itr
transform class:sun/net/NetHooks
transform class:sun/net/sdp/SdpProvider
transform class:sun/net/NetHooks$Provider
transform class:com/fanzhu/adapter/impl/WildCock
transform class:com/fanzhu/adapter/CockAdapter
transform class:java/net/NetworkInterface
main start
transform class:java/net/NetworkInterface$1
transform class:java/net/InterfaceAddress
transform class:java/lang/Shutdown
transform class:java/lang/Shutdown$Lock
transform class:java/net/DefaultInterface
Process finished with exit code 0
从上面的输出结果我们能够发现:
执行 main 方法之前会加载系统类和自定义类;
在 ClassFileTransformer 中 会去拦截系统类和自己实现的类对象;
如果你想对某些类对象做增强,那么在拦截的时候抓住该类使用字节码编译工具即可实现。
todo
有些 Java 中 rt.jar 中的类如 Thread,ArrayList 等,这种方式行不通,因为 ClassFileTransformer 好像拦截不到这些类,因为下面的这个方法没有这些类的输出,但是对于其他的如 ThreadPoolExecutor 等又能拦截到。
版权声明: 本文为 InfoQ 作者【淡泊明志、宁静致远】的原创文章。
原文链接:【http://xie.infoq.cn/article/60e28146090d1a7b3a5ce4c2b】。未经作者许可,禁止转载。
淡泊明志、宁静致远
还未添加个人签名 2021.01.27 加入
还未添加个人简介
评论