使用ASM和agent监控属性变化

文章目录

ASM

  1. ASM 不是运行时 AOP
  2. ASM 发生在类加载期
  3. 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

只有类第一次初始化的时候才会被调用

作用:

  1. 类是否被加载
  2. 哪个 ClassLoader 加载
  3. 加载时机(初始化前)
  4. 是否要"改这个类的字节码"
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

  1. 谁调用它?
    👉 ASM:ClassReader.accept(classVisitor, ...)
  2. 什么时候会调用 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() + ")"
            );
        }

    }
}
相关推荐
黎雁·泠崖9 小时前
【魔法森林冒险】5/14 Allen类(三):任务进度与状态管理
java·开发语言
qq_124987075310 小时前
基于SSM的动物保护系统的设计与实现(源码+论文+部署+安装)
java·数据库·spring boot·毕业设计·ssm·计算机毕业设计
Coder_Boy_10 小时前
基于SpringAI的在线考试系统-考试系统开发流程案例
java·数据库·人工智能·spring boot·后端
Mr_sun.10 小时前
Day06——权限认证-项目集成
java
瑶山10 小时前
Spring Cloud微服务搭建四、集成RocketMQ消息队列
java·spring cloud·微服务·rocketmq·dashboard
abluckyboy10 小时前
Java 实现求 n 的 n^n 次方的最后一位数字
java·python·算法
2301_8187320610 小时前
前端调用控制层接口,进不去,报错415,类型不匹配
java·spring boot·spring·tomcat·intellij-idea
2501_9419820510 小时前
深度对比:Java、Go、Python 实现企微外部群推送,哪个效率更高?
java·golang·企业微信
马猴烧酒.11 小时前
【面试八股|JAVA多线程】JAVA多线程常考面试题详解
java·服务器·数据库