Spring AOP

目录

1 AOP概述

1.1 什么是AOP?

1.2 什么是Spring AOP?

2 Spring AOP快速入门

2.1 引入AOP依赖

2.2 编写AOP程序

3 Spring AOP详情

3.1 Spring AOP核心概念

3.1.1 切点(Pointcut)

3.1.2 连接点(Join Point)

3.1.3 通知(Advice)

3.1.4 切面(Aspect)

3.2 通知类型

3.3  @PointCut注解

3.4 切面优先级@Order

3.5 切点表达式

3.5.1 execution表达式

3.5.2  @annotation

4 Spring AOP原理 

4.1 代理模式

4.2 静态代理

4.3 动态代理

4.3.1 JDK动态代理

1 AOP概述

Spring框架的两大核心,第一大核心是IoC,第二大核心就是AOP

1.1 什么是AOP?

Aspect Oriented Programming(面向切面编程),所谓的切面就是指一类特定问题,所以AOP可以理解为面向特定方法编程,比如"登录校验"就是一类特定问题,登录拦截器就是对"登录校验"这类问题的统一处理,所以拦截器也是AOP的一种应用,AOP是一种思想,拦截器就是AOP思想的一种实现,Spring框架实现了这种思想,提供了拦截器技术的相关接口

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

1.2 什么是Spring AOP?

AOP是一种思想,它的实现方式有很多,有Spring AOP、AspectJ、CGLIB等,Spring AOP就是其中的一种实现方法

举个例子:目前有一个项目,在项目中开发了很多的业务功能

有一些业务的执行效率较低,耗时较长,需要对接口进行优化,此时就需要定位出执行耗时比较长的业务方法,在针对该业务方法来进行优化,那么如何定位呢?就需要统计当前项目中每一个业务的执行耗时,因此需要在业务方法运行前和运行后,记录下方法的开始时间和结束时间,两者之差就是这个方法的耗时

由于在一个项目中有很多的业务板块,每一个业务板块有很多的接口,一个接口又包含很多方法,不可能去记录每个业务方法的执行耗时,而AOP就可以在不修改原始方法的基础上,针对特定的方法进行功能的增强

2 Spring AOP快速入门

需求:统计图书系统中各个接口的执行方法

2.1 引入AOP依赖

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

2.2 编写AOP程序

记录Controller中每个方法的执行时间

@Slf4j
@Aspect
@Component
public class TimeAspect {
    /**
     * 记录方法耗时
     */
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        //记录方法开始执行时间
        long begin = System.currentTimeMillis();
        //执行目标方法
        Object result = joinPoint.proceed();
        //记录方法执行结束时间
        long end = System.currentTimeMillis();
        //记录方法耗时
        log.info(joinPoint + "执行耗时:" + (end - begin) + "ms");
        return result;
    }
}

 AOP面向切面编程的优势:

1 代码无入侵    2 减少了重复代码    3 提高开发效率    4 维护方便

3 Spring AOP详情

3.1 Spring AOP核心概念

3.1.1 切点(Pointcut)

切点的作用就是提供一组规则,通过表达式来描述,告诉程序对哪些方法来进行功能增强

表达式execution(* com.example.demo.controller.*.*(..))就是切点表达式

3.1.2 连接点(Join Point)

满足切点表达式规则的方法就是连接点,也就是AOP可以被控制的方法,例如com.example.demo.controller路径下的方法都是连接点

package com.example.demo.controller;


@Slf4j
@RequestMapping("/book")
@RestController
public class BookController {
   
    @RequestMapping("/getBookListByPage")
    public PageResult<BookInfo> getBookListByPage(PageRequest pageRequest) {
        
    }
    @RequestMapping("/addBook")
    public String addBook(BookInfo bookInfo) {
        
    }
}

上述BookController中的方法都是连接点

3.1.3 通知(Advice)

通知就是具体的工作,指定哪些重复的逻辑,例如记录业务方法的耗时时间就是通知

在AOP面向切面编程当中,把这部分重复的代码逻辑抽取出来单独定义,这部分代码就是通知的内容 

3.1.4 切面(Aspect)

切面(Aspect)= 切点(Pointcut) + 通知(Advice),切面既包含了通知逻辑的定义,也包含了连接点的定义

上述部分就属于切面,切面所在的类被称为切面类(@Aspect注解标识的类)

3.2 通知类型

Spring中AOP的通知类型:

@Around:环绕通知,此注解表示的通知方法在目标前后都被执行

