前言
Hi 我是来自西部大嫖客杰西。
上篇文章我从代理模式的概念,慢慢引入了静态代理和动态代理,还讲了一些关于动态代理的分类及概念的特别说明。如果你没看到我上节的文章,我可以送一个传送门:
- ① Spring AOP 之代理模式
- ② Spring AOP 之 AOP 详解与基础用法
- ③ Spring AOP 之源码解析
理解了设计模式,那么我们开始学习一下看 Spring
团队利用了代理模式糅合到 Spring AOP
当中去的。
AOP 介绍
什么是 AOP ? AOP 其实跟 OOP (面向对象编程)一样,都是一种编程思想。它的全称是 Aspect-oriented Programming
,也就是面向切面编程。
在应用上,AOP 其实是 OOP 的一种补充。还记得,OOP 为什么这么流行?因为 OOP 的出现是可以让人们将复杂的问题抽象化,简单化;同时,OOP 的三大特性如封装/继承/多态,可以封装细节内容,使编程更加简单,更符合人类的思维。这些选项就已经优于古老的面向过程的思想(但是说面向过程并不优越,只是说明 OOP 更加受大众欢迎)。
但是为什么 AOP 是一个补充呢?因为虽然 OOP 已经是理想的编程模型,但是它任然有不足之处。在实际编程中,我们常常会遇到一些普通,通用但又是不可缺少的功能(代码)。
那怎么办?我们只能将涉及多业务流程的通用功能抽取并单独封装,形成独立的切面,在合适的时机将这些切面横向切入到业务流程指定的位置中。例如说,我们常常使用的日志功能,事务等等。
总的来说,AOP 其实是将散布于不同业务但功能通用的代码从业务逻辑中抽取出来,封装成独立的模块(切面)。同时,当业务逻辑中执行的过程中,AOP 把切面和关注点切入业务流程。
AOP 应用场景
切面应用场景大多数都会想到的是日志或者事务。在单体应用中使用切面实现日志是个不错的选择,但是分布式后就不一定了,当然这是题外话。
下面我说些常用的 AOP 场景。
- 缓存
- 异常处理,可以用来捕捉异常
- 调试,性能优化,计算某个方法执行的时间进行精准优化
- 持久化
- 事务,这个我们经常使用的 @Transactional 注解
- 日志
- 记录追踪,可以记录执行了哪些功能(本质上和日志差不多)
AOP 术语
想了解清楚 Spring AOP,那么了解它们的组成非常重要。这对接下来的源码分析也有一定的帮助。
名称 | 说明 |
---|---|
Aspect |
一个关注点的模块化。因为一个关注点可以横切多个对象,所以我们可以将它进行模块管理。事务管理(@Transactinal )就是一个很好的例子。在 Spring AOP 中,切面可以基于模式或者基于 @Aspect 注解的方式实现。 |
Join Point |
程序执行过程中的一个点,如方法的执行或异常的处理。在 Spring AOP 中,连接点总是表示方法执行。 |
Advice |
在切面的某个特定的连接点上的执行的动作 |
Introduction |
代表类型声明其他方法或字段。Spring AOP 允许您将新的接口(和相应的实现)引入任何被建议的对象。例如,您可以使用一个介绍来让一个 bean 实现一个 IsModified 接口,以简化缓存。 |
Target Object |
即将被代理的目标对象。 |
AOP proxy |
为了实现 Aspect 契约(通知方法执行等)而由 AOP框架创建的对象。在Spring框架中,AOP代理是JDK动态代理或CGLIB代理。 |
Weave |
将 aspect 与其他应用程序类型或对象链接以创建 advised 对象。 |
关于 AOP 在 Spring 中的应用
我们都知道了,Spring AOP
基于动态代理的 AOP 框架。其中动态代理包含了两种模式:JDK Dynamic Proxy
和 Cglib Proxy
。
而 AspectJ
属于静态代理,因为它实际上是在编译期增强,与动态代理的编织时期不同,所以 Spring 虽然集成了 AspectJ
,但 Spring
实际上仅仅用了 Aspect
提供的用于切入点解析和匹配的库来解释与 AspectJ5
相同的注解。但 AOP
运行时仍然纯 Spring AOP
并且不依赖 AspectJ
编译器或编织器。
但是实际上在 Spring
也可以使用原生的 AspectJ
(文末有链接传送门)。 如果您的拦截需要包括目标类中的方法调用甚至构造函数,那么考虑使用 Spring
驱动的本机 AspectJ
编织,而不是 Spring
的基于代理的 AOP
框架。这构成了具有不同特征的 AOP
使用的不同模式,所以在做决定之前一定要熟悉编织。
举个栗子
我决定从一个例子开始,讲解 Spring AOP 在 Spring Boot 的常用实践。下面是步骤说明:
- 初始化 Spring Boot
- 引入依赖
- 编写 Controller
- 编写切面
- 测试
初始化 Spring Boot
初始化方法我一般用以下两种
- 通过 start.spring.io 自定义项目,然后下载/导入。
- 通过 IDEA 来新建
Spring Boot
项目。
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 因为我是通过
web
进行测试的,所以需要引入web
依赖。 - 因为需要切面的依赖,所以引入了
aop
。
编写 Controller
在 demoApplication.java
写入代码
@SpringBootApplication
@RestController
public class DemoV2020012901Application {
public static void main(String[] args) {
SpringApplication.run(DemoV2020012901Application.class, args);
}
@RequestMapping("helloworld")
public String helloworld() {
System.out.println("hello world");
return "helloworld";
}
}
编写切面
添加文件夹 aspect
,建立一个切面 DemoAspect.java
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/*
@Component 注解是必须加的,因为 Spring 需要检测到这个实体类;
仅仅一个 @Aspect 不足以让 Spring 检测得到。
(XML 模式的话我们一般是用了 <aop:aspectj-autoproxy/> 让 Spring 检测的)
*/
@Aspect
@Component
public class DemoAspect {
@Pointcut("execution(* helloworld(..))") // 切点表达式,属于 AspectJ 5 标准表达式
private void pointcut() {} // 切点签名
@Before("pointcut()")
public void before() {
System.out.println("before");
}
}
启动测试
这时候我们就可以启动应用测试了。测试地址访问:http://localhost:8080/helloworld
控制台会输出
before
hello world
AspectJ 切入点指示器
在上面的例子当中,切点表达式我使用的是 execution
。其实 Spring
还提供了许多切入点指示器,与强大的 AspectJ
的切点函数相匹配。
切入点类别 |
名称 |
入参 |
说明 |
---|---|---|---|
方法 | execution |
方法匹配模式串 | 匹配满足某一模式的所有目标类方法的连接点 |
方法 | @annotation |
方法注解类名 | 表示标注了特定注解的目标类方法连接点 |
方法入参 | args |
类名 | 通过判别目标类方法运行时入参对象的类型定义指定连接点。如 args(com.jc.Student) 表示所有有且仅有一个类型匹配于 Student 入参方法。 |
方法入参 | @args |
类型注解类名 | 通过判断目标类方法运行时入参对象的类是否标准特定注解来指定连接点。如 @args(com.jc.Test) 表示任何这样的一个目标方法,它有一个入参对象类标注了 @Test 注解。 |
目标类 | within |
类名匹配串 | 表示特定领域下所有的连接点。如 within(com.jc.service.*) 是指匹配 service 包下的所有连接点,即所有类的所有方法。 |
目标类 | target |
类名 | 目标类按类型匹配于指定类,则目标类的所有连接点匹配这个切点。如 @target(com.jc.Student) 定义的切点,Student 及 Student 实现类 MaleStudent 中的所有连接点都匹配切点 |
目标类 | @within |
类名注解类名 | 假如目标类按照类型匹配于某个类A,且类A标注了特定的注解,则目标类的所有连接点匹配这个切点。例如 @within(com.jc.Test) ,假设 Student 类标注了 @Test 注解,则 Student 及实现累 MaleStudent 的所有连接点都匹配这个切点 |
目标类 | @target |
类名注解类名 | 假如目标类标注了特定注解,则目标类的所有连接点都匹配该切点。如 @target(com.jc.Test) 。如果 Student 类标注了 @Test ,则 Student 的所有连接点都匹配这个切点。 |
代理类 | this |
类名 | 代理类按类型匹配于指定类,则被代理的目标类的所有连接点都匹配该切点 |
各种指示器都可以试一下,但是我们最常用的是 execution
。
Advice 的类型
Advice
的类型有五种,能满足大部分的企业应用需求,而且它们也非常好区分。以下就是它的类型:
名字 |
说明 |
---|---|
Before advice |
在连接点之前运行的通知。 Before advice 不能阻止连接点执行,除非代码抛出异常。 |
AfterReturning advice |
在连接点正常完成后运行的通知(例如,如果一个方法没有抛出异常返回)。 |
After throwing advice |
如果方法通过抛出异常退出,则执行通知。 |
After(finally) advice |
不管连接点以何种方式退出(正常或异常返回),都要执行的通知 |
Around advice |
围绕连接点(如方法调用)的通知。这是最强大的advice。Around通知可以在方法调用前后执行自定义行为。它还负责选择是继续到连接点,还是通过返回它自己的返回值或抛出异常来简化建议的方法执行。 |
❤️ 下面是不同类型的用法示例:
After Returning advice
/* 1. @AfterReturning 有四个参数:value / pointcut / returning / argNames
2. 下面的 pointcut 填的也是切面表达式,如果与 value 同时存在,则会覆盖 value
3. retVal 指的是方法返回值的对象
*/
@AfterReturning(value = "pointcut()", returning = "retVal")
public void afterReturning(Object retVal) {
System.out.println(retVal);
}
After throwing advice
/*
1. @AfterThrowing 有四个参数:value / pointcut / throwing / argNames
2. 使用 throwing 参数,这样可以限定异常才执行 advice。如果没设置则抛出的所有异常都会执行 advice
*/
@AfterThrowing(pointcut = "pointcut()")
public void AfterThrowing() {
System.out.println("AfterThrowing");
}
After(finally) advice
/*
1. @AfterThrowing 有二个参数:value / argNames
*/
@After(value = "pointcut()")
public void AfterThrowing() {
System.out.println("AfterThrowing");
}
Around advice
/*
1. @AfterThrowing 有二个参数:value / argNames
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
❤️
关于 Advice
的使用选型,官方建议使用最特定的通知类型可以提供更简单的编程模型,减少出错的可能性。例如,如果只需要用方法的返回值更新缓存,则最好实现 After return
通知,而不是 Around
通知,尽管 Around
通知可以完成相同的工作。
传送门
⭐️ 关于是选择 Spring AOP 还是 FULL AspectJ
?官网文档告诉你: Spring AOP or Full AspectJ?
⭐️ 要想完全使用 AspectJ
的编织器,可以参考官方文档: Using AspectJ with Spring Applications
⭐️ 更加了解 Advice?官网文档告诉你: Declaring Advice
⭐️ 当然,假设你还在使用原生的 Spring XML 配置方式怎么办?一个例子带你飞: Schema-based AOP Support
⭐️ 想了解 Spring 基于代理是怎么实现 AOP 框架?想看官方是怎么讲解的: Proxying Mechanisms!++下文我会讲关于 AOP 在 Spring Boot 的源码解析哟++。