spring-aop

2022-07-18

spring-aop

AOP(面向切面编程)

Aspect-Oriented Programming

AOP 中的基本单元是 Aspect(切面)

指在程序运行期间动态的将某段代码切入到指定方法指定位置进行运行的编程方式

一. 基本概念

1.1 Acpect(切面)

aspectpointcountadvice 组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP就是负责实施切面的框架, 它将切面所定义的横切逻辑织入到切面所指定的连接点中.
AOP的工作重心在于如何将增强织入目标对象的连接点上, 这里包含两个工作:

  1. 如何通过 pointcut 和 advice 定位到特定的 joinpoint 上
  2. 如何在 advice 中编写切面代码.

可以简单地认为, 使用 @Aspect 注解的类就是切面.

1.2 advice(增强)

由 aspect 添加到特定的 join point(即满足 point cut 规则的 join point) 的一段代码.

许多 AOP框架, 包括 Spring AOP, 会将 advice 模拟为一个拦截器(interceptor), 并且在 join point 上维护多个 advice, 进行层层拦截.

例如 HTTP 鉴权的实现, 我们可以为每个使用 RequestMapping 标注的方法织入 advice, 当 HTTP 请求到来时, 首先进入到 advice 代码中, 在这里我们可以分析这个 HTTP 请求是否有相应的权限, 如果有, 则执行 Controller, 如果没有, 则抛出异常. 这里的 advice 就扮演着鉴权拦截器的角色了.

即advice就是增强业务功能定义的代码,相当于一个装饰器,给现有的功能添加额外的功能,而不改变现有的逻辑代码

1.3 join point(连接点)

程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理.

spring aop中,连接点只指方法

连接点就是需要增强功能的方法,需要添加advice的方法

1.4 point cut(切点)

切点是一段规则,给符合point cut规则的join point添加advice

只会给满足切点定义规则的连接点增强功能

1.5 Target(目标对象)

织入 advice 的目标对象. 目标对象也被称为 advised object.
因为 Spring AOP 使用运行时代理的方式来实现 aspect, 因此 adviced object 总是一个代理对象(proxied object)
注意, adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类.

1.6 AOP proxy

一个类被 AOP 织入 advice, 就会产生一个结果类, 它是融合了原类和增强逻辑的代理类.
在 Spring AOP 中, 一个 AOP 代理是一个 JDK 动态代理对象或 CGLIB 代理对象.

Spring AOP 默认使用标准的 JDK 动态代理(dynamic proxy)技术来实现 AOP 代理, 通过它, 我们可以为任意的接口实现代理.
如果需要为一个类实现代理, 那么可以使用 CGLIB 代理. 当一个业务逻辑对象没有实现接口时, 那么Spring AOP 就默认使用 CGLIB 来作为 AOP 代理了. 即如果我们需要为一个方法织入 advice, 但是这个方法不是一个接口所提供的方法, 则此时 Spring AOP 会使用 CGLIB 来实现动态代理. 鉴于此, Spring AOP 建议基于接口编程, 对接口进行 AOP 而不是类.

1.7 织入

将 aspect 和其他对象连接起来, 并创建 adviced object 的过程.
根据不同的实现技术, AOP织入有三种方式:

  • 编译器织入, 这要求有特殊的Java编译器.
  • 类装载期织入, 这需要有特殊的类装载器.
  • 动态代理织入, 在运行期为目标类添加增强(Advice)生成子类的方式.
    Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期织入.

二. 实战

2.1 自定义注解

package com.mahaonan.webim.basic.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author: M˚Haonan
 * @Date: 2019-05-21 17:08
 * @Description: 自定义注解,用于在join point上标识point cut规则
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Action {

}

2.2 切面类

package com.mahaonan.webim.basic.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * @Author: M˚Haonan
 * @Date: 2019-05-21 16:57
 * @Description: 测试spring aop
 */
@Aspect
@Component
public class TestAspect {
    /**
     * 定义切点,匹配baseServiceImpl下的所有方法
     */
    @Pointcut("bean(basicServiceImpl)")
    public void noticeLogin(){

    }

    /**
     * 定义advice,aop的业务逻辑方法
     * 在指定方法调用之前执行
     */
    @Before(value = "noticeLogin() && @annotation(Action)")
    public void test1(){
        System.out.println("有人登陆了");
    }


    @Around(value = "noticeLogin() && @annotation(Action)")
    public Object test2(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println(System.currentTimeMillis());
        Object proceed = joinPoint.proceed();
        System.out.println(System.currentTimeMillis());
        return proceed;
    }
}

