文章目录
ASM
- ASM 不是运行时 AOP
- ASM 发生在类加载期
- ASM 改的是 "将要被 JVM 执行的代码本身"
ClassFileTransformer 决定"要不要改"
ClassVisitor 决定"改哪一部分结构"
MethodVisitor 决定"在指令级别怎么改"
依赖
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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
<relativePath/>
</parent>
<groupId>com.security</groupId>
<artifactId>security-agent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>security-agent</name>
<description>Security Context Authentication Monitor Agent</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<java.version>17</java.version>
<byte-buddy.version>1.14.11</byte-buddy.version>
<slf4j.version>2.0.12</slf4j.version>
<logback.version>1.5.6</logback.version>
</properties>
<dependencies>
<!-- ByteBuddy 核心库 -->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>${byte-buddy.version}</version>
</dependency>
<!-- ByteBuddy Agent -->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>${byte-buddy.version}</version>
</dependency>
<!-- Spring Security (仅编译时,不打包) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>6.2.1</version>
<scope>provided</scope>
</dependency>
<!-- Lombok (仅编译时) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- SLF4J API -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- Logback 实现 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<!-- Maven Assembly Plugin - 打包所有依赖 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<!-- <Premain-Class>com.example.agent.SecurityContextAgent</Premain-Class>-->
<!-- <Agent-Class>com.example.agent.SecurityContextAgent</Agent-Class>-->
<!-- <Premain-Class>com.example.agent.monitorField.FieldWriteAgent</Premain-Class>-->
<!-- <Agent-Class>com.example.agent.monitorField.FieldWriteAgent</Agent-Class>-->
<Premain-Class>com.example.agent.monitorField.FieldWriteAgent</Premain-Class>
<Agent-Class>com.example.agent.monitorField.FieldWriteAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Boot-Class-Path-Allowed>true</Boot-Class-Path-Allowed>
</manifestEntries>
</archive>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
agent
java
package com.example.agent.monitorField;
import lombok.extern.slf4j.Slf4j;
import java.lang.instrument.Instrumentation;
@Slf4j
public class FieldWriteAgent {
public static void premain(String args, Instrumentation inst) {
log.info("start FieldWriteAgent");
inst.addTransformer(new FieldWriteTransformer(), true);
}
}
ClassFileTransformer
只有类第一次初始化的时候才会被调用
作用:
- 类是否被加载
- 哪个 ClassLoader 加载
- 加载时机(初始化前)
- 是否要"改这个类的字节码"
java
package com.example.agent.monitorField;
import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.jar.asm.ClassReader;
import net.bytebuddy.jar.asm.ClassVisitor;
import net.bytebuddy.jar.asm.ClassWriter;
import net.bytebuddy.jar.asm.Opcodes;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
@Slf4j
public class FieldWriteTransformer implements ClassFileTransformer {
// 要修改字节码的类
private static final String TARGET_CLASS =
"com/web/controller/tool/TestUser";
@Override
public byte[] transform(
Module module,
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer
) {
if (!TARGET_CLASS.equals(className)) {
return null;
}
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
log.info("111111111111111");
ClassVisitor cv = new UserClassVisitor(Opcodes.ASM9, cw);
cr.accept(cv, ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
}
}
ClassVisitor
- 谁调用它?
👉 ASM:ClassReader.accept(classVisitor, ...) - 什么时候会调用 visitMethod()?
class 文件里每出现一个 method_info,就调用一次,包括:- 普通方法
- 构造方法
<init> - 静态初始化
<clinit> - private / static / final
- 编译器生成的方法(lambda、access$xxx)
什么时候会触发 visitMethod
触发链路是这样的 👇
java
类被 JVM 加载
↓
Instrumentation 调用 transformer.transform()
↓
new ClassReader(classBytes)
↓
cr.accept(classVisitor, ...)
↓
ASM 顺序遍历 class 文件结构
↓
遇到一个 method_info
↓
调用 ClassVisitor.visitMethod()
关键点:
- 这是 类加载期
- 这是 字节码解析期
- 与方法是否执行 完全无关
java
package com.example.agent.monitorField;
import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.jar.asm.ClassVisitor;
import net.bytebuddy.jar.asm.MethodVisitor;
@Slf4j
public class UserClassVisitor extends ClassVisitor {
public UserClassVisitor(int api, ClassVisitor cv) {
super(api, cv);
log.info("222222222222222");
}
@Override
public MethodVisitor visitMethod(
int access,
String name,
String descriptor,
String signature,
String[] exceptions
) {
MethodVisitor mv = super.visitMethod(
access, name, descriptor, signature, exceptions
);
log.info("33333333333333");
// 你返回一个 MethodVisitor,等于告诉 ASM:"这个方法的字节码,由我来接管",是否结果类是在FieldWriteTransformer 判断了
return new FieldWriteMethodVisitor(api, mv);
}
}
MethodVisitor
java
package com.example.agent.monitorField;
import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.jar.asm.MethodVisitor;
import net.bytebuddy.jar.asm.Opcodes;
@Slf4j
public class FieldWriteMethodVisitor extends MethodVisitor {
// 要修改字节码的类
private static final String TARGET_OWNER = "com/web/controller/tool/TestUser";
// 要在修改此属性时打印信息
private static final String TARGET_FIELD = "username";
public FieldWriteMethodVisitor(int api, MethodVisitor mv) {
super(api, mv);
}
@Override
public void visitFieldInsn(
int opcode,
String owner,
String name,
String descriptor
) {
log.info("opcode={} owner={}, name={}, descriptor={}",opcode, owner, name, descriptor);
// 只监听 User.name
if ((opcode == Opcodes.PUTFIELD || opcode == Opcodes.PUTSTATIC)
&& TARGET_OWNER.equals(owner)
&& TARGET_FIELD.equals(name)) {
// ====== 插入监控逻辑 ======
mv.visitInsn(Opcodes.DUP); // 复制 value(栈顶)
log.info("authentication is monitor");
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/example/agent/monitorField/FieldWatchHook",
"onFieldWrite",
"(Ljava/lang/Object;)V",
false
);
}
super.visitFieldInsn(opcode, owner, name, descriptor);
}
}
HOOK
何时被调用:属性被修改的时候,以上类都是在类加载的时候被调用,只有这个hook是在属性被修改的时候调用
java
package com.example.agent.monitorField;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class FieldWatchHook {
public static void onFieldWrite(Object value) {
log.info("onFieldWrite change to {}", value);
}
}
打印哪里导致的属性变化
jdk9
java
package com.example.agent.monitorField;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class FieldWatchHook {
private static final StackWalker WALKER =
StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
public static void onFieldWrite(Object value) {
System.out.println("[Agent] TestUser.username changed to: " + value);
WALKER.forEach(frame -> {
System.out.println(
" at " + frame.getClassName()
+ "." + frame.getMethodName()
+ "(" + frame.getFileName()
+ ":" + frame.getLineNumber() + ")"
);
});
}
}
jdk8
java
package com.example.agent.monitorField;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class FieldWatchHook {
public static void onFieldWrite(Object value) {
System.out.println("[Agent] TestUser.username changed: " + value);
StackTraceElement[] stack =
Thread.currentThread().getStackTrace();
for (StackTraceElement ste : stack) {
String cls = ste.getClassName();
// 过滤无意义栈
if (cls.startsWith("java.")
|| cls.startsWith("sun.")
|| cls.startsWith("com.agent")) {
continue;
}
System.out.println(
" at " + ste.getClassName()
+ "." + ste.getMethodName()
+ "(" + ste.getFileName()
+ ":" + ste.getLineNumber() + ")"
);
}
}
}