Spring AOP(一):AOP 的介绍与使用

70 阅读7分钟

AOP 编程思想

在 Spring 中 IOC 与 AOP 是两大最为核心的思想,其中AOP 的全称是 “Aspect Oriented Programming”,即面向切面编程

  • 从结构上来说,它利用“横切”的技术,解剖开封装的对象内部,将可重用的代码织入进类中;
  • 从功能上来说是 OOP 的补充和完善,OOP 引入封装、继承、多态等概念来建立一种纵向的对象层次结构,但是并不适合处理横向的关系,比如日志、安全校验、统一异常处理等这种散布在各处的无关代码(横切 cross cutting)

AOP 技术能将这些影响了多个类的公共行为封装到一个可重用模块(切面 Aspect) 。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

AOP 技术将软件系统分为两个部分,核心关注点与横切关注点。AOP的作用在于分离系统中的核心关注点和横切关注点。

  • 核心关注点:业务处理的主要流程
  • 横切关注点:经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。

AOP 核心概念

1、 AOP 切面概念

  • 切面(Aspect) :切面由切点和增强/通知组成,它既包括了横切逻辑的定义、也包括了连接点的定义。
  • 连接点(Join point) :能够被拦截的地方,Spring AOP 是基于动态代理的,所以是方法拦截的,每个成员方法都可以称之为连接点;
  • 增强/通知(Advice) :表示添加到切点的一段逻辑代码,并定位连接点的方位信息,简单来说就定义了是干什么的,具体是在哪干;
  • 切点(Poincut) :每个方法都可以称之为连接点,我们具体定位到某一个方法就成为切点;
  • 引入/引介(Introduction) :允许我们向现有的类添加新方法或属性,是一种特殊的增强
  • 织入(Weaving) :将增强/通知添加到目标类的具体连接点上的过程;
    • 切面的织入有三种方式:编译时织入(AspectJ)、类加载时期织入、运行时织入(Spring)
  • 目标对象(target object) :引入中所提到的目标类,也就是要被通知的对象,也就是真正的业务逻辑,他可以在毫不知情的情况下,被织入切面,而自己专注于业务本身的逻辑
  • 代理对象(proxy object) :将切面织入目标对象后所得到的就是代理对象。代理对象是正在具备通知所定义的功能,并且被引入了的对象。

2、 五种通知类型

  • 前置通知(Before Advice) :在目标方法被调用前调用通知功能;
  • 后置通知(After Advice) :在目标方法被调用之后调用通知功能;
  • 返回通知(After-returning) :在目标方法成功执行之后调用通知功能;
  • 异常通知(After-throwing) :在目标方法抛出异常之后调用通知功能;
  • 环绕通知(Around) :把整个目标方法包裹起来,在被调用前和调用之后分别调用通知功能。

AOP 简单使用

1、 配置开启 AOP

在启动类或配置类上加 @EnableAspectJAutoProxy 注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
	// 是否使用CGLIB代理,默认不使用。默认使用JDK动态代理
	boolean proxyTargetClass() default false;
	
    // 是否将代理类作为线程本地变量(threadLocal)暴露(可以通过AopContext访问)
    // 主要设计的目的是用来解决内部调用的问题
	boolean exposeProxy() default false;
}

2、定义切面

使用@Aspect 注解定义切面类来管理 AOP

@Aspect  // 定义切面
@Component // 交由 Spring 管理
public class ControllerLogAspect {
	// 定义切点
    
}

3、 定义切点

切点表达式:

官网中一共给出了 9 种切点表达式的定义方式,但是工作中常用的是excecution表达式以及annotation表达式。有兴趣的可以查看 Spring 官网 了解其他定义方式。

excecution 表达式

// <>为非必填
execution(
    <modifiers-pattern>  		// 方法的可见性,如public,protected;
    ret-type-pattern 			// 方法的返回值类型,如int,void等;
    <declaring-type-pattern>    // 方法所在类的全路径名
    name-pattern 				// 方法名类型
    (param-pattern) 			// 方法的参数类型
    <throws-pattern>			// 方法抛出的异常类型
)

// 示例
// 1.所有权限为public的,返回值不限,方法名称不限,方法参数个数及类型不限的方法,简而言之,所有public的方法
execution(public * *(..))

// 2.所有权限为public的,返回值限定为String的,方法名称不限,方法参数个数及类型不限的方法
execution(public java.lang.String *(..)) 

// 3.所有权限为public的,返回值限定为String的,方法名称限定为test开头的,方法参数个数及类型不限的方法
execution(public java.lang.String test*(..))
 
// 4.所有权限为public的,返回值限定为String的,方法所在类限定为com.xiaoyi.learn.facade.controller包下的任意类,方法名称限定为test开头的,方法参数个数及类型不限的方法
execution(public java.lang.String com.xiaoyi.learn.facade.controller.*.test*(..))
     
