- Maven 依赖 (pom.xml)
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
http://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>2.7.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>dynamic-pointcut-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 主应用类
java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DynamicPointcutApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context =
SpringApplication.run(DynamicPointcutApplication.class, args);
// 测试动态切入点
UserService userService = context.getBean(UserService.class);
OrderService orderService = context.getBean(OrderService.class);
System.out.println("=== 测试开始 ===");
// 测试1: 匹配方法
userService.getUserById(123L);
userService.getUserById(456L);
// 测试2: 不匹配的方法
userService.getAllUsers();
// 测试3: 其他服务的方法
orderService.createOrder("product1", 2);
orderService.cancelOrder(789L);
System.out.println("=== 测试结束 ===");
context.close();
}
}
- 自定义 DynamicMethodMatcherPointcut
java
package com.example.aop;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.support.DynamicMethodMatcherPointcut;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 自定义动态方法匹配切入点
* 动态匹配:每次方法调用时都会执行匹配检查
* 可以根据运行时参数决定是否应用通知
*/
@Component
public class UserIdAuditPointcut extends DynamicMethodMatcherPointcut {
/**
* 静态匹配检查 - 在代理创建时执行一次
* 可以在这里进行快速筛选,减少动态检查的开销
*/
@Override
public boolean matches(Method method, Class<?> targetClass) {
// 只匹配UserService类
if (!targetClass.getName().contains("UserService")) {
return false;
}
// 只匹配方法名以"getUser"开头的方法
String methodName = method.getName();
return methodName.startsWith("getUser");
}
/**
* 动态匹配检查 - 每次方法调用时执行
* 可以根据方法参数进行动态判断
*/
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
// 如果静态匹配不通过,直接返回false
if (!matches(method, targetClass)) {
return false;
}
// 检查参数
if (args != null && args.length > 0) {
Object firstArg = args[0];
// 只对userId为奇数的请求进行审计
if (firstArg instanceof Long) {
Long userId = (Long) firstArg;
return userId % 2 != 0; // 只审计奇数ID
}
}
return false;
}
/**
* 这个方法来自MethodMatcher接口
* 对于DynamicMethodMatcherPointcut,必须返回true
* 表示这是一个动态匹配器
*/
@Override
public boolean isRuntime() {
return true; // 表明这是动态切入点
}
}
- 定义通知 (Advice)
java
package com.example.aop;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* 审计通知 - 在方法执行前后进行审计
*/
@Component
public class AuditAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
String methodName = invocation.getMethod().getName();
Object[] args = invocation.getArguments();
// 前置审计
System.out.println("【审计开始】方法: " + methodName +
", 参数: " + Arrays.toString(args));
long startTime = System.currentTimeMillis();
try {
// 执行原方法
Object result = invocation.proceed();
// 后置审计
long endTime = System.currentTimeMillis();
System.out.println("【审计成功】方法: " + methodName +
", 执行时间: " + (endTime - startTime) + "ms" +
", 结果: " + result);
return result;
} catch (Exception e) {
// 异常审计
System.out.println("【审计失败】方法: " + methodName +
", 异常: " + e.getMessage());
throw e;
}
}
}
- 配置 AOP
java
package com.example.config;
import com.example.aop.AuditAdvice;
import com.example.aop.UserIdAuditPointcut;
import org.springframework.aop.Advisor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AopConfig {
@Bean
public Advisor userIdAuditAdvisor(UserIdAuditPointcut pointcut,
AuditAdvice advice) {
// 将切入点与通知组合成Advisor
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
- 业务服务类
java
package com.example.service;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
@Service
public class UserService {
/**
* 这个方法会被动态切入点匹配
* 只有当userId为奇数时才会触发审计
*/
public String getUserById(Long userId) {
System.out.println("执行 getUserById, userId: " + userId);
return "User-" + userId;
}
/**
* 这个方法会被静态匹配过滤掉(不以getUser开头)
*/
public List<String> getAllUsers() {
System.out.println("执行 getAllUsers");
return Arrays.asList("User-1", "User-2", "User-3");
}
/**
* 这个方法会被静态匹配到,但动态匹配可能被过滤
*/
public String getUserByName(String name) {
System.out.println("执行 getUserByName, name: " + name);
return "User: " + name;
}
}
```java
package com.example.service;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
public String createOrder(String product, int quantity) {
System.out.println("创建订单: " + product + ", 数量: " + quantity);
return "Order-" + System.currentTimeMillis();
}
public boolean cancelOrder(Long orderId) {
System.out.println("取消订单: " + orderId);
return true;
}
}
- 测试控制器 (可选,用于Web测试)
java
package com.example.controller;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Autowired
private UserService userService;
@GetMapping("/user/{id}")
public String getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
@GetMapping("/users")
public String getAllUsers() {
return userService.getAllUsers().toString();
}
}
- 测试类
java
package com.example;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.example.service.UserService;
@SpringBootTest
class DynamicPointcutApplicationTests {
@Autowired
private UserService userService;
@Test
void testDynamicPointcut() {
System.out.println("=== 测试动态切入点 ===");
// 测试1: userId为123(奇数)- 应该触发审计
System.out.println("\n测试1 - 奇数ID (应该触发审计):");
userService.getUserById(123L);
// 测试2: userId为456(偶数)- 不应该触发审计
System.out.println("\n测试2 - 偶数ID (不应该触发审计):");
userService.getUserById(456L);
// 测试3: getAllUsers - 不应该触发审计
System.out.println("\n测试3 - getAllUsers (不应该触发审计):");
userService.getAllUsers();
// 测试4: getUserByName - 参数不是Long,静态匹配但动态不匹配
System.out.println("\n测试4 - getUserByName (不应该触发审计):");
userService.getUserByName("John");
}
}
运行结果示例
text
=== 测试开始 ===
执行 getAllUsers
【审计开始】方法: getUserById, 参数: [123]
执行 getUserById, userId: 123
【审计成功】方法: getUserById, 执行时间: 2ms, 结果: User-123
执行 getUserById, userId: 456
执行 getAllUsers
创建订单: product1, 数量: 2
取消订单: 789
=== 测试结束 ===
关键点说明
动态匹配 vs 静态匹配:
matches(Method, Class<?>):静态匹配,在代理创建时执行一次
matches(Method, Class<?>, Object...):动态匹配,每次方法调用时执行
性能考虑:
动态匹配有性能开销,因为每次方法调用都需要检查
应该先进行静态匹配过滤,减少动态匹配的调用次数
使用场景:
需要根据运行时参数决定是否应用通知
例如:只审计特定参数值的调用、参数验证等
配置要点:
isRuntime() 必须返回 true
需要通过 DefaultPointcutAdvisor 将切入点和通知组合
这个示例展示了如何创建和使用 DynamicMethodMatcherPointcut 来实现基于方法参数的动态AOP拦截。