likes
comments
collection
share

Spring AOP 概念 & 应用 ——AOP 源码解析(一)

作者站长头像
站长
· 阅读数 31

一、概念

参考:

1.1 什么是AOP?

AOP(Aspect Oriented Programming),即面向切面编程,通俗点说,是把代码逻辑的一些公共部分(比如参数校验、事务等)抽象出来,形成功能模块,然后把程序在某个位置(切点)“切开”,把这些抽象好的功能模块,放进去(织入)。这样代码运行时,到达切点后,就可以执行我们抽象出来的通用代码了。

其实AOP与通用功能模块化相比,主要是多了一步把功能“织入”到目标代码这一步,让代码复用更加加单,有时候你甚至不知道用到了AOP。

1.2 术语

简单介绍一下AOP的术语:

  • Pointcut(切点): 定义了要在哪里把代码切开,比如在某个包下的某个公共方法执行前切入,具象化一点,就是Spring中定义切点的表达式,比如:"execution (* com.hellohu.spring.aop.*.*(..))"
  • Advice(通知):就是具体要执行的动作,比如参数校验、异常处理等抽象出来的功能模块。
  • Aspect(切面):Pointcut(切点)+ Advice(通知)。
  • Join Point(连接点) :程序中运行的一个点,与切点的关系,可以类比为对象与类的关系,也就是连接点是程序具体运行时的一个点。具象化一点,就是Spring中的JoinPoint接口,也就是我们定义通知时第一个入参,会提供方法签名、参数等信息。
  • Introduction(引入):在一个类上引入额外的方法&实现,Spring AOP可以给被切的对象添加额外的接口和对应的实现。
  • Target object:被一个或多个切面“织入”的对象,由于Spring使用动态代理,因此在Spring AOP中,始终是代理对象。
  • Weaving(织入):把程序在“Pointcut”处切开,然后把“Advice”放进去的过程。可以在编译时织入(使用aspectJ编译器),可以在类加载的时织入,也可以在运行时织入。Spring AOP使用的是运行时织入。

以上这些概念都比较抽象,其实用起来还是非常简单的,不用过于纠结。

Spring AOP支持多种通知类型:

  • Before advice(前置通知): 在切点之前运行,一般不会影响后续运行,除非抛出异常。
  • After returning advice(返回通知):在切点正常返回后执行。
  • After throwing advice(异常通知): 切点抛出异常后执行。
  • After (finally) advice(后置通知): 切点执行完成之后执行,无论是否抛出异常。
  • Around advice(环绕通知): 可以环绕切点执行,可以选择继续执行切点(甚至可以多次执行),也可以直接返回自定义的结果或者直接抛出异常。

二、使用

我们先看下Spring AOP该如何使用。

2.1 开启AOP支持

引入AOP依赖,至于版本,其实挑一个你喜欢的就好。

<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-aop</artifactId>
   <version>5.3.9</version>
</dependency>
<dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjweaver</artifactId>
   <version>1.9.5</version>
</dependency>

开启AOP的支持,可以使用注解的方式开启,也可以使用XML引入。我们这里仅演示使用注解开启的方法,注解使用@EnableAspectJAutoProxy开启即可。

@Configuration
@EnableAspectJAutoProxy 
public class HelloConfig {
}

2.2 定义切面

使用注解定义切面非常简单,只要使用@Aspect注解即可:

@Aspect
@Component
public class LogAspect {}

切面 = 切点+通知,有了切面类,接下来就是要定义切点和通知了。

2.2.1 定义切点

Spring AOP使用一个类似正则的表达式,来表示切点,确切的说,是复用(或者说是实现)了ASpectJ的提供的部分注解。ASpectJ定义的切点有很多种,不过Spring AOP,仅支持在方法上定义切点,其他ASpectJ中定义的在属性、构造器上的切点,是不支持的。接下来我们定义一下切点:

@Pointcut("execution (* com.hellohu.spring.aop.*.*(..))")
public void pointCut() {
}

以上代表切所有的com.hellohu.spring.aop包下类的所有公共方法。切点的表达式有很多种,这里解释下比较常用的execution表达式:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
  • modifier:修饰符,public, private等,不写表示匹配任意修饰符(?代表可选)
  • ret-type:返回类型,“*”表示匹配任意类型
  • declaring-type:类路径,包括包名,不写匹配任意类型,“..”表示匹配所有子包和类,比如 com.hellohu.spring.aop..表示匹配com.hellohu.spring.aop包下所有类。
  • name-pattern:方法名称,使用“*”表示通配符
  • param-pattern:参数类型和数量,*代表任意类型的一个参数,“..”则是任意类型任意数量
    • () 匹配没有参数的方法
    • (..) 匹配有任意数量参数的方法
    • (*) 匹配有一个任意类型参数的方法
    • (*,String) 匹配有两个参数的方法,并且第一个为任意类型,第二个为 String 类型
  • throws-pattern:抛出异常类型,不写是匹配任意类型

以上就是execution表达式的说明,其他的还有args、this等表达式,这里不再赘述,可以参考第15章-Spring AOP切点表达式(Pointcut)详解-CSDN博客

另外,表达式直接,也可以使用"与或非"(&& || !)连接,比如

@Pointcut("execution (* com.hellohu.spring.aop.*.*(..)) && !bean(notCut*)")

