Spring AOP

大纲:

  • AOP概念
  • AOP实现方式与原理
  • 代理模式

1. AOP概述

AOP是Spring框架的第⼆⼤核⼼

AOP-面向切面编程

什么是面向切面编程?

切⾯就是指某⼀类特定问题

什么是⾯向特定⽅法编程呢?

⽐如学习的"登录校验",就是⼀类特定问题.登录校验拦截器,就是对"登录校验"这类问题的统⼀处理.所以,拦截器也是AOP的⼀种应⽤.

AOP是⼀种思想,拦截器是AOP思想的⼀种实现.Spring框架实现了这种思想,提供了拦截器技术的相关接⼝.

同样的,统⼀数据返回格式和统⼀异常处理,也是AOP思想的⼀种实现

简单来说: AOP是⼀种思想,是对某⼀类事情的集中处理

什么是SpringAOP?

Spring AOP是AOP的一种实现方式

举个例子:

现在有⼀个项⽬,项⽬中开发了很多的业务功能,有⼀些业务的执⾏效率⽐较低,耗时较⻓,我们需要对接⼝进⾏优化,那么就需要定位出执⾏耗时⽐较⻓的业务⽅法,再针对该业务⽅法来进⾏优化

如何定位呢?

我们就需要统计当前项⽬中每⼀个业务⽅法的执⾏耗时

可以在业务⽅法运⾏前和运⾏后,记录下⽅法的开始时间和结束时间,两者之差就是这个⽅法的耗时.

AOP就可以做到在不改动这些原始⽅法的基础上,针对特定的⽅法进⾏功能的增强(解耦)

 

2. Spring AOP入门

需求统计图书系统各个接⼝⽅法的执⾏时间 

2.1 引入AOP依赖

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-aop</artifactId>
 </dependency>

2.2 编写AOP程序 

 

@Aspect
@Slf4j
@Component
public class TimeAspect {
    @Around("execution(* com.example.Bookdemo.controller.*.*(..))")

    public Object timeCost(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
       //执行目标方法
        Object result = joinPoint.proceed();

        long end = System.currentTimeMillis();
        log.info(joinPoint+"消耗时间: "+(end-start)+"ms");
        return result;
    }
}

其中:

  • @Aspect:标识这是⼀个切⾯类
  • @Around: 环绕通知,在⽬标⽅法的前后都会被执⾏.后⾯的表达式表⽰对哪些⽅法进⾏增强. 
  • ProceedingJoinPoint.proceed() 让原始⽅法执⾏

通过上⾯的程序,我们也可以感受到AOP⾯向切⾯编程的⼀些优势: 

  • 代码⽆侵⼊:不修改原始的业务⽅法,就可以对原始的业务⽅法进⾏了功能的增强或者是功能的改变 
  • 减少了重复代码 
  • 提⾼开发效率 
  • 维护⽅便

 

3. Spring AOP详解

3.1 核心概念

3.1.1 切点((Pointcut))

Pointcut 的作⽤就是提供⼀组规则,告诉程序对哪些⽅法来进⾏功能增强.

切点表达式:

 

3.1.2 连接点(JoinPoint) 

以上述代码为例, com.example.Bookdemo.controller路径下的方法都是连接点

切点和连接点的关系

连接点是满⾜切点表达式的元素.

切点可以看做是保存了众多连接点的⼀个集合.

⽐如: 切点表达式:全体教师 连接点就是:张三,李四等各个⽼师

3.1.3 通知(Advice)

通知就是具体要做的⼯作,指哪些重复的逻辑,也就是共性功能(最终体现为⼀个⽅法)

 

3.1.4 切面(Aspect) 

 切⾯(Aspect)=切点(Pointcut)+通知(Advice)

通过切⾯就能够描述当前AOP程序需要针对于哪些⽅法,在什么时候执⾏什么样的操作

切⾯所在的类,我们⼀般称为切⾯类(被@Aspect注解标识的类)

3.2 通知类型

类型含义
@Around环绕通知,此注解标注的通知⽅法在⽬标⽅法前,后都被执⾏
@Before此注解标注的通知⽅法在⽬标⽅法前被执⾏
@After后置通知,此注解标注的通知⽅法在⽬标⽅法后被执⾏,⽆论是否有异常都会执⾏
@AfterReturning返回后通知,此注解标注的通知⽅法在⽬标⽅法后被执⾏,有异常不会执⾏
@AfterThrowing异常后通知,此注解标注的通知⽅法发⽣异常后执⾏

下面用代码看一下:

①正常:

@Component
@Aspect
@Slf4j
public class AspectDemo4 {

    @Before("execution(* com.example.aopdemo.controller.*.*(..))")
    public void doBefore(){
        log.info("执行AspectDemo before...");
    }

