Java Agent字节码插桩术

关于JVMTI(jvm tool interface)

 JVMTI是⽤来开发和监控JVM所使⽤的程序接⼝,可以探查JVM内部状态,并控制JVM应⽤程序的执⾏,JVMTI的客户端,或称为代理(agent),提供了很多函数,可以监听感兴趣的事件,以便来查询或控制应⽤程序,进而实现控制JVM应用程序目标。但是需要注意的是,并⾮所有的JVM实现都⽀持JVMTI。

Idea的Debug功能就是通过JDWP协议定制化JVMTI的功能实现与JVM的双向通信连接,在获取断点位置的代码信息同时向IDEA客户端进行推送信息,实现监听功能!

如果要自己实现定制化的JVMTI功能的话,需要编写dll文件 更多资料参考 

javaagent是java1.5之后引⼊的特性,基于JVMTI实现,⽀持JVMTI部分功能。主要应⽤场景是对类加载进⾏拦截修改和对已加载的类进⾏重定义。此外还⽀持获取已加载的类,以及实例内存占⽤计算等。

javaagent的启动方式

javaagent的表现形式是监听一个JAR包,用于监听目标应用,它有两种启动方式:

  1. 加载时启动:agent的功能随着⽬标应⽤⼀起启动,通过设置⽬标应⽤的jvm参数:"-javaagent:",即可加载javaagent监听目标jar包。
    • 这种方式是在类加载之前进行拦截的,在符合JVM的规范下可以任意类的结构。比如修改类的名称,加入方法等等
  2. 运行时附着:借助jvmtools⼯具包将javaagent包注⼊到⽬标应⽤中,实现运行过程的监听。
    • 类加载之后进行拦截,可以对方法内部增加逻辑,不能增加方法修改类的结构。

代码演示

加载时启动(premain)

与运行时JAR包的启动区别

  运⾏时JAR包 javaagentjar包
启动类 Main-class Premain-class
启动⽅法 main premain
启动⽅式 java-jarxxx.jar jvm参数设置:-javaagent:xxx.jar

 

编写启动类(Premain)

复制代码
public class MyAgent {
    /**
     *
     * @param arg 启动参数
     * @param instrumentation 类的加载和定义都是通过它来实现的
     */
    // 加载时启动
    public static void premain(String arg, Instrumentation instrumentation) {
        System.out.println("hello premain");
    }

}
复制代码

maven插件设置然后进行编译打包

复制代码
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Project-name>${project.name}</Project-name>
                            <Project-version>${project.version}</Project-version>
                            <!--设置启动类的位置-->
                            <Premain-Class>javaagent.MyAgent</Premain-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                    <skip>true</skip>
                </configuration>
            </plugin>
复制代码

目标类

public class MyApp {
    public static void main(String[] args) throws IOException {
        System.out.println("访问主程序");
    }
}

将打包好的javaagent的jar包位置作为参数设置到JVM参数中

可以看到在类加载之前就已经被拦截了

 

附着启动(agentmain)

如果想要在应⽤运⾏之后去监听它,⽽⼜不去重启它,就可以采⽤另⼀种⽅式附着启动。其相关属性通过以表来⽐对:

  运⾏时JAR包 javaagentjar包
启动类 Main-class Agent-class
启动⽅法 main agentmain
启动⽅式 java-jarxxx.jar tools⼯具附着

编写启动类

public class MyAgent {
/**
*
* @param arg 启动参数
* @param instrumentation 类的加载和定义都是通过它来实现的
*/
// 运行时启动
public static void agentmain(String arg, Instrumentation instrumentation) {
System.out.println("hello agentmain");
}
}

maven插件设置然后进行编译打包

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.2</version>
<configuration>
<archive>
<manifestEntries>
<Project-name>${project.name}</Project-name>
<Project-version>${project.version}</Project-version>
<!--设置启动类的位置-->
<Agent-Class>javaagent.MyAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
<skip>true</skip>
</configuration>
</plugin>

目标类

public class MyApp {
public static void main(String[] args) throws IOException {
System.out.println("访问主程序");
// 设置运行时
System.in.read();
}
}