@Around("pointcut() && @annotation(timeWatch)")
    public Object runTime(ProceedingJoinPoint joinPoint, TimeWatch timeWatch) throws Throwable{
        System.out.println("注解作用的方法名: " + joinPoint.getSignature().getName());
        System.out.println("所在类的简单类名: " + joinPoint.getSignature().getDeclaringType().getSimpleName());
        System.out.println("所在类的完整类名: " + joinPoint.getSignature().getDeclaringType());
        System.out.println("目标方法的声明类型: " + Modifier.toString(joinPoint.getSignature().getModifiers()));
        System.out.println(timeWatch.print());
        System.out.println(joinPoint.getKind());
        Object[] args = joinPoint.getArgs();
        
        System.out.println();
        Object proceed = joinPoint.proceed();
        return proceed;
    }

三. 表达式详解

https://my.oschina.net/u/1251536/blog/1631705

3.1 表达式示例

execution(* com.sample.service.impl..*.*(..))

详述:

  • execution(),表达式的主体
  • 第一个“*”符号,表示返回值类型任意;
  • com.sample.service.impl,AOP所切的服务的包名,即我们的业务部分
  • 包名后面的“..”,表示当前包及子包
  • 第二个“*”,表示类名,*即所有类
  • .*(..),表示任何方法名,括号表示参数,两个点表示任何参数类型

3. 2 execution表达式语法格式

execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)

除了返回类型模式方法名模式参数模式外,其它项都是可选的。

四. 底层原理(代理模式)

五. 问题总结

5.1 aop执行顺序导致的异常信息无法打印

**项目背景:**dubbo服务中有一个aop编写的全局异常处理,专门处理dubbo service抛出的异常,代码如下

@Component
@Aspect
public class ServiceExceptionAdvice {

    protected final Logger log = LoggerFactory.getLogger(getClass());

    @Around("execution(* com.wanmei.roshan.sops.service.*.*(..))")
    public ResponseResult doAround(ProceedingJoinPoint joinPoint) {
        ResponseResult ret;
        try {
            ret = (ResponseResult) joinPoint.proceed();
        } catch (Throwable e) {
            if (e instanceof CodeMessageException) {
                ret = ResponseResultUtil.error(((CodeMessageException) e).getCode(), e.getMessage());
            } else {
                Object[] paramValues = joinPoint.getArgs();
                String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
                // log
                StringBuilder sb = new StringBuilder();
                sb.append("[ServiceException] ").append(joinPoint.getSignature().getDeclaringTypeName()).append("# ")
                        .append(joinPoint.getSignature().getName()).append(" (), params=");
                if (paramNames != null && paramNames.length > 0) {
                    Map<String, Object> paramMap = new HashMap<>();
                    for (int i = 0; i < paramNames.length; i++) {
                        paramMap.put(paramNames[i], paramValues[i]);
                    }
                    sb.append(JsonUtils.objectToJson(paramMap));
                }
                log.info(sb.toString(), e);

                ret = ResponseResultUtil.error(MessageCode.SYSTEM_INTERNAL_ERROR);
            }
        }
        return ret;
    }

}

这里的逻辑很简单,捕获异常,然后包装为ResponseResult对象,返回给消费者

后面,我加了一个同样的aop如下

@Aspect
@Component
@Slf4j
public class LocalParamsAspect {


    @Around("execution(* com.wanmei.roshan.sops.service.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        LocalParamsCache.LOCAL_PARAMS_MAP.set(new HashMap<>());
        try {
            return joinPoint.proceed();
        }catch (Throwable t){
            return null;
        }finally {
            LocalParamsCache.LOCAL_PARAMS_MAP.remove();
        }
    }
}

这个的作用是为了在dubbo调用之前,注入一个本地参数缓存Threadlocal,便于参数传递。问题就出在这个catch块,当抛出异常时,返回null

由于我在写的时候没有指定aop顺序,所以可能是ServiceExceptionAdvice先执行

对于around而言,比如A和B两个aop,如果A先执行,那么顺序如下

A around -> b around -> 业务方法(不抛异常)->b around -> a around

A around -> b around -> 业务方法(抛异常)->b catch -> a round(此时不会进入catch了)

那么就会导致异常捕获发生在了LocalParamsAspect中,这时候直接返回了null,导致异常信息丢失

因此需要指定aop的执行顺序,特别是多个aop同时定义时,最好理清楚业务关系,定制好顺序

因此给ServiceExceptionAdvice加上了顺序@Order(1000),LocalParamsAspect加上了顺序@Order(999),让LocalParamsAspec先执行,异常捕获发生在

ServiceExceptionAdvice