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拦截。

相关推荐
廋到被风吹走17 小时前
【Spring 】Spring Security深度解析:过滤器链、认证授权架构与现代集成方案
java·spring·架构
予枫的编程笔记17 小时前
Elasticsearch聚合分析与大规模数据处理:解锁超越搜索的进阶能力
java·大数据·人工智能·分布式·后端·elasticsearch·全文检索
BD_Marathon17 小时前
PostMan简介
java
一勺菠萝丶17 小时前
宝塔 vs 1Panel 有什么区别?能不能同时安装?
java
码农小卡拉17 小时前
Springboot “钩子”:@PostConstruct注解
java·spring boot·后端·spring·spring cloud
毕设源码-郭学长17 小时前
【开题答辩全过程】以 快递仓库管理系统为例,包含答辩的问题和答案
java
William_cl17 小时前
ASP.NET Core ViewData:弱类型数据交互的精髓与避坑指南
后端·asp.net·交互
奔波霸的伶俐虫17 小时前
spring boot集成kafka学习
spring boot·学习·kafka
内存不泄露17 小时前
基于Spring Boot和Vue的在线考试系统设计与实现
vue.js·spring boot·后端