前置设置和加载时启动没有什么区别,运行时附着的启动方式是必须通过jvm/lib/tools.jar中的API注⼊⾄⽬标应⽤:

导入依赖

<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>${JAVA_HOME}/lib/tools.jar</systemPath>
</dependency>
public class AttachStart {
public static void main(String[] args) throws Exception {
// 获取jvm进程列表 借用tool工具实现进程交互
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (int i = 0; i < list.size(); i++) {
System.out.println(String.format("[%s] %s", i, list.get(i).displayName()));
}
System.out.println("输入数字指定要attach的进程");
// 选择jvm进程
BufferedReader read = new BufferedReader(new InputStreamReader(System.in));
String line = read.readLine();
int i = Integer.parseInt(line);
// 附着agent
VirtualMachine virtualMachine = VirtualMachine.attach(list.get(i));
// 将对应的agent功能附着到指定进程进去实现运行时启动
virtualMachine.loadAgent("E:/javaCode/javaagent/target/javaagent-1.0-SNAPSHOT.jar", "111");
virtualMachine.detach();
System.out.println("加载成功");
}
}

选择要附着的jvm进程,需要注意的是一旦附着成功,agent的功能实现是在对方的进程上实现的。

结果很明显,agent的功能是在目标类之后才显现出来

 

核心功能

addTransformer(变压器): 添加类加载拦截器,可重定义加载的类,类如果在这之前加载会失效。

retransformClasses(类重新变更):将类进行二次加载,重新被变压器所拦截,注意加载的类是有限制的,仅可对运⾏指令码进⾏修改:不可修改类结构如继承、接⼝、类符、变更属性、变更⽅法等。可以新增privatestatic/final的方法;必须maven插件添加Can-Retransform-Classes=true该⽅法执⾏才有效,且addTransformer⽅法的canRetransform参数也为true。

redefineClasses(重新定义类):在经过变压器之前进行重定义,逻辑上跟类重新变更没太大的区别

getAllLoadedClasses(获取所有类)

getInitiatedClasses(获取已实例化的类)

getObjectSize(计算对象大小)

示例

public class MyApp {
public static void main(String[] args) throws IOException {
System.out.println("访问主程序");
new HelloWorld().hello();
}
}
public class HelloWorld {
public void hello() {
System.out.println("hello!!");
}
}
// 加载时启动
public static void premain(String arg, Instrumentation instrumentation) throws UnmodifiableClassException, ClassNotFoundException {
System.out.println("hello premain");
// 如果类在变压器之前加载那么就不能修改类(运行时加载不能用)
HelloWorld helloWorld = new HelloWorld();
instrumentation.addTransformer(new ClassFileTransformer() {
/**
* @param loader
* @param className 动态加载的类名
* @param classBeingRedefined 这个类重新加载之前的类
* @param protectionDomain 类的基本信息
* @param classfileBuffer 这个类的字节码,如果返回null就按照原有的进行加载覆盖
* @return 返回指令码
* @throws IllegalClassFormatException
*/
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
if (!"javaagent/HelloWorld".equals(className)) {
return null;
}
// 配合javassist修改指令码
ClassPool pool = new ClassPool();
pool.appendSystemPath();
try {
CtClass ctClass = pool.get("javaagent.HelloWorld");
CtMethod method = ctClass.getDeclaredMethod("hello");
method.insertBefore("System.out.println(\"插入前置逻辑\");");
return ctClass.toBytecode();
} catch (NotFoundException | CannotCompileException | IOException e) {
e.printStackTrace();
}
return null;
}
},true);
// 重新触发指定类的加载但是对字节码的修改有一定的限制,只是重走变压器的逻辑
instrumentation.retransformClasses(HelloWorld.class);
// 这里的访问的方法内部逻辑已经被修改了
// 原理有点像是spring热部署:(单例)类只加载一次它所指向的方法地址不变,但是方法自身指令码发生了改变(钥匙还是原来的钥匙但是房间里的东西可能就不一样了!)!
helloWorld.hello();
}

结果

 

posted @   猫长寿  阅读(730)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
点击右上角即可分享
微信分享提示