SpringAOP-Advisor

声明式配置

用法示例

@Pointcut("execution(* com.example.demo.Person.test(..))")
public void logPoint() {}

@Aspect
@Conponent // 如果看到有的写法这里没有 @Conponent 注解,一定是其他地方注入了这个 bean。要么就是使用了 AspectJ 的方式
@Order(1)
public class TestAdvice {
    // 环绕通知:四合一通知
    @Around("logPoint()")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        // 1. 获取方法名
        String methodName = pjp.getSignature().getName();
        // 2. 获取类名
        String className = pjp.getTarget().getClass().getSimpleName();
        // 3. 获取参数列表(修改会影响目标方法)
        Object[] args = pjp.getArgs();
        // 4. 目标方法执行的时候,user 参数的id是123
        User user = (User) args[0];
        user.setId(123L);
        try {
            System.out.println("前置通知: Around");
            Object result = pjp.proceed(); // 执行目标方法
            System.out.println("返回通知: Around");
            return result;
        } catch (Exception e) {
            System.out.println("异常通知: Around");
            throw e;
        } finally {
            System.out.println("后置通知: Around");
        }
    }

    // 前置通知:拦截注解
    @Before("@annotation(MyAnnotation)")
    public void beforeAdvice() {
        System.out.println("前置通知: Before");
        // return;  如果前置通知里 return,目标方法将不会执行
    }

    // 异常通知
    @AfterThrowing(pointcut = "@annotation(MyAnnotation)", throwing = "ex") // 通过 throwing 指定异常参数名
    public void afterThrowingAdvice(JoinPoint jp, Exception ex) {
        System.out.println("异常通知: afterThrowing,发生异常:" + ex);
    }

    // 返回通知
    @AfterReturning(pointcut = "@annotation(MyAnnotation)", returning = "result") // 通过 returning 指定返回值参数名
    public void afterReturningAdvice(JoinPoint jp, Object result) {
        System.out.println("返回通知:AfterReturning,结果为:" + result);
    }

    // 后置通知
    @After("@annotation(MyAnnotation)")
    public void afterAdvice() {
        System.out.println("后置通知:logAfter");
    }
}

通知参数

连接点参数

通知类型 参数 必须 说明
@Before JoinPoint
@AfterReturning JoinPoint 通过 returning 属性绑定返回值
@AfterThrowing JoinPoint 通过 throwing 属性绑定异常对象
@After JoinPoint
@Around ProceedingJoinPoint 必须调用 proceed() 执行目标方法

自定义参数

示例1:单个参数

/**
 * 表达式分两部分:execution(* com.example.service.*.*(..)) 和 args(id, ..)
 * 		第一部分 execution(* com.example.service.*.*(..)):就是确定切点的
 * 		第二部分 args(id, ..):表示把目标方法中的第一个参数映射到 id 上
 *
 * public void logUserId(Long id) 映射参数后就可以使用这个参数了,传入方法中
 */
@Before("execution(* com.example.service.*.*(..)) && args(id, ..)")
public void logUserId(Long id) {}

// 这个方法不会作为切点,因为类型不匹配
public String getUserName(Integer userId) {} // 方法参数是Integer

// 这个方法会作为切点
public String getUserName(Long userId) {}

示例2:多个参数

@Around("execution(* com.example.service.*.*(..)) && args(id, name, ..)")
public Object logParams(ProceedingJoinPoint pjp, Long id, String name){}

// 会被拦截
void method1(Long id, String name) 
void method2(Long id, String name, Object... others)
  
// 不会被拦截
void method3(String name, Long id)  // 参数顺序不对
void method4(Long id)               // 缺少第二个String参数

编程式配置

用法示例

1. 前置通知 (MethodBeforeAdvice)

@Bean
public Advisor beforeAdvisor() {
    // 1. 定义切点(匹配所有Service的public方法)
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression("execution(public * com.example.service.*.*(..))");
    
    // 2. 定义前置通知
    Advice advice = (MethodBeforeAdvice) (method, args, target) -> {
        System.out.println("【前置通知】调用方法: " + method.getName());
        System.out.println("  参数: " + Arrays.toString(args));
    };
    advice.setOrder(Ordered.HIGHEST_PRECEDENCE); // 设置优先级最高
    return new DefaultPointcutAdvisor(pointcut, advice);
}

2. 返回通知 (AfterReturningAdvice)

@Bean
public Advisor afterReturningAdvisor() {
    NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
    pointcut.addMethodName("getUser*"); // 匹配getUser开头的方法
    
    Advice advice = (AfterReturningAdvice) (returnValue, method, args, target) -> {
        System.out.println("【返回通知】方法: " + method.getName());
        System.out.println("  返回值: " + returnValue);
    };
    
    return new DefaultPointcutAdvisor(pointcut, advice);
}

