diff --git a/press-monster-test/demo/README.md b/press-monster-test/demo/README.md new file mode 100644 index 0000000..a9ca31c --- /dev/null +++ b/press-monster-test/demo/README.md @@ -0,0 +1,233 @@ +# ByteBuddy Agent 拦截演示项目 + +## 项目概述 + +这是一个完整的ByteBuddy Java Agent实现示例,展示了如何使用AgentBuilder进行字节码增强和方法拦截。项目演示了多种目标类匹配方式,包括类名匹配、注解匹配、包路径匹配等。 + +## 核心特性 + +✅ **使用AgentBuilder方式进行字节码增强** +✅ **使用委托方式对目标方法增强** +✅ **设定用来增强方法的拦截器** +✅ **在委托类中使用拦截器对目标方法前后增强** +✅ **完成能在方法调用前后打印入参和返回结果的插件** +✅ **展示多种目标类匹配方式(类名、注解、包路径等)** + +## 项目结构 + +``` +src/main/java/io/github/kazusa/pressmonster/demo/ +├── agent/ # Agent核心代码 +│ ├── SimpleAgent.java # Agent主类,展示多种匹配方式 +│ ├── MethodInterceptor.java # 拦截器接口 +│ ├── LoggingInterceptor.java # 具体的日志拦截器实现 +│ └── DelegateMethodHandler.java # 委托方法处理器 +├── annotation/ # 自定义注解 +│ └── Monitored.java # 用于注解匹配演示 +├── service/ # 示例服务类 +│ ├── UserService.java # 普通服务类,用于类名匹配 +│ └── OrderService.java # 带注解的服务类,用于注解匹配 +├── DemoApplication.java # 演示应用主类 +├── SampleController.java # Spring Controller示例 +└── run-demo.sh # 便捷运行脚本 +``` + +## 拦截匹配方式 + +### 1. 精确类名匹配 +```java +named("io.github.kazusa.pressmonster.demo.SampleController") +``` + +### 2. Spring注解匹配 +```java +isAnnotatedWith(RestController.class) +``` + +### 3. 自定义注解匹配 +```java +isAnnotatedWith(named("io.github.kazusa.pressmonster.demo.annotation.Monitored")) +``` + +### 4. 包路径前缀匹配 +```java +nameStartsWith("io.github.kazusa.pressmonster.demo.service") +``` + +### 5. 类名后缀匹配 +```java +nameEndsWith("Service") +``` + +### 6. 类名包含匹配 +```java +nameContains("Controller") +``` + +### 7. 复合条件匹配 +```java +// 使用OR条件组合多种匹配方式 +named("className") +.or(isAnnotatedWith(SomeAnnotation.class)) +.or(nameStartsWith("packagePrefix")) +.and(not(nameContains("Test"))) // 排除测试类 +``` + +## 快速开始 + +### 方式1: 使用运行脚本(推荐) + +```bash +# 直接运行脚本 +./run-demo.sh + +# 或者 +bash run-demo.sh +``` + +脚本会自动编译、打包,并提供3种运行选择: +1. 不使用Agent运行(对比效果) +2. 使用Agent运行(展示拦截效果) +3. 同时运行两种方式进行对比 + +### 方式2: 手动编译运行 + +```bash +# 1. 编译和打包 +mvn clean package -DskipTests + +# 2. 不使用Agent运行(看不到拦截日志) +java -jar target/demo-1.0.0-SNAPSHOT.jar + +# 3. 使用Agent运行(可以看到拦截日志) +java -javaagent:target/demo-1.0.0-SNAPSHOT.jar -jar target/demo-1.0.0-SNAPSHOT.jar +``` + +## 预期输出示例 + +### 使用Agent运行时的输出 + +``` +======================================== +Simple ByteBuddy Agent 启动中... +Agent参数: 无 +======================================== +[AGENT] 成功增强类: io.github.kazusa.pressmonster.demo.SampleController (匹配方式: SPRING_CONTROLLER) +[AGENT] 成功增强类: io.github.kazusa.pressmonster.demo.service.UserService (匹配方式: SERVICE_PACKAGE) +[AGENT] 成功增强类: io.github.kazusa.pressmonster.demo.service.OrderService (匹配方式: SERVICE_PACKAGE) +[AGENT] ByteBuddy Agent 安装完成! + +======================================== +=== ByteBuddy Agent 拦截演示应用 === +======================================== + +🔍 测试1: @RestController注解匹配 +目标类: SampleController +匹配规则: isAnnotatedWith(RestController.class) +---------------------------------------- +[INTERCEPT] [SPRING_CONTROLLER] Before method: SampleController.getUsers() +[INTERCEPT] [SPRING_CONTROLLER] Arguments: [] +SampleController.getUsers() 执行中... +[INTERCEPT] [SPRING_CONTROLLER] After method: SampleController.getUsers() +[INTERCEPT] [SPRING_CONTROLLER] Return: {users=[user1, user2, user3], total=3} +[INTERCEPT] [SPRING_CONTROLLER] Duration: 15.23ms +[INTERCEPT] [SPRING_CONTROLLER] ========================================== +``` + +## 核心实现原理 + +### 1. Agent入口点 (SimpleAgent.java) +- 实现`premain`方法作为Agent入口 +- 配置AgentBuilder的匹配规则和转换逻辑 +- 使用`MethodDelegation.to()`进行方法委托 + +### 2. 委托处理器 (DelegateMethodHandler.java) +- 使用`@RuntimeType`注解支持动态类型匹配 +- 集成拦截器调用逻辑 +- 处理原方法执行和异常捕获 + +### 3. 拦截器 (LoggingInterceptor.java) +- 实现`beforeMethod`、`afterMethod`、`handleException`方法 +- 提供参数和返回值的格式化输出 +- 记录方法执行时间和性能统计 + +### 4. Maven配置 +- 配置`Premain-Class`和相关Agent属性 +- 使用Shade插件打包依赖到Fat JAR +- 支持同时作为普通应用和Agent运行 + +## 扩展功能 + +### 添加新的匹配规则 +在`SimpleAgent.buildTypeMatchers()`方法中添加新的匹配条件: + +```java +.or(nameMatches(".*Controller.*")) // 正则表达式匹配 +.or(isInterface()) // 匹配接口 +.or(hasSuperType(named("某个父类"))) // 匹配特定父类的子类 +``` + +### 自定义拦截器 +实现`MethodInterceptor`接口创建自定义拦截器: + +```java +public class CustomInterceptor implements MethodInterceptor { + @Override + public void beforeMethod(Object target, Method method, Object[] args, String matchType) { + // 自定义前置逻辑 + } + + @Override + public Object afterMethod(Object target, Method method, Object[] args, Object result, + String matchType, long startTime) { + // 自定义后置逻辑 + return result; + } + + @Override + public void handleException(Object target, Method method, Object[] args, + Throwable throwable, String matchType) { + // 自定义异常处理逻辑 + } +} +``` + +## 依赖说明 + +- **ByteBuddy**: 1.14.9 - 字节码操作核心库 +- **Spring Web**: 用于@RestController注解支持 +- **Jackson**: JSON序列化(可选) +- **SLF4J + Logback**: 日志框架 + +## 注意事项 + +1. **Java版本**: 支持Java 8+ +2. **ClassLoader隔离**: Agent代码与应用代码使用不同的ClassLoader +3. **性能影响**: 拦截会带来额外的性能开销,生产环境需要谨慎使用 +4. **异常处理**: 拦截器异常不应影响原始方法的执行逻辑 + +## 故障排除 + +### 常见问题 + +**Q: Agent没有生效,看不到拦截日志** +A: 检查是否使用了`-javaagent`参数,确保JAR文件路径正确 + +**Q: 编译失败** +A: 检查Java版本和Maven配置,确保所有依赖都能正确下载 + +**Q: 某些类没有被拦截** +A: 检查匹配规则,确认目标类符合预期的匹配条件 + +**Q: 运行时出现ClassNotFoundException** +A: 使用Maven Shade插件打包所有依赖到Fat JAR + +## 进一步学习 + +- [ByteBuddy官方文档](https://bytebuddy.net/) +- [Java Instrumentation API](https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html) +- [Java Agent开发指南](https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html) + +--- + +*这个项目完整展示了ByteBuddy Agent的核心功能,可以作为学习字节码增强和AOP编程的参考示例。* \ No newline at end of file diff --git a/press-monster-test/demo/pom.xml b/press-monster-test/demo/pom.xml index 3fdef6e..f09d622 100644 --- a/press-monster-test/demo/pom.xml +++ b/press-monster-test/demo/pom.xml @@ -32,6 +32,18 @@ press-monster-common + + + net.bytebuddy + byte-buddy + 1.14.9 + + + net.bytebuddy + byte-buddy-agent + 1.14.9 + + org.springframework @@ -59,4 +71,71 @@ + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 8 + 8 + UTF-8 + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + true + io.github.kazusa.pressmonster.demo.DemoApplication + + + + io.github.kazusa.pressmonster.demo.agent.SimpleAgent + io.github.kazusa.pressmonster.demo.agent.SimpleAgent + true + true + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.4.1 + + + package + + shade + + + false + + + io.github.kazusa.pressmonster.demo.DemoApplication + + io.github.kazusa.pressmonster.demo.agent.SimpleAgent + io.github.kazusa.pressmonster.demo.agent.SimpleAgent + true + true + + + + + + + + + + \ No newline at end of file diff --git a/press-monster-test/demo/run-demo.sh b/press-monster-test/demo/run-demo.sh new file mode 100755 index 0000000..249d427 --- /dev/null +++ b/press-monster-test/demo/run-demo.sh @@ -0,0 +1,106 @@ +#!/bin/bash + +# ByteBuddy Agent Demo 运行脚本 + +echo "=========================================" +echo "ByteBuddy Agent 拦截演示脚本" +echo "=========================================" + +# 设置项目目录 +PROJECT_DIR="/Users/kazusa/project/java/press-monster-java/press-monster-test/demo" +cd "$PROJECT_DIR" + +echo "当前目录: $(pwd)" + +# 清理并编译项目 +echo -e "\n🔨 编译项目..." +mvn clean compile > /dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "❌ 编译失败,请检查代码" + mvn clean compile + exit 1 +fi + +# 打包项目 +echo "📦 打包项目..." +mvn package -DskipTests > /dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "❌ 打包失败,请检查配置" + mvn package -DskipTests + exit 1 +fi + +# 检查jar文件是否存在 +JAR_FILE="$PROJECT_DIR/target/demo-1.0.0-SNAPSHOT.jar" +if [ ! -f "$JAR_FILE" ]; then + echo "❌ JAR文件不存在: $JAR_FILE" + exit 1 +fi + +echo "✅ 编译和打包完成" + +echo -e "\n=========================================" +echo "运行选项:" +echo "1. 不使用Agent运行(对比效果)" +echo "2. 使用Agent运行(展示拦截效果)" +echo "=========================================" + +# 提供两种运行方式供用户选择 +echo -e "\n选择运行方式:" +echo "输入 1 - 不使用Agent运行" +echo "输入 2 - 使用Agent运行" +echo "输入 3 - 同时运行两种方式进行对比" +read -p "请选择 (1/2/3): " choice + +case $choice in + 1) + echo -e "\n🚀 运行方式1: 不使用Agent" + echo "命令: java -jar $JAR_FILE" + echo "----------------------------------------" + java -jar "$JAR_FILE" + ;; + 2) + echo -e "\n🚀 运行方式2: 使用Agent" + echo "命令: java -javaagent:$JAR_FILE -jar $JAR_FILE" + echo "----------------------------------------" + java -javaagent:"$JAR_FILE" -jar "$JAR_FILE" + ;; + 3) + echo -e "\n🚀 对比运行:" + echo -e "\n【第一次运行 - 不使用Agent】" + echo "命令: java -jar $JAR_FILE" + echo "----------------------------------------" + java -jar "$JAR_FILE" + + echo -e "\n\n【第二次运行 - 使用Agent】" + echo "命令: java -javaagent:$JAR_FILE -jar $JAR_FILE" + echo "----------------------------------------" + java -javaagent:"$JAR_FILE" -jar "$JAR_FILE" + ;; + *) + echo "❌ 无效选择,默认使用Agent运行" + echo -e "\n🚀 使用Agent运行:" + echo "命令: java -javaagent:$JAR_FILE -jar $JAR_FILE" + echo "----------------------------------------" + java -javaagent:"$JAR_FILE" -jar "$JAR_FILE" + ;; +esac + +echo -e "\n=========================================" +echo "演示完成!" +echo "=========================================" + +# 显示帮助信息 +echo -e "\n💡 说明:" +echo "- 第一种方式:普通运行,不会看到拦截日志" +echo "- 第二种方式:Agent运行,会看到详细的拦截日志" +echo "- Agent会拦截所有匹配的类和方法,打印参数和返回值" +echo "" +echo "📝 匹配规则:" +echo "1. @RestController注解的类 (SampleController)" +echo "2. service包下的类 (UserService, OrderService)" +echo "3. 以Service结尾的类名" +echo "4. @Monitored注解的类" +echo "" +echo "🔧 如需修改拦截规则,请编辑:" +echo " src/main/java/io/github/kazusa/pressmonster/demo/agent/SimpleAgent.java" \ No newline at end of file diff --git a/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/DemoApplication.java b/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/DemoApplication.java index 625872e..d3b266b 100644 --- a/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/DemoApplication.java +++ b/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/DemoApplication.java @@ -1,27 +1,164 @@ package io.github.kazusa.pressmonster.demo; +import io.github.kazusa.pressmonster.demo.service.OrderService; +import io.github.kazusa.pressmonster.demo.service.UserService; + +import java.util.HashMap; +import java.util.Map; + /** * 演示应用 - * 展示Press Monster Agent的功能 + * 展示ByteBuddy Agent的多种拦截匹配方式 + * + * 运行方式: + * 1. 作为普通应用运行:java -jar demo.jar + * 2. 作为Agent运行:java -javaagent:demo.jar -jar demo.jar */ public class DemoApplication { public static void main(String[] args) { - System.out.println("=== Press Monster Agent 演示 ==="); + System.out.println("========================================"); + System.out.println("=== ByteBuddy Agent 拦截演示应用 ==="); + System.out.println("========================================"); - // 模拟应用运行 try { - // 等待一段时间让Agent收集信息 - Thread.sleep(2000); - SampleController controller = new SampleController(); - controller.getUsers(); - // 输出收集到的API信息 - printApiEndpoints(); + // 等待Agent完全加载 + Thread.sleep(1000); + + System.out.println("\n开始测试不同类型的拦截匹配...\n"); + + // 1. 测试@RestController注解匹配 - SampleController + testControllerMethods(); + + // 2. 测试包路径和类名后缀匹配 - UserService + testUserServiceMethods(); + + // 3. 测试自定义@Monitored注解匹配 - OrderService + testOrderServiceMethods(); + + // 4. 测试异常处理 + testExceptionHandling(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } + + System.out.println("\n========================================"); + System.out.println("=== 所有测试完成 ==="); + System.out.println("========================================"); } - private static void printApiEndpoints() {} + /** + * 测试Controller方法(@RestController注解匹配) + */ + private static void testControllerMethods() { + System.out.println("🔍 测试1: @RestController注解匹配"); + System.out.println("目标类: SampleController"); + System.out.println("匹配规则: isAnnotatedWith(RestController.class)"); + System.out.println("----------------------------------------"); + + SampleController controller = new SampleController(); + + // 测试无参数方法 + controller.getUsers(); + + // 测试带参数方法 + controller.getUserById("user123"); + + // 测试复杂参数方法 + Map userData = new HashMap<>(); + userData.put("name", "张三"); + userData.put("email", "zhangsan@example.com"); + controller.createUser(userData); + + System.out.println(); + } + + /** + * 测试UserService方法(包路径前缀 + 类名后缀匹配) + */ + private static void testUserServiceMethods() { + System.out.println("🔍 测试2: 包路径前缀 + 类名后缀匹配"); + System.out.println("目标类: UserService"); + System.out.println("匹配规则: nameStartsWith(\"io.github.kazusa.pressmonster.demo.service\") OR nameEndsWith(\"Service\")"); + System.out.println("----------------------------------------"); + + UserService userService = new UserService(); + + // 测试查询方法 + userService.findUserById("user456"); + + // 测试列表方法 + userService.getAllUsers(); + + // 测试创建方法 + userService.createUser("李四", "lisi@example.com"); + + // 测试更新方法 + Map updateData = new HashMap<>(); + updateData.put("name", "李四(已更新)"); + userService.updateUser("user456", updateData); + + System.out.println(); + } + + /** + * 测试OrderService方法(自定义@Monitored注解匹配) + */ + private static void testOrderServiceMethods() { + System.out.println("🔍 测试3: 自定义@Monitored注解匹配"); + System.out.println("目标类: OrderService"); + System.out.println("匹配规则: isAnnotatedWith(named(\"io.github.kazusa.pressmonster.demo.annotation.Monitored\"))"); + System.out.println("----------------------------------------"); + + OrderService orderService = new OrderService(); + + // 测试查询订单 + orderService.findOrderById("order789"); + + // 测试用户订单列表 + orderService.getOrdersByUserId("user123"); + + // 测试创建订单 + orderService.createOrder("user456", 299.99, "商品A x2"); + + // 测试更新订单状态 + orderService.updateOrderStatus("order789", "shipped"); + + // 测试取消订单 + orderService.cancelOrder("order789", "用户要求取消"); + + System.out.println(); + } + + /** + * 测试异常处理 + */ + private static void testExceptionHandling() { + System.out.println("🔍 测试4: 异常处理拦截"); + System.out.println("目标方法: OrderService.calculateTotalAmount()"); + System.out.println("----------------------------------------"); + + OrderService orderService = new OrderService(); + + try { + // 测试正常情况 + String[] validOrderIds = {"order1", "order2", "order3"}; + double total = orderService.calculateTotalAmount(validOrderIds); + System.out.println("计算结果: " + total); + + } catch (Exception e) { + System.out.println("捕获到异常: " + e.getMessage()); + } + + try { + // 测试异常情况 + orderService.calculateTotalAmount(null); + + } catch (Exception e) { + System.out.println("成功捕获预期异常: " + e.getClass().getSimpleName() + " - " + e.getMessage()); + } + + System.out.println(); + } } \ No newline at end of file diff --git a/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/agent/DelegateMethodHandler.java b/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/agent/DelegateMethodHandler.java new file mode 100644 index 0000000..b0cb141 --- /dev/null +++ b/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/agent/DelegateMethodHandler.java @@ -0,0 +1,87 @@ +package io.github.kazusa.pressmonster.demo.agent; + +import net.bytebuddy.implementation.bind.annotation.*; + +import java.lang.reflect.Method; +import java.util.concurrent.Callable; + +/** + * 委托方法处理器 + * 使用ByteBuddy的MethodDelegation机制,拦截目标方法并调用拦截器 + * 这是实现字节码增强的核心类 + */ +public class DelegateMethodHandler { + + // 拦截器实例,用于处理方法拦截逻辑 + private final MethodInterceptor interceptor; + + // 匹配类型,用于标识是通过什么方式匹配到的目标类 + private final String matchType; + + /** + * 构造函数 + * @param interceptor 拦截器实例 + * @param matchType 匹配类型标识 + */ + public DelegateMethodHandler(MethodInterceptor interceptor, String matchType) { + this.interceptor = interceptor; + this.matchType = matchType; + } + + /** + * 使用@RuntimeType注解的拦截方法 + * 这是ByteBuddy委托的核心方法,所有被拦截的方法调用都会委托到这里 + * + * @param target 目标对象实例(被拦截的对象) + * @param method 被拦截的方法 + * @param args 方法的所有参数 + * @param zuper 原方法的调用引用(用于调用原始方法) + * @return 方法的返回值 + * @throws Throwable 可能抛出的异常 + */ + @RuntimeType + public Object intercept(@This Object target, // 目标对象 + @Origin Method method, // 原始方法 + @AllArguments Object[] args, // 所有参数 + @SuperCall Callable zuper) // 原方法调用 + throws Throwable { + + long startTime = System.nanoTime(); + Object result = null; + + try { + // 1. 调用拦截器的beforeMethod方法 + interceptor.beforeMethod(target, method, args, matchType); + + // 2. 调用原始方法 + result = zuper.call(); + + // 3. 调用拦截器的afterMethod方法 + result = interceptor.afterMethod(target, method, args, result, matchType, startTime); + + return result; + + } catch (Throwable throwable) { + // 4. 如果出现异常,调用拦截器的handleException方法 + interceptor.handleException(target, method, args, throwable, matchType); + + // 重新抛出异常,保持原有的异常处理流程 + throw throwable; + } + } + + /** + * 创建委托方法处理器的工厂方法 + * 用于在Agent中创建不同匹配类型的处理器 + */ + public static DelegateMethodHandler create(String matchType) { + return new DelegateMethodHandler(new LoggingInterceptor(), matchType); + } + + /** + * 创建带自定义拦截器的委托方法处理器 + */ + public static DelegateMethodHandler create(MethodInterceptor interceptor, String matchType) { + return new DelegateMethodHandler(interceptor, matchType); + } +} \ No newline at end of file diff --git a/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/agent/LoggingInterceptor.java b/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/agent/LoggingInterceptor.java new file mode 100644 index 0000000..f69b489 --- /dev/null +++ b/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/agent/LoggingInterceptor.java @@ -0,0 +1,147 @@ +package io.github.kazusa.pressmonster.demo.agent; + +import java.lang.reflect.Method; +import java.util.Arrays; + +/** + * 日志拦截器实现 + * 实现方法调用前后的日志记录,包括参数、返回值和执行时间 + */ +public class LoggingInterceptor implements MethodInterceptor { + + private static final String LOG_PREFIX = "[INTERCEPT]"; + + @Override + public void beforeMethod(Object target, Method method, Object[] args, String matchType) throws Throwable { + String className = target.getClass().getSimpleName(); + String methodName = method.getName(); + + System.out.println(String.format("%s [%s] Before method: %s.%s()", + LOG_PREFIX, matchType, className, methodName)); + + if (args != null && args.length > 0) { + System.out.println(String.format("%s [%s] Arguments: %s", + LOG_PREFIX, matchType, formatArguments(args))); + } else { + System.out.println(String.format("%s [%s] Arguments: []", + LOG_PREFIX, matchType)); + } + } + + @Override + public Object afterMethod(Object target, Method method, Object[] args, Object result, + String matchType, long startTime) throws Throwable { + String className = target.getClass().getSimpleName(); + String methodName = method.getName(); + long duration = System.nanoTime() - startTime; + double durationMs = duration / 1_000_000.0; + + System.out.println(String.format("%s [%s] After method: %s.%s()", + LOG_PREFIX, matchType, className, methodName)); + + if (result != null) { + System.out.println(String.format("%s [%s] Return: %s", + LOG_PREFIX, matchType, formatReturnValue(result))); + } else { + System.out.println(String.format("%s [%s] Return: null", + LOG_PREFIX, matchType)); + } + + System.out.println(String.format("%s [%s] Duration: %.2fms", + LOG_PREFIX, matchType, durationMs)); + + System.out.println(String.format("%s [%s] ==========================================", + LOG_PREFIX, matchType)); + + return result; + } + + @Override + public void handleException(Object target, Method method, Object[] args, + Throwable throwable, String matchType) throws Throwable { + String className = target.getClass().getSimpleName(); + String methodName = method.getName(); + + System.err.println(String.format("%s [%s] Exception in method: %s.%s()", + LOG_PREFIX, matchType, className, methodName)); + System.err.println(String.format("%s [%s] Exception type: %s", + LOG_PREFIX, matchType, throwable.getClass().getSimpleName())); + System.err.println(String.format("%s [%s] Exception message: %s", + LOG_PREFIX, matchType, throwable.getMessage())); + + // 重新抛出异常,保持原有的异常处理逻辑 + throw throwable; + } + + /** + * 格式化方法参数 + */ + private String formatArguments(Object[] args) { + if (args == null || args.length == 0) { + return "[]"; + } + + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < args.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(formatSingleValue(args[i])); + } + sb.append("]"); + + return sb.toString(); + } + + /** + * 格式化返回值 + */ + private String formatReturnValue(Object value) { + return formatSingleValue(value); + } + + /** + * 格式化单个值 + */ + private String formatSingleValue(Object value) { + if (value == null) { + return "null"; + } + + // 如果是字符串,添加引号 + if (value instanceof String) { + return "\"" + value + "\""; + } + + // 如果是数组,使用Arrays.toString + if (value.getClass().isArray()) { + if (value instanceof Object[]) { + return Arrays.toString((Object[]) value); + } else if (value instanceof int[]) { + return Arrays.toString((int[]) value); + } else if (value instanceof long[]) { + return Arrays.toString((long[]) value); + } else if (value instanceof boolean[]) { + return Arrays.toString((boolean[]) value); + } else if (value instanceof double[]) { + return Arrays.toString((double[]) value); + } else if (value instanceof float[]) { + return Arrays.toString((float[]) value); + } else if (value instanceof byte[]) { + return Arrays.toString((byte[]) value); + } else if (value instanceof char[]) { + return Arrays.toString((char[]) value); + } else if (value instanceof short[]) { + return Arrays.toString((short[]) value); + } + } + + // 对于复杂对象,限制输出长度避免过长 + String str = value.toString(); + if (str.length() > 200) { + return str.substring(0, 200) + "... (truncated)"; + } + + return str; + } +} \ No newline at end of file diff --git a/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/agent/MethodInterceptor.java b/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/agent/MethodInterceptor.java new file mode 100644 index 0000000..b611815 --- /dev/null +++ b/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/agent/MethodInterceptor.java @@ -0,0 +1,49 @@ +package io.github.kazusa.pressmonster.demo.agent; + +import java.lang.reflect.Method; + +/** + * 方法拦截器接口 + * 定义方法拦截的标准接口,实现AOP切面功能 + */ +public interface MethodInterceptor { + + /** + * 方法执行前的拦截处理 + * + * @param target 目标对象实例 + * @param method 被拦截的方法 + * @param args 方法参数 + * @param matchType 匹配类型(用于标识是通过什么方式匹配到的) + * @throws Throwable 可能抛出的异常 + */ + void beforeMethod(Object target, Method method, Object[] args, String matchType) throws Throwable; + + /** + * 方法执行后的拦截处理 + * + * @param target 目标对象实例 + * @param method 被拦截的方法 + * @param args 方法参数 + * @param result 方法返回结果 + * @param matchType 匹配类型 + * @param startTime 方法开始执行时间(纳秒) + * @return 可能修改后的返回结果 + * @throws Throwable 可能抛出的异常 + */ + Object afterMethod(Object target, Method method, Object[] args, Object result, + String matchType, long startTime) throws Throwable; + + /** + * 方法执行异常时的拦截处理 + * + * @param target 目标对象实例 + * @param method 被拦截的方法 + * @param args 方法参数 + * @param throwable 抛出的异常 + * @param matchType 匹配类型 + * @throws Throwable 可能重新抛出的异常 + */ + void handleException(Object target, Method method, Object[] args, + Throwable throwable, String matchType) throws Throwable; +} \ No newline at end of file diff --git a/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/agent/SimpleAgent.java b/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/agent/SimpleAgent.java new file mode 100644 index 0000000..d02584d --- /dev/null +++ b/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/agent/SimpleAgent.java @@ -0,0 +1,147 @@ +package io.github.kazusa.pressmonster.demo.agent; + +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.matcher.ElementMatchers; +// import org.springframework.web.bind.annotation.RestController; // 注释掉,避免ClassNotFoundException + +import java.lang.instrument.Instrumentation; + +import static net.bytebuddy.matcher.ElementMatchers.*; + +/** + * 简单Agent示例 + * 展示使用ByteBuddy AgentBuilder的多种目标类匹配方式 + * 这是一个完整的Java Agent实现,演示了字节码增强的核心技术 + */ +public class SimpleAgent { + + /** + * Java Agent的入口方法 + * JVM在启动时会调用这个方法,通过-javaagent参数指定 + * + * @param agentArgs Agent参数 + * @param inst Instrumentation实例,用于字节码操作 + */ + public static void premain(String agentArgs, Instrumentation inst) { + System.out.println("========================================"); + System.out.println("Simple ByteBuddy Agent 启动中..."); + System.out.println("Agent参数: " + (agentArgs != null ? agentArgs : "无")); + System.out.println("========================================"); + + try { + // 创建AgentBuilder实例,配置多种匹配方式 + new AgentBuilder.Default() + // 配置匹配条件:展示多种目标类匹配方式 + .type(buildTypeMatchers()) + // 配置方法匹配:匹配所有public方法(排除Object的基础方法) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> + builder.method( + isPublic() + .and(not(isDeclaredBy(Object.class))) // 排除Object类的方法 + .and(not(isConstructor())) // 排除构造函数 + .and(not(isStatic())) // 排除静态方法(可选) + ) + // 使用MethodDelegation.to()进行方法委托 + .intercept(MethodDelegation.to(DelegateMethodHandler.create(determineMatchType(typeDescription.getName())))) + ) + // 配置监听器,用于调试和日志 + .with(new AgentBuilder.Listener() { + @Override + public void onDiscovery(String typeName, ClassLoader classLoader, + net.bytebuddy.utility.JavaModule module, boolean loaded) {} + + @Override + public void onTransformation(net.bytebuddy.description.type.TypeDescription typeDescription, + ClassLoader classLoader, net.bytebuddy.utility.JavaModule module, + boolean loaded, net.bytebuddy.dynamic.DynamicType dynamicType) { + System.out.println("[AGENT] 成功增强类: " + typeDescription.getName() + + " (匹配方式: " + determineMatchType(typeDescription.getName()) + ")"); + } + + @Override + public void onIgnored(net.bytebuddy.description.type.TypeDescription typeDescription, + ClassLoader classLoader, net.bytebuddy.utility.JavaModule module, + boolean loaded) {} + + @Override + public void onError(String typeName, ClassLoader classLoader, + net.bytebuddy.utility.JavaModule module, boolean loaded, Throwable throwable) { + System.err.println("[AGENT] 增强类失败: " + typeName); + throwable.printStackTrace(); + } + + @Override + public void onComplete(String typeName, ClassLoader classLoader, + net.bytebuddy.utility.JavaModule module, boolean loaded) {} + }) + // 安装到JVM + .installOn(inst); + + System.out.println("[AGENT] ByteBuddy Agent 安装完成!"); + + } catch (Exception e) { + System.err.println("[AGENT] Agent安装失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 构建类型匹配器 - 展示多种匹配方式 + * 这里演示了ByteBuddy支持的各种目标类匹配方式 + */ + private static net.bytebuddy.matcher.ElementMatcher.Junction buildTypeMatchers() { + return + // 方式1:精确类名匹配 + named("io.github.kazusa.pressmonster.demo.SampleController") + + // 方式2:Spring @RestController注解匹配(使用字符串名称避免类加载问题) + .or(isAnnotatedWith(named("org.springframework.web.bind.annotation.RestController"))) + + // 方式3:自定义@Monitored注解匹配 + .or(isAnnotatedWith(named("io.github.kazusa.pressmonster.demo.annotation.Monitored"))) + + // 方式4:包路径前缀匹配 + .or(nameStartsWith("io.github.kazusa.pressmonster.demo.service")) + + // 方式5:类名后缀匹配 + .or(nameEndsWith("Service")) + + // 方式6:类名包含匹配 + .or(nameContains("Controller")) + + // 方式7:多包路径匹配(OR条件) + .or(nameStartsWith("io.github.kazusa.pressmonster.demo.controller") + .or(nameStartsWith("io.github.kazusa.pressmonster.demo.api"))) + + // 排除条件:排除测试类和内部类 + .and(not(nameContains("Test"))) + .and(not(nameContains("$"))) // 排除内部类 + + // 排除Java核心类和第三方库 + .and(not(nameStartsWith("java."))) + .and(not(nameStartsWith("javax."))) + .and(not(nameStartsWith("sun."))) + .and(not(nameStartsWith("com.sun."))) + .and(not(nameStartsWith("org.springframework."))) + .and(not(nameStartsWith("net.bytebuddy."))); + } + + /** + * 根据类名判断匹配类型 + * 用于在日志中显示是通过什么方式匹配到的目标类 + */ + private static String determineMatchType(String className) { + if ("io.github.kazusa.pressmonster.demo.SampleController".equals(className)) { + return "EXACT_NAME"; + } else if (className.contains("Controller")) { + return "SPRING_CONTROLLER"; + } else if (className.startsWith("io.github.kazusa.pressmonster.demo.service")) { + return "SERVICE_PACKAGE"; + } else if (className.endsWith("Service")) { + return "SERVICE_SUFFIX"; + } else { + return "OTHER_RULE"; + } + } +} \ No newline at end of file diff --git a/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/annotation/Monitored.java b/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/annotation/Monitored.java new file mode 100644 index 0000000..230b2c1 --- /dev/null +++ b/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/annotation/Monitored.java @@ -0,0 +1,36 @@ +package io.github.kazusa.pressmonster.demo.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义监控注解 + * 用于演示基于注解的拦截匹配方式 + * 可以标记在类或方法上,被标记的类/方法将被Agent拦截 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Monitored { + + /** + * 监控描述信息 + */ + String value() default ""; + + /** + * 是否启用性能监控 + */ + boolean enablePerformance() default true; + + /** + * 是否记录参数 + */ + boolean logParameters() default true; + + /** + * 是否记录返回值 + */ + boolean logReturn() default true; +} \ No newline at end of file diff --git a/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/service/OrderService.java b/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/service/OrderService.java new file mode 100644 index 0000000..d451a47 --- /dev/null +++ b/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/service/OrderService.java @@ -0,0 +1,147 @@ +package io.github.kazusa.pressmonster.demo.service; + +import io.github.kazusa.pressmonster.demo.annotation.Monitored; + +import java.util.HashMap; +import java.util.Map; + +/** + * 订单服务类 + * 使用@Monitored注解标记,用于演示基于自定义注解的拦截匹配 + * 这个类会被Agent通过以下规则匹配: + * 1. 自定义注解匹配:isAnnotatedWith(named("io.github.kazusa.pressmonster.demo.annotation.Monitored")) + * 2. 包路径前缀匹配:nameStartsWith("io.github.kazusa.pressmonster.demo.service") + * 3. 类名后缀匹配:nameEndsWith("Service") + */ +@Monitored(value = "订单服务监控", enablePerformance = true, logParameters = true, logReturn = true) +public class OrderService { + + /** + * 根据订单ID查找订单信息 + */ + @Monitored("查询订单详情") + public Map findOrderById(String orderId) { + System.out.println("OrderService.findOrderById() 执行中..."); + + // 模拟数据库查询延时 + try { + Thread.sleep(15); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + Map order = new HashMap<>(); + order.put("id", orderId); + order.put("userId", "user123"); + order.put("amount", 299.99); + order.put("status", "completed"); + order.put("createTime", System.currentTimeMillis() - 86400000); // 一天前 + + return order; + } + + /** + * 根据用户ID获取订单列表 + */ + public Map getOrdersByUserId(String userId) { + System.out.println("OrderService.getOrdersByUserId() 执行中..."); + + // 模拟数据库查询延时 + try { + Thread.sleep(25); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + Map result = new HashMap<>(); + result.put("orders", new String[]{"order001", "order002", "order003"}); + result.put("userId", userId); + result.put("total", 3); + result.put("totalAmount", 899.97); + + return result; + } + + /** + * 创建新订单 + */ + @Monitored(value = "创建订单", logParameters = true) + public Map createOrder(String userId, double amount, String productInfo) { + System.out.println("OrderService.createOrder() 执行中..."); + + // 模拟业务逻辑延时 + try { + Thread.sleep(40); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + String newOrderId = "order_" + System.currentTimeMillis(); + + Map newOrder = new HashMap<>(); + newOrder.put("id", newOrderId); + newOrder.put("userId", userId); + newOrder.put("amount", amount); + newOrder.put("productInfo", productInfo); + newOrder.put("status", "pending"); + newOrder.put("createTime", System.currentTimeMillis()); + + return newOrder; + } + + /** + * 更新订单状态 + */ + public boolean updateOrderStatus(String orderId, String newStatus) { + System.out.println("OrderService.updateOrderStatus() 执行中..."); + + // 模拟数据库操作延时 + try { + Thread.sleep(20); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 模拟状态更新成功 + return true; + } + + /** + * 取消订单 + */ + @Monitored("取消订单") + public boolean cancelOrder(String orderId, String reason) { + System.out.println("OrderService.cancelOrder() 执行中..."); + + // 模拟业务逻辑延时 + try { + Thread.sleep(30); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 模拟取消成功 + return true; + } + + /** + * 计算订单总金额(演示可能出现异常的方法) + */ + public double calculateTotalAmount(String[] orderIds) { + System.out.println("OrderService.calculateTotalAmount() 执行中..."); + + if (orderIds == null || orderIds.length == 0) { + throw new IllegalArgumentException("订单ID列表不能为空"); + } + + // 模拟计算延时 + try { + Thread.sleep(35); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 模拟计算总金额 + return orderIds.length * 199.99; + } +} \ No newline at end of file diff --git a/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/service/UserService.java b/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/service/UserService.java new file mode 100644 index 0000000..0524dfe --- /dev/null +++ b/press-monster-test/demo/src/main/java/io/github/kazusa/pressmonster/demo/service/UserService.java @@ -0,0 +1,117 @@ +package io.github.kazusa.pressmonster.demo.service; + +import java.util.HashMap; +import java.util.Map; + +/** + * 用户服务类 + * 用于演示基于包路径和类名后缀的拦截匹配 + * 这个类会被Agent通过以下规则匹配: + * 1. 包路径前缀匹配:nameStartsWith("io.github.kazusa.pressmonster.demo.service") + * 2. 类名后缀匹配:nameEndsWith("Service") + */ +public class UserService { + + /** + * 根据用户ID查找用户信息 + */ + public Map findUserById(String userId) { + System.out.println("UserService.findUserById() 执行中..."); + + // 模拟数据库查询延时 + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + Map user = new HashMap<>(); + user.put("id", userId); + user.put("name", "User " + userId); + user.put("email", "user" + userId + "@example.com"); + user.put("status", "active"); + + return user; + } + + /** + * 获取所有用户列表 + */ + public Map getAllUsers() { + System.out.println("UserService.getAllUsers() 执行中..."); + + // 模拟数据库查询延时 + try { + Thread.sleep(20); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + Map result = new HashMap<>(); + result.put("users", new String[]{"user1", "user2", "user3", "user4", "user5"}); + result.put("total", 5); + result.put("page", 1); + result.put("pageSize", 10); + + return result; + } + + /** + * 创建新用户 + */ + public Map createUser(String name, String email) { + System.out.println("UserService.createUser() 执行中..."); + + // 模拟数据库操作延时 + try { + Thread.sleep(30); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + String newUserId = "user_" + System.currentTimeMillis(); + + Map newUser = new HashMap<>(); + newUser.put("id", newUserId); + newUser.put("name", name); + newUser.put("email", email); + newUser.put("status", "active"); + newUser.put("createTime", System.currentTimeMillis()); + + return newUser; + } + + /** + * 更新用户信息 + */ + public boolean updateUser(String userId, Map updateData) { + System.out.println("UserService.updateUser() 执行中..."); + + // 模拟数据库操作延时 + try { + Thread.sleep(25); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 模拟更新成功 + return true; + } + + /** + * 删除用户 + */ + public boolean deleteUser(String userId) { + System.out.println("UserService.deleteUser() 执行中..."); + + // 模拟数据库操作延时 + try { + Thread.sleep(15); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 模拟删除成功 + return true; + } +} \ No newline at end of file