@Before:前置通知,在目标方法前执行

@After:后置通知,在目标方法后执行,无论是否异常都会执行

@AfterReturning:返回后通知,在目标方法后执行,有异常不会执行

@AfterThrowing:异常后通知,在发生异常后执行

接下来通过代码测试一下

@Slf4j
@Aspect
@Component
public class AspectDemo {
    //前置通知
    @Before("execution(* com.example.demo.controller.*.*(..))")
    public void doBefore() {
        log.info("执⾏ Before ⽅法");
    }
    //后置通知
    @After("execution(* com.example.demo.controller.*.*(..))")
    public void doAfter() {
        log.info("执⾏ After ⽅法");
    }
    //返回后通知
    @AfterReturning("execution(* com.example.demo.controller.*.*(..))")
    public void doAfterReturning() {
        log.info("执⾏ AfterReturning ⽅法");
    }
    //抛出异常后通知
    @AfterThrowing("execution(* com.example.demo.controller.*.*(..))")
    public void doAfterThrowing() {
        log.info("执⾏ doAfterThrowing ⽅法");
    }
    //添加环绕通知
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("Around ⽅法开始执⾏");
        Object result = joinPoint.proceed();
        log.info("Around ⽅法结束执⾏");
        return result;
    }
}
@RestController
@RequestMapping("/test")
public class TextController {
    @RequestMapping("/t1")
    public String t1(){
        return "t1";
    }
    @RequestMapping("/t2")
    public boolean t2(){
        int a = 10/0;
        return true;
    }
}

先来测试一下t1,观察日志

可以看到,在程序正常运行的情况下,@ AfterThrowing标识的通知方法不会执行,@Around标识的通知方法包含两部分,一个"前置逻辑",一个"后置逻辑","前置逻辑"会在@Before标识的通知方法之前执行,"后置逻辑"会在@After标识的通知方法后面执行

测试t2,观察日志 

当程序发生异常情况时, @AfterReturning标识的通知方法不会执行,@AfterThrowing会执行,@Around环绕通知中,在方法调用时出现异常,不会执行"后置逻辑"

注意事项:

@Around环绕通知需要调用ProceedingJoinPoint.proceed()来让原始方法执行,其他方法不需要

@Around环绕通知方法的返回值,必须指定Object来接收原始方法的返回值,否则原始方法执行完毕时,不能获取到返回值

一个切面可以有多个切点

3.3  @PointCut注解

在上述代码中存在一个问题,有大量重复的切点表达式execution(*com.example.demo.controller.*.*(..)),Spring提供了@PointCut注解,把公共的切点表达式提取出来,其他地方需要时直接引用该切点表达式即可

上述代码可以修改为:

@Slf4j
@Aspect
@Component
public class AspectDemo {
    //定义公共切点表达式
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    private void pt() {}
    @Before("pt()")
    public void doBefore() {
        log.info("执⾏ Before ⽅法");
    }
    //后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执⾏ After ⽅法");
    }
    //返回后通知
    @AfterReturning("pt()")
    public void doAfterReturning() {
        log.info("执⾏ AfterReturning ⽅法");
    }
    //抛出异常后通知
    @AfterThrowing("pt()")
    public void doAfterThrowing() {
        log.info("执⾏ doAfterThrowing ⽅法");
    }
    //添加环绕通知
    @Around("pt()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("Around ⽅法开始执⾏");
        Object result = joinPoint.proceed();
        log.info("Around ⽅法结束执⾏");
        return result;
    }
}

在当前的代码中,定义了一个切点pt,当切点定义使用private修饰时,只能在当前切面类中使用,当其他切面类也要使用当前切点定义时,就需要把private改成public,其中引用的方式为:全限定类名 + 切点名称()

@Slf4j
@Aspect
@Component
public class AspectDemo2 {
    @Before("com.example.demo.aop.AspectDemo.pt()")
    public void doBefore() {
        log.info("执⾏ AspectDemo2的Before ⽅法");
    }
}

可以看出,当有多个切面时,切面的执行顺序时按照名称进行排序的,先执行AspectDemo里面的Before方法,再执行AspectDemo2里的Before方法

3.4 切面优先级@Order

当在一个项目中,定义了多个切面类时,并且这些切面类的多个切入点都匹配了同一个目标方法,通知方法的执行顺序是按照名称进行排序的

