- Maven 依赖 (pom.xml)
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="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>- 主应用类
packagecom.example;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.context.ConfigurableApplicationContext;importorg.springframework.context.annotation.EnableAspectJAutoProxy;@SpringBootApplication@EnableAspectJAutoProxy(proxyTargetClass=true)publicclassDynamicPointcutApplication{publicstaticvoidmain(String[]args){ConfigurableApplicationContextcontext=SpringApplication.run(DynamicPointcutApplication.class,args);// 测试动态切入点UserServiceuserService=context.getBean(UserService.class);OrderServiceorderService=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
packagecom.example.aop;importorg.springframework.aop.MethodMatcher;importorg.springframework.aop.support.DynamicMethodMatcherPointcut;importorg.springframework.stereotype.Component;importjava.lang.reflect.Method;/** * 自定义动态方法匹配切入点 * 动态匹配:每次方法调用时都会执行匹配检查 * 可以根据运行时参数决定是否应用通知 */@ComponentpublicclassUserIdAuditPointcutextendsDynamicMethodMatcherPointcut{/** * 静态匹配检查 - 在代理创建时执行一次 * 可以在这里进行快速筛选,减少动态检查的开销 */@Overridepublicbooleanmatches(Methodmethod,Class<?>targetClass){// 只匹配UserService类if(!targetClass.getName().contains("UserService")){returnfalse;}// 只匹配方法名以"getUser"开头的方法StringmethodName=method.getName();returnmethodName.startsWith("getUser");}/** * 动态匹配检查 - 每次方法调用时执行 * 可以根据方法参数进行动态判断 */@Overridepublicbooleanmatches(Methodmethod,Class<?>targetClass,Object...args){// 如果静态匹配不通过,直接返回falseif(!matches(method,targetClass)){returnfalse;}// 检查参数if(args!=null&&args.length>0){ObjectfirstArg=args[0];// 只对userId为奇数的请求进行审计if(firstArginstanceofLong){LonguserId=(Long)firstArg;returnuserId%2!=0;// 只审计奇数ID}}returnfalse;}/** * 这个方法来自MethodMatcher接口 * 对于DynamicMethodMatcherPointcut,必须返回true * 表示这是一个动态匹配器 */@OverridepublicbooleanisRuntime(){returntrue;// 表明这是动态切入点}}- 定义通知 (Advice)
packagecom.example.aop;importorg.aopalliance.intercept.MethodInterceptor;importorg.aopalliance.intercept.MethodInvocation;importorg.springframework.stereotype.Component;importjava.util.Arrays;/** * 审计通知 - 在方法执行前后进行审计 */@ComponentpublicclassAuditAdviceimplementsMethodInterceptor{@OverridepublicObjectinvoke(MethodInvocationinvocation)throwsThrowable{StringmethodName=invocation.getMethod().getName();Object[]args=invocation.getArguments();// 前置审计System.out.println("【审计开始】方法: "+methodName+", 参数: "+Arrays.toString(args));longstartTime=System.currentTimeMillis();try{// 执行原方法Objectresult=invocation.proceed();// 后置审计longendTime=System.currentTimeMillis();System.out.println("【审计成功】方法: "+methodName+", 执行时间: "+(endTime-startTime)+"ms"+", 结果: "+result);returnresult;}catch(Exceptione){// 异常审计System.out.println("【审计失败】方法: "+methodName+", 异常: "+e.getMessage());throwe;}}}- 配置 AOP
packagecom.example.config;importcom.example.aop.AuditAdvice;importcom.example.aop.UserIdAuditPointcut;importorg.springframework.aop.Advisor;importorg.springframework.aop.support.DefaultPointcutAdvisor;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;@ConfigurationpublicclassAopConfig{@BeanpublicAdvisoruserIdAuditAdvisor(UserIdAuditPointcutpointcut,AuditAdviceadvice){// 将切入点与通知组合成AdvisorreturnnewDefaultPointcutAdvisor(pointcut,advice);}}- 业务服务类
packagecom.example.service;importorg.springframework.stereotype.Service;importjava.util.Arrays;importjava.util.List;@ServicepublicclassUserService{/** * 这个方法会被动态切入点匹配 * 只有当userId为奇数时才会触发审计 */publicStringgetUserById(LonguserId){System.out.println("执行 getUserById, userId: "+userId);return"User-"+userId;}/** * 这个方法会被静态匹配过滤掉(不以getUser开头) */publicList<String>getAllUsers(){System.out.println("执行 getAllUsers");returnArrays.asList("User-1","User-2","User-3");}/** * 这个方法会被静态匹配到,但动态匹配可能被过滤 */publicStringgetUserByName(Stringname){System.out.println("执行 getUserByName, name: "+name);return"User: "+name;}}```javapackagecom.example.service;importorg.springframework.stereotype.Service;@ServicepublicclassOrderService{publicStringcreateOrder(Stringproduct,intquantity){System.out.println("创建订单: "+product+", 数量: "+quantity);return"Order-"+System.currentTimeMillis();}publicbooleancancelOrder(LongorderId){System.out.println("取消订单: "+orderId);returntrue;}}- 测试控制器 (可选,用于Web测试)
packagecom.example.controller;importcom.example.service.UserService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PathVariable;importorg.springframework.web.bind.annotation.RestController;@RestControllerpublicclassTestController{@AutowiredprivateUserServiceuserService;@GetMapping("/user/{id}")publicStringgetUser(@PathVariableLongid){returnuserService.getUserById(id);}@GetMapping("/users")publicStringgetAllUsers(){returnuserService.getAllUsers().toString();}}- 测试类
packagecom.example;importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importcom.example.service.UserService;@SpringBootTestclassDynamicPointcutApplicationTests{@AutowiredprivateUserServiceuserService;@TestvoidtestDynamicPointcut(){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");}}运行结果示例
=== 测试开始 === 执行 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拦截。