写点什么

javaagent

  • 2022 年 1 月 20 日
  • 本文字数:7960 字

    阅读完需:约 26 分钟

java 的世界里,有时候你会听到这样一些名词 - 探针字节码插桩,这背后用到的技术其实就叫做javaagent,今天我就来揭秘一下 javaagent 背后的原理。

概念

javaagent 是什么?

javaagent 是 java 命令的一个参数,参数 javaagent可以指定一个 jar 包,并且对该 jar 包有两个要求:

  1. 这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项;

  2. 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 需要几个步骤:

  1. 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入 Can-Redefine-Classes 和 Can-Retransform-Classes 选项;

  2. 创建一个 Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己实现;

  3. 将上述文件打成 jar 包;

  4. 使用 -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.AdapterTesttransform class:sun/nio/cs/ThreadLocalCoderstransform class:sun/nio/cs/ThreadLocalCoders$1transform class:sun/nio/cs/ThreadLocalCoders$Cachetransform class:sun/nio/cs/ThreadLocalCoders$2transform class:com/intellij/rt/execution/application/AppMainV2$Agenttransform class:com/intellij/rt/execution/application/AppMainV2transform class:java/lang/NoSuchMethodExceptiontransform class:java/lang/reflect/InvocationTargetExceptiontransform class:com/intellij/rt/execution/application/AppMainV2$1transform class:java/net/Sockettransform class:java/lang/invoke/MethodHandleImpltransform class:java/net/InetSocketAddresstransform class:java/net/SocketAddresstransform class:java/net/InetAddresstransform class:java/lang/invoke/MethodHandleImpl$1transform class:java/net/InetSocketAddress$InetSocketAddressHoldertransform class:java/lang/invoke/MethodHandleImpl$2transform class:java/util/function/Functiontransform class:java/lang/invoke/MethodHandleImpl$3transform class:java/lang/invoke/MethodHandleImpl$4transform class:java/lang/ClassValuetransform class:java/lang/ClassValue$Entrytransform class:sun/security/action/GetBooleanActiontransform class:java/net/InetAddress$1transform class:java/lang/ClassValue$Identitytransform class:java/lang/ClassValue$Versiontransform class:java/lang/invoke/MemberName$Factorytransform class:java/lang/invoke/MethodHandleStaticstransform class:java/lang/invoke/MethodHandleStatics$1transform class:sun/misc/PostVMInitHooktransform class:sun/misc/PostVMInitHook$1transform class:java/net/InetAddress$InetAddressHoldertransform class:java/net/InetAddress$Cachetransform class:java/net/InetAddress$Cache$Typetransform class:java/net/InetAddressImplFactorytransform class:sun/usagetracker/UsageTrackerClienttransform class:java/util/concurrent/atomic/AtomicBooleantransform class:sun/usagetracker/UsageTrackerClient$1transform class:sun/usagetracker/UsageTrackerClient$4transform class:java/net/Inet6AddressImpltransform class:java/net/InetAddressImpltransform class:sun/usagetracker/UsageTrackerClient$3transform class:java/net/InetAddress$2transform class:sun/net/spi/nameservice/NameServicetransform class:java/net/Inet4Addresstransform class:java/io/FileOutputStream$1transform class:java/net/SocksSocketImpltransform class:java/net/SocksConststransform class:java/net/PlainSocketImpltransform class:java/net/AbstractPlainSocketImpltransform class:java/net/SocketImpltransform class:java/net/SocketOptionstransform class:java/net/AbstractPlainSocketImpl$1transform class:java/net/Inet6Addresstransform class:sun/launcher/LauncherHelpertransform class:java/net/Inet6Address$Inet6AddressHoldertransform class:sun/misc/URLClassPath$FileLoader$1transform class:java/net/SocketExceptiontransform class:java/io/IOExceptiontransform class:java/net/SocksSocketImpl$3transform class:java/net/ProxySelectortransform class:sun/net/spi/DefaultProxySelectortransform class:sun/net/spi/DefaultProxySelector$1transform class:sun/net/NetPropertiestransform class:sun/net/NetProperties$1transform class:java/util/Properties$LineReadertransform class:com/fanzhu/adapter/AdapterTesttransform class:sun/launcher/LauncherHelper$FXHelpertransform class:java/lang/Class$MethodArraytransform class:java/net/URItransform class:com/fanzhu/adapter/Cocktransform class:java/net/URI$Parsertransform class:com/fanzhu/adapter/Ducktransform class:java/lang/Voidtransform class:sun/net/spi/DefaultProxySelector$NonProxyInfotransform class:sun/net/spi/DefaultProxySelector$3transform class:java/net/Proxytransform class:java/net/Proxy$Typetransform class:java/util/ArrayList$Itrtransform class:sun/net/NetHookstransform class:sun/net/sdp/SdpProvidertransform class:sun/net/NetHooks$Providertransform class:com/fanzhu/adapter/impl/WildCocktransform class:com/fanzhu/adapter/CockAdaptertransform class:java/net/NetworkInterfacemain starttransform class:java/net/NetworkInterface$1transform class:java/net/InterfaceAddresstransform class:java/lang/Shutdowntransform class:java/lang/Shutdown$Locktransform class:java/net/DefaultInterface
Process finished with exit code 0
复制代码

从上面的输出结果我们能够发现:

  • 执行 main 方法之前会加载系统类和自定义类;

  • 在 ClassFileTransformer 中 会去拦截系统类和自己实现的类对象;

  • 如果你想对某些类对象做增强,那么在拦截的时候抓住该类使用字节码编译工具即可实现。

todo

  1. 有些 Java 中 rt.jar 中的类如 Thread,ArrayList 等,这种方式行不通,因为 ClassFileTransformer 好像拦截不到这些类,因为下面的这个方法没有这些类的输出,但是对于其他的如 ThreadPoolExecutor 等又能拦截到。


发布于: 刚刚阅读数: 2
用户头像

还未添加个人签名 2021.01.27 加入

还未添加个人简介

评论

发布
暂无评论
javaagent