@Slf4j
@Aspect
@Component
public class AspectDemo {
    //定义公共切点表达式
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    public void pt() {}
    @Before("pt()")
    public void doBefore() {
        log.info("执⾏AspectDemo的Before方法");
    }
    //后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执⾏AspectDemo的After方法");
    }
}
@Slf4j
@Aspect
@Component
public class AspectDemo2 {
    @Before("com.example.demo.aop.AspectDemo.pt()")
    public void doBefore() {
        log.info("执⾏AspectDemo2的Before方法");
    }
    @After("com.example.demo.aop.AspectDemo.pt()")
    public void doAfter() {
        log.info("执⾏AspectDemo2的After方法");
    }
}

通过上述程序可以看出,存在多个切面类时,按照名称进行排序, 其中

@Before通知:字母排名靠前的先执行

@After通知:字母排名靠后的先执行

这种方式不方便管理,因为类名具有一定的意义,因此就可以用到Spring提供的注解 @Order来控制这些切面通知的执行顺序

@Slf4j
@Aspect
@Component
@Order(1)
public class AspectDemo2 {
    @Before("com.example.demo.aop.AspectDemo.pt()")
    public void doBefore() {
        log.info("执⾏AspectDemo2的Before方法");
    }
    @After("com.example.demo.aop.AspectDemo.pt()")
    public void doAfter() {
        log.info("执⾏AspectDemo2的After方法");
    }
}
@Slf4j
@Aspect
@Component
@Order(2)
public class AspectDemo {
    //定义公共切点表达式
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    public void pt() {}
    @Before("pt()")
    public void doBefore() {
        log.info("执⾏AspectDemo的Before方法");
    }
    //后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执⾏AspectDemo的After方法");
    }
}

 @Order注解标识的切面类,执行顺序如下:

@Before通知:数字越小先执行

@After通知:数字越大先执行

@Order控制切面的优先级,先执行优先级较高的切面,在执行优先级较低的切面,最终执行目标方法

3.5 切点表达式

切点表达式常见有两种方式

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

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

3.5.1 execution表达式

execution()是最常见的切点表达式,用来匹配方法,语法为:

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

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

 切点表达式示例:

TestController下的public修饰,返回类型为String,方法名为t1,无参方法

execution(public String com.example.demo.controller.TestController.t1())

 省略访问修饰符

execution(String com.example.demo.controller.TestController.t1())

 匹配所有返回类型

execution(* com.example.demo.controller.TestController.t1())

 匹配TestControlle下的所有无参方法

execution(* com.example.demo.controller.TestController.*())

 匹配TestController下的所有方法

execution(* com.example.demo.controller.TestController.*(..))

 匹配controller包下所有的类的所有方法

execution(* com.example.demo.controller.*.*(..))

 匹配所有包下的TestController

execution(* com..TestController.*(..))

 匹配com.example.demo包下,子孙包下的所有类的所有方法

execution(* com.example.demo..*(..))

3.5.2  @annotation

execution表达式更适合有规则的,如果我们要匹配对各无规则的方法,例如:TestController中的t1()和UserController中的u1()这两个方法,此时使用execution就很不方便,因此可以借助自定义注解的方式以及另一种切点表达式 @annotation来描述者一类的切点

实现步骤:

1.编写自定义注解

2.使用 @annotation表达式来描述切点

3.在连接点的方法上添加自定义注解

package com.example.demo.controller;

@RestController
@RequestMapping("/test")
public class TextController {
    @RequestMapping("/t1")
    public String t1(){
        return "t1";
    }
}
package com.example.demo.controller;

@RequestMapping("/user")
@RestController
public class UserController {
    @RequestMapping("/u1")
    public String u1() {
        return "u1";
    }
}

自定义注解@MyAspect

package com.example.demo.aop;

//定义这个注解只匹配方法,如果加在类上会报错
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {
}

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

@Slf4j
@Component
@Aspect
public class MyAspectDemo {
    //前置通知
    @Before("@annotation(com.example.demo.aop.MyAspect)")
    public void doBefore() {
        log.info("执行MyAspectDemo的before方法");
    }
    //后置通知
    @After("@annotation(com.example.demo.aop.MyAspect)")
    public void doAfter() {
        log.info("执行MyAspectDemo的after方法");
    }
}

在TextController中的t1()和UserController中的u1()方法中添加自定义注解@MyAspect

public class TextController {
    @MyAspect
    @RequestMapping("/t1")
    public String t1(){
        return "t1";
    }
}
@MyAspect
    @RequestMapping("/u1")
    public String u1() {
        return "u1";
    }