3. 异常通知 (ThrowsAdvice)

@Bean
public Advisor throwsAdvisor() {
    JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
    pointcut.setPattern("com.example.service.*"); // 正则匹配
    
    Advice advice = new ThrowsAdvice() {
        // 拦截Exception类型异常
        public void afterThrowing(Method method, Object[] args, Object target, Exception ex) {
            System.out.println("【异常通知】方法: " + method.getName());
            System.out.println("  异常: " + ex.getClass().getSimpleName());
        }
    };
    
    return new DefaultPointcutAdvisor(pointcut, advice);
}

4. 环绕通知 (MethodInterceptor)

@Bean
public Advisor aroundAdvisor() {
  
  	// 切注解
    AnnotationMatchingPointcut pointcut = AnnotationMatchingPointcut.forMethodAnnotation(Loggable.class);
    
    Advice advice = (MethodInterceptor) invocation -> {
        Method method = invocation.getMethod();
        System.out.println("【环绕前置】" + method.getName());
        
        try {
            Object result = invocation.proceed();
            System.out.println("【环绕返回】返回值: " + result);
            return result;
        } catch (Exception e) {
            System.out.println("【环绕异常】" + e.getMessage());
            throw e;
        } finally {
            System.out.println("【环绕后置】");
        }
    };
    
    return new DefaultPointcutAdvisor(pointcut, advice);
}

5. 后置通知 (通过AfterReturningAdvice+ThrowsAdvice组合实现)

@Bean
public Advisor afterAdvisor() {
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression("@annotation(com.example.Audit)");
    
    // 组合Advice实现最终效果
    Advice advice = new org.springframework.aop.support.DelegatingIntroductionInterceptor() {
        @Override
        public Object invoke(MethodInvocation mi) throws Throwable {
            try {
                return super.invoke(mi);
            } finally {
                System.out.println("【后置通知】方法执行完成: " + mi.getMethod().getName());
            }
        }
    };
    
    return new DefaultPointcutAdvisor(pointcut, advice);
}

切点类型

  • AspectJExpressionPointcut:最强大的表达式匹配(声明式配置支持的表达式都可以)

  • NameMatchMethodPointcut:简单方法名匹配

    NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
    pointcut.addMethodName("getUserById");  // 匹配 getUserById 方法
    pointcut.addMethodName("save*");   // 匹配 save 开头的方法
    
  • AnnotationMatchingPointcut:注解匹配

    // 匹配类上有 @Service 注解的方法
    AnnotationMatchingPointcut classPointcut = AnnotationMatchingPointcut.forClassAnnotation(Service.class);
    // 匹配方法上有 @Transactional 注解的方法
    AnnotationMatchingPointcut methodPointcut = AnnotationMatchingPointcut.forMethodAnnotation(Transactional.class);
    
  • JdkRegexpMethodPointcut:正则表达式匹配

    JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
    pointcut.setPattern("com.example.service.*.query[A-Z].*");  // 匹配 query 打头的方法(方法名只能字符串)
    
  • 自定义切点

    public class SuffixMatchPointcut extends StaticMethodMatcherPointcut {
        private final String suffix;
        public SuffixMatchPointcut(String suffix) {
            this.suffix = suffix;
        }
        @Override
        public boolean matches(Method method, Class<?> targetClass) {
            return method.getName().endsWith(suffix);
        }
    }
    
    // 使用示例:前置通知
    @Bean
    public MethodBeforeAdvice beforeAdvisor() {
        return (method, args, target) -> {
            System.out.println("【后缀匹配拦截】方法 " + method.getName() + " 被调用,参数: " + Arrays.toString(args));
        };
    }
    

注意事项

  1. 切面类必须是 Spring bean,如果没有一定其他地方注入了这个 bean,要么就是 AspectJ 方式(看 jvm 参数,maven 依赖和插件可以判断)
  2. 只能拦截 public 普通方法,不能拦截构造方法,字段的访问等(如果需要这些功能要使用 AspectJ)
  3. 顺序问题,有可能被拦截多次,自己的拦截在最外层,内层的拦截报错了,导致自己的拦截信息不准确,可以把优先级调高(数字越小越优先)
  4. Spring 环境中需要使用注解 @EnableAspectJAutoProxy 开启 AOP 功能,如果是 SpringBoot 环境不需要,内部已经自动开启了
posted @ 2024-11-02 20:35  CyrusHuang  阅读(45)  评论(0)    收藏  举报