// 5.所有权限为public的,返回值限定为String的,方法所在类限定为com.xiaoyi.learn.facade.controller包及其子包下的任意类,方法名称限定为test开头的,方法参数个数及类型不限的方法
execution(public java.lang.String com.xiaoyi.learn.facade.controller..*.test*(..))
 
// 6.所有权限为public的,返回值限定为String的,方法所在类限定为com.xiaoyi.learn.facade.controller包及其子包下的Controller结尾的类,方法名称限定为test开头的,方法参数个数及类型不限的方法
execution(public java.lang.String com.xiaoyi.learn.facade.controller..*Controller.test*(..))
 
// 7.所有权限为public的,返回值限定为String的,方法所在类限定为com.xiaoyi.learn.facade.controller包及其子包下的Controller结尾的类,方法名称限定为test开头的,方法参数限定第一个为String类,第二个不限但是必须有两个参数
execution(public java.lang.String com.xiaoyi.learn.facade.controller..*Controller.test*(String,*))

// 8.所有权限为public的,返回值限定为String的,方法所在类限定为com.xiaoyi.learn.facade.controller包及其子包下的Controller结尾的类,方法名称限定为test开头的,方法参数限定第一个为String类,第二个可有可无并且不限定类型
execution(public java.lang.String com.xiaoyi.learn.facade.controller..*Controller.test*(String,..))

权限修饰符能不能是 public 之外的其他修饰符?

如果使用的是JDK动态代理,这个修饰符必须是public,因为JDK动态代理是针对于目标类实现的接口进行的,接口的实现方法必定是public的。

如果不使用JDK动态代理而使用CGLIB代理(@EnableAspectJAutoProxy(proxyTargetClass = true))那么修饰符还可以使用protected或者默认修饰符。但是不能使用private修饰符,因为CGLIB代理生成的代理类是继承目标类的,private方法子类无法复写,自然也无法代理。基于此,修饰符是不能写成*这种格式的。

@annotation表达式

自定义注解的方式实现方法拦截

@annotation(annotation-type)

// 代表所有被ControllerLogAnnotation注解所标注的方法
// 使用注解的方法定义切点一般会和自定义注解配合使用
@annotation(ControllerLogAnnotation)

定义通知

上述五种通知类型,根据业务需求采用不同的通知类型,以下举例请求日志统一拦截需求:

@Slf4j
@Aspect
@Order(-1) // 方法上有多个通知时进行排序
@Component
public class ControllerLogAspect {

    @Pointcut("execution(* com.xiaoyi.learn.facade.api.controller.*.*(..))")
    private void executionPointcut() {
    }

    @Pointcut("@annotation(ControllerLogAnnotation)")
    private void annotationPointcut() {
    }

    @Around("executionPointcut() || annotationPointcut()")
    public Object controllerLog(ProceedingJoinPoint joinPoint) throws Throwable {
        return this.controllerLogHandler(joinPoint);
    }

    /**
     * Controller 请求包装, 打印日志, 转换异常
     */
    private Object controllerLogHandler(ProceedingJoinPoint joinPoint) throws Throwable{

        try {
            String methodName = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
            Object[] args = joinPoint.getArgs();
            log.info("start execute. method = {} | args = {}", methodName, JSONObject.toJSONString(args));
            long start = System.currentTimeMillis();
            Object result = joinPoint.proceed();
            long end = System.currentTimeMillis();
            log.info("end execute. time = {} | method = {} | args = {} | result = {}",
                end - start, methodName,
                JSONObject.toJSONString(args),
                JSONObject.toJSONString(result)
            );
            return result;
        } catch (Exception ex) {
            // TODO RPC 中可根据不同的异常或业务编码转换成系统内异常类型
            throw new BusinessException(ErrorCodeEnum.RPC_ERROR, ex);
        }
    }
}

通知中获取参数与执行

JoinPoint 接口,用于获取执行的接口参数信息

public interface JoinPoint {
	// 返回代理对象
    Object getThis();
	// 返回目标对象
    Object getTarget();
	// 返回当前的切点的参数
    Object[] getArgs();
	// 返回这个目标类中方法的描述信息,比如修饰符,名称等
    Signature getSignature();

    SourceLocation getSourceLocation();

    String getKind();

    JoinPoint.StaticPart getStaticPart();

}

ProceedingJoinPoint:继承 JoinPoint 接口,增加了提供执行的两个方法

public interface ProceedingJoinPoint extends JoinPoint {
    void set$AroundClosure(AroundClosure var1);

    default void stack$AroundClosure(AroundClosure arc) {
        throw new UnsupportedOperationException();
    }
	// 直接执行当前的方法
    Object proceed() throws Throwable;
	// 可以改变当前执行方法的参数,然后用改变后的参数执行这个方法
    Object proceed(Object[] var1) throws Throwable;
}

总结

以上就是对 Spring AOP 的简单介绍与使用,在 Spring 中有很多功能是基于 AOP 来实现的,比如使用@RestControllerAdvice 与 @ExceptionHandler 进行全局异常处理、Spring 事务管理机制等。