Spring AOP的实现方式(常见面试题) 

1.基于注解@Aspect

2.基于自定义注解

3.基于Spring API(通过xml配置的方式)

4.基于代理来实现

4 Spring AOP原理 

Spring AOP是基于动态代理来实现AOP的

4.1 代理模式

代理模式也叫委托模式

定义:为其他方法提供一种代理以控制对这个对象的访问,它的作用就是通过提供一个代理类,在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用

在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用

代理模式的主要角色

Subject:业务接口类,可以是抽象类或者接口

RealSubject:业务实现类,具体的业务执行,被代理的对象

Proxy:代理类,RealSubject的代理

代理模式分为静态代理动态代理

静态代理:由程序员创建代理类会特定工具自动生成源代码对其编辑,在程序运行前代理类的.class文件就已经存在

动态代理:在程序运行时,运用反射机制动态创建而成

4.2 静态代理

所谓的静态代理就是在程序运行前,代理类的.class文件就已经存在了,以房屋出租为例

1.定义接口(实现房东要做的事情,也是中介需要做的事情)

public interface HouseSubject {
    void rentHouse();
}

2. 实现接口类(房东出租房子)

public class RealHouseSubject implements HouseSubject{
    @Override
    public void rentHouse() {
        System.out.println("我是房东,我出租房子");
    }
}

3. 代理(中介,帮房东出租房子)

public class HouseProxy implements HouseSubject{
    private HouseSubject houseSubject;
    public HouseProxy(HouseSubject houseSubject) {
        this.houseSubject = houseSubject;
    }
    @Override
    public void rentHouse() {
        //开始代理
        System.out.println("我是中介,开始代理");
        //代理房东出租房子
        houseSubject.rentHouse();
        //代理结束
        System.out.println("我是中介,代理结束");
    }
}

4. 使用

public class Main {
    public static void main(String[] args) {
        HouseSubject subject = new RealHouseSubject();
        //创建代理类
        HouseSubject proxy = new HouseProxy(subject);
        //通过代理类访问目标方法
        proxy.rentHouse();
    }
}

从静态代理可以看出,在出租房屋之前,中介已经做好了相关的工作,就等租户来租房子了,静态代理有个缺点,由于代码都是写死的,对目标对象的每个方法的增强都是手动来完成的,非常的不灵活

4.3 动态代理

相比静态代理来说,动态代理更加灵活,不需要针对每个目标对象都创建一个代理对象,而是把这个创建代理对象的工作推迟到程序运行时由JVM来实现,在程序运行的时候,根据需要动态创建

4.3.1 JDK动态代理

定义JDK动态代理类

public class JDKInvocationHandle implements InvocationHandler {
    //目标对象即被代理的对象
    private Object target;
    
    public JDKInvocationHandle(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("我是中介,开始代理");
        //通过反射调用被代理的方法
        Object retVal = method.invoke(target,args);
        System.out.println("我是中介,代理结束");
        return retVal;
    }
}

创建一个代理对象并使用 

public class Main {
    public static void main(String[] args) {
        HouseSubject target = new RealHouseSubject();
        //创建一个代理类:通过被代理类、被代理实现的接口,方法调用来创建
        HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(
                target.getClass().getClassLoader(), 
                new Class[]{HouseSubject.class},
                new JDKInvocationHandle(target)
        );
        proxy.rentHouse();
    }
}

InvocationHandler接口是Java动态代理的关键接口之一,它定义了一个单一方法invoke(),用于处理被代理对象的方法调用

proxy:代理对象             

method:代理对象需要实现的方法,即其中需要重写的方法

args:method所对应方法的参数

总结:

1 AOP是一种思想,是对某一类事情的集中处理,Spring框架实现了AOP,称之为Spring AOP

2 Spring AOP常见的方式由两种:

1)基于注解@Aspect来实现                2)基于自定义注解来实现

1)Spring AOP是基于动态代理实现的

2)动态代理是基于JDK和CGLIB实现的
3)在Spring Boot 2.X开始,对于接口默认使用CGLIB代理

4)当设置spring.aop.proxy-target-class=false时,对于接口使用JDK代理,对于使用CGLIB代理

proxyTargetClass⽬标对象代理⽅式
false实现了接⼝jdk代理
false未实现接⼝(只有实现类)cglib代理
true实现了接⼝cglib代理
true未实现接⼝(只有实现类)cglib代理

  • 15
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值