    @After("execution(* com.example.aopdemo.controller.*.*(..))")
    public void doAfter(){
        log.info("执行AspectDemo doAfter...");
    }
    @AfterReturning("execution(* com.example.aopdemo.controller.*.*(..))")
    public void doAfterReturning(){
        log.info("执行AspectDemo doAfterReturning...");
    }
    @AfterThrowing("execution(* com.example.aopdemo.controller.*.*(..))")
    public void doAfterThrowing(){
        log.info("执行AspectDemo doAfterThrowing...");
    }
    @Around("execution(* com.example.aopdemo.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("执行AspectDemo doAround 前...");
        Object result = joinPoint.proceed();
        log.info("执行AspectDemo doAround 后...");
        return result;
    }
}

测试代码:

@RestController
public class TestController {


    @RequestMapping("/t2")
    public String t2(){
        int a = 10/0;
        return "t2";
    }

}

 

程序正常运⾏的情况下, @AfterThrowing 标识的通知⽅法不会执⾏

②异常:

@ AfterReturning 标识的通知⽅法不会执⾏, @AfterThrowing 标识的通知⽅法执⾏了

通知中的环绕后的代码逻辑也不会在执⾏了 

3.3 @PointCut

上⾯代码存在⼀个问题,就是存在⼤量重复的切点表达式 

execution(* com.example.demo.controller.*.*(..)) , Spring提供了 表达式提取出来,需要⽤到时引⽤该切⼊点表达式即可.

@Aspect
@Component
@Slf4j
public class AspectDemo {
    @Pointcut("execution(* com.example.aopdemo.controller.*.*(..))")
    public void pt(){
    }

    @Before("pt()")
    public void doBefore(){
        log.info("执行AspectDemo before...");
    }

    @After("pt()")
    public void doAfter(){
        log.info("执行AspectDemo doAfter...");
    }
    @AfterReturning("pt()")
    public void doAfterReturning(){
        log.info("执行AspectDemo doAfterReturning...");
    }
    @AfterThrowing("pt()")
    public void doAfterThrowing(){
        log.info("执行AspectDemo doAfterThrowing...");
    }
    @Around("pt()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("执行AspectDemo doAround 前...");
        Object result = joinPoint.proceed();
        log.info("执行AspectDemo doAround 后...");
        return result;
    }
}

 

当切点定义使⽤private修饰时,仅能在当前切⾯类中使⽤,当其他切⾯类也要使⽤当前切点定义时,就需要把private改为public 

3.4 切⾯优先级@Order

当我们在⼀个项⽬中,定义了多个切⾯类时,并且这些切⾯类的多个切⼊点都匹配到了同⼀个⽬标⽅法. 当⽬标⽅法运⾏的时候,这些切⾯类中的通知⽅法都会执⾏,那么这⼏个通知⽅法的执⾏顺序是什么样的呢?

此时有AspectDemo1,AspectDemo2,AspectDemo3

经过验证可得,是按按照切⾯类的类名字⺟排序 

Spring 给我们提供了⼀个新的注解,来控制这些切⾯通知的执⾏顺序:@Order

使用如下:

运行:

使用@Order时,数字越小,优先级越高

3.5 切点表达式

切点表达式常⻅有两种表达⽅式

1. execution(... ) :根据⽅法的签名来匹配

2. @annotation(... ):根据注解匹配

3.5.1 execution表达式

语法:

execution(< 访问修饰符 > < 返回类型 > < 包名 . 类名 . ⽅法 ( ⽅法参数 )> < 异常 >)

其中,访问修饰符和异常可以省略

* :匹配任意字符,只匹配⼀个元素(返回类型,包,类名,⽅法或者⽅法参数)

.. :匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数

3.5.2  @annotation

execution表达式更适⽤有规则的,如果我们要匹配多个⽆规则的⽅法呢,

⽐如:TestController中的t1() 和HelloController中的h2()这两个⽅法.

这个时候我们使⽤execution这种切点表达式来描述就不是很⽅便了.

实现自定义注解:

  • 编写⾃定义注解
  • 使⽤ @annotation 表达式来描述切点
  • 在连接点的⽅法上添加⾃定义注解

3.5.2.1 自定义注解

创建一个注解类

 

@Target 标识了 Annotation 所修饰的对象范围,即该注解可以⽤在什么地⽅

@Retention 指Annotation被保留的时间⻓短,标明注解的⽣命周期

3.5.2.2 切面类

使⽤ @annotation 切点表达式定义切点,只对@MyAspect生效

@Aspect
@Component
@Slf4j
public class MyAspectDemo {
    @Before("@annotation(com.example.aopdemo.aspect.MyAspect)")
    public void doBefore(){
        log.info("执行MyAspectDemo before...");
    }
    @After("@annotation(com.example.aopdemo.aspect.MyAspect)")
    public void doAfter(){
        log.info("执行MyAspectDemo After...");
    }

}

 

3.5.2.3 添加自定义注解

   

 

测试一下:

t1:

t2:

 切面未执行