您的位置:首页 > 娱乐 > 八卦 > Spring AOP

Spring AOP

2024/10/6 12:35:51 来源:https://blog.csdn.net/2302_82270778/article/details/139006140  浏览:    关键词: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{@Overridepublic void rentHouse() {System.out.println("我是房东,我出租房子");}
}

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

public class HouseProxy implements HouseSubject{private HouseSubject houseSubject;public HouseProxy(HouseSubject houseSubject) {this.houseSubject = houseSubject;}@Overridepublic 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;}@Overridepublic 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代理

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com