以上表示匹配com.hellohu.spring.aop包下所有类的所有public方法,但是排除名字以notCut开头的Bean。

2.2.2 定义通知

我们简单定义一个日志通知,把所有的通知类型都简单的实现一下,正好顺便可以观察一下通知的执行顺序。

package com.hellohu.spring.aop;

import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LogAspect {

    @Pointcut("execution (* com.hellohu.spring.aop.*.*(..))")
    public void pointCut() {
    }

    @Before(value = "pointCut()")
    public void before(JoinPoint joinPoint) {
        //记录参数
        System.out.println("before advice: " + genSignatureStr(joinPoint.getSignature()) + ", args:[" + StringUtils.join(joinPoint.getArgs(), ",") + "]");
    }

    @AfterReturning(value = "pointCut()", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {
        //记录返回值
        String resultString = result == null ? "void" : result.toString();
        System.out.println("return advice: " + genSignatureStr(joinPoint.getSignature()) + ", return :" + resultString);
    }


    @After(value = "pointCut()")
    public void after(JoinPoint joinPoint) {
        //什么也没记录
        System.out.println("after/finally advice: " + genSignatureStr(joinPoint.getSignature()));
    }

    @AfterThrowing(value = "pointCut()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Exception e) {
        //记录运行时的异常
        System.out.println("after throwing advice: " + genSignatureStr(joinPoint.getSignature()) + " exception:" + e.getMessage());
    }

    @Around(value = "pointCut()")
    public Object around(JoinPoint joinPoint) throws Throwable {
        //记录运行耗时
        System.out.println("around advice begin");
        long now = System.currentTimeMillis();
        try {
            return ((ProceedingJoinPoint) joinPoint).proceed();
        } finally {
            long cost = System.currentTimeMillis() - now;
            System.out.println("around advice end " + genSignatureStr(joinPoint.getSignature()) + " cost " + cost + "ms");
        }
    }


    private String genSignatureStr(Signature signature) {
        return signature.getDeclaringType().getName() + "." + signature.getName();
    }
}

2.2.3 测试执行

我们先定义一个被“切”的类

package com.hellohu.spring.aop;
import java.util.Objects;
import org.springframework.stereotype.Repository;

@Repository
public class AopDaoImpl{

    public int update(String s) {
        Objects.requireNonNull(s);
        System.out.println("update " + s);
        return 1;
    }
}

测试代码,第一次调用update会正常执行,第二次会抛出空指针异常。

@Test
public void testAop() {
    try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(HelloConfig.class)) {
        AopDao aopDao = ctx.getBean(AopDao.class);
        aopDao.update("hello world!!");
        aopDao.update(null);
    }
}

第一次aopDao.update("hello world!!");执行结果:

around advice begin
before advice: com.hellohu.spring.aop.AopDao.update, args:[hello world!!]
update hello world!!
return advice: com.hellohu.spring.aop.AopDao.update, return :1
after/finally advice: com.hellohu.spring.aop.AopDao.update
around advice end com.hellohu.spring.aop.AopDao.update cost 4ms

第二次aopDao.update(null);执行结果:

around advice begin
before advice: com.hellohu.spring.aop.AopDao.update, args:[]
after throwing advice: com.hellohu.spring.aop.AopDao.update exception:null
after/finally advice: com.hellohu.spring.aop.AopDao.update
around advice end com.hellohu.spring.aop.AopDao.update cost 0ms
。。。省略异常日志。。。

从执行效果上来看,五种通知执行的先后顺序为:

  1. 环绕通知。
  2. 前置通知。
  3. 返回/异常通知二选一,正常返回执行返回通知,否则执行异常通知。
  4. 后置通知,该通知无论是否异常,都会执行,类似于finally的效果。

2.2.4 补充说明

  1. 切点的表达式,也可以直接写在各种注解的value属性上,比@Around(value = "execution (* com.hellohu.spring.aop.*.*(..))"),效果是一样的,只不过定义在方法上,更方便复用。上面所有的通知,统一使用"pointCut()"即可。甚至不在一个切面类中,也可以复用,只不过需要指定类的全路径了,比如我在另一个切面复用这个表达式: @Around(value = "com.hellohu.spring.aop.LogAspect.pointCut()")
  2. 这个例子中,仅定义了一个切面类,那如果多个切面类,执行的先后顺序是怎样的呢,可以通过@Order注解来指定切面的优先级。

三、执行原理

后续文章,会通过解析Spring AOP的源码,对AOP的原理一探究竟。不过在探究原理前,我们可以大致猜下Spring AOP是如何实现的。

  1. 引入,通过@EnableAspectJAutoProxy注解开启AOP功能,那必然是通过该注解引入了某个 BeanPostProcessor,在bean创建完成后,对bean做了动态代理。
  2. 关于动态代理,相信你已经熟知了,无非是基于JDK还是CGLIB,不过无论使用哪种动态代理,如何把各种切面组织在一起,各种通知执行的顺序等等,应该是统一写在一起来维护的。
  3. 具体通知的实现,各种通知应该是不同的,不出意外,应该是典型的模板+策略模式。

后续会逐步解析Spring AOP的源码,自下而上的分析,也就是说,按照上面猜测的步骤,3/2/1的方式去分析。

系列所有文章

转载自:https://juejin.cn/post/7366086988350504971
评论
请登录