Spring Boot实现DynamicMethodMatcherPointcut示例

  1. 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>
  1. 主应用类
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();
    }
}
  1. 自定义 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; // 表明这是动态切入点
    }
}
  1. 定义通知 (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;
        }
    }
}
  1. 配置 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);
    }
}
  1. 业务服务类
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;
    }
}
  1. 测试控制器 (可选,用于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();
    }
}
  1. 测试类
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拦截。

相关推荐
步步为营DotNet1 小时前
深度解析CancellationToken:.NET中的优雅取消机制
java·前端·.net
leobertlan7 小时前
2025年终总结
前端·后端·程序员
面向Google编程8 小时前
从零学习Kafka:数据存储
后端·kafka
易安说AI9 小时前
Claude Opus 4.6 凌晨发布,我体验了一整晚,说说真实感受。
后端
易安说AI9 小时前
Ralph Loop 让Claude无止尽干活的牛马...
前端·后端
易安说AI9 小时前
用 Claude Code 远程分析生产日志,追踪 Claude Max 账户被封原因
后端
JH30739 小时前
SpringBoot 优雅处理金额格式化:拦截器+自定义注解方案
java·spring boot·spring
颜酱10 小时前
图结构完全解析:从基础概念到遍历实现
javascript·后端·算法
Coder_Boy_10 小时前
技术让开发更轻松的底层矛盾
java·大数据·数据库·人工智能·深度学习
invicinble10 小时前
对tomcat的提供的功能与底层拓扑结构与实现机制的理解
java·tomcat