本节目标
-
了解AOP的概念
-
学习Spring AOP的实现方式以及实现原理, 对代理模式有一定了解
1. AOP概述
学习完Spring的统一功能之后, 我们进入到AOP的学习. AOP是Spring框架的第二大核心(第一大核心是 IoC)
什么是AOP?
• Aspect Oriented Programming(面向切面编程)
什么是面向切面编程呢? 切面就是指某一类特定问题, 所以AOP也可以理解为面向特定方法编程
什么是面向特定方法编程呢? 比如上个章节学习的"登录校验", 就是一类特定问题. 登录校验拦截器, 就 是对"登录校验"这类问题的统一处理. 所以, 拦截器也是AOP的一种应用. AOP是一种思想, 拦截器是AOP 思想的一种实现. Spring框架实现了这种思想, 提供了拦截器技术的相关接口.
同样的, 统一数据返回格式和统一异常处理, 也是AOP思想的一种实现
简单来说:AOP是一种思想, 是对某一类事情的集中处理
什么是Spring AOP?
AOP是一种思想, 它的实现方法有很多, 有Spring AOP,也有AspectJ、CGLIB等.
Spring AOP是其中的一种实现方式
学会了统一功能之后, 是不是就学会了Spring AOP呢, 当然不是.
拦截器作用的维度是URL(一次请求和响应), @ControllerAdvice 应用场景主要是全局异常处理 (配合自定义异常效果更佳), 数据绑定, 数据预处理. AOP作用的维度更加细致(可以根据包、类、方法 名、参数等进行拦截), 能够实现更加复杂的业务逻辑
举个例子:
我们现在有一个项目,项目中开发了很多的业务功能

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

这种方法是可以解决问题的, 但一个项目中会包含很多业务模块, 每个业务模块又有很多接口, 一个接口 又包含很多方法, 如果我们要在每个业务方法中都记录方法的耗时, 对于程序员而言, 会增加很多的工作量.
2. Spring AOP快速入门
学习什么是AOP后, 我们先通过下面的程序体验下AOP的开发, 并掌握Spring中AOP的开发步骤
需求: 统计图书系统各个接口方法的执行时间.
2.1 引入AOP依赖
在pom.xml文件中添加配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2 编写AOP程序
记录Controller中每个方法的执行时间
java
@Slf4j
@Aspect
@Component
public class TimeAspect {
@Around("execution(* com.bit.book.springbookdemo..*.*(..))")
public Object timeRecord(ProceedingJoinPoint pjp) throws Throwable {
//1.记录开始时间
long start=System.currentTimeMillis();
//2.执行目标方法
Object proceed=pjp.proceed();
long end=System.currentTimeMillis();
log.info(pjp.getSignature().toString()+"耗时"+(end-start)+"ms");
return proceed;
}
}

对程序进行简单的讲解:
-
@Aspect: 标识这是一个切面类
-
@Around: 环绕通知, 在目标方法的前后都会被执行. 后面的表达式表示对哪些方法进行增强.
-
ProceedingJoinPoint.proceed() 让原始方法执行

我们通过AOP入门程序完成了业务接口执行耗时的统计
通过上面的程序, 我们也可以感受到AOP面向切面编程的一些优势:
• 代码无侵入: 不修改原始的业务方法, 就可以对原始的业务方法进行了功能的增强或者是功能的改变
• 减少了重复代码
• 提高开发效率
• 维护方便
3. Spring AOP 详解
下面我们再来详细学习AOP, 主要是以下几部分
• Spring AOP中涉及的核心概念
• Spring AOP通知类型
• 多个AOP程序的执行顺序
3.1 Spring AOP核心概念
3.1.1 切点(Pointcut)
切点(Pointcut), 也称之为"切入点"
Pointcut 的作用就是提供一组规则 (使用 AspectJ pointcut expression language 来描述), 告诉程序对哪些方法来进行功能增强

上面的表达式 execution(* com.example.demo.controller.*.*(..)) 就是切点表达式.
3.1.2 连接点(Join Point)
满足切点表达式规则的方法, 就是连接点. 也就是可以被AOP控制的方法
以入门程序举例, 所有 com.example.demo.controller 路径下的方法, 都是连接点
3.1.3 通知(Advice)
通知就是具体要做的工作, 指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
比如上述程序中记录业务方法的耗时时间, 就是通知

3.1.4 切面(Aspect)
切面(Aspect) = 切点(Pointcut) + 通知(Advice)

切面所在的类, 我们一般称为切面类(被@Aspect注解标识的类)
3.2 通知类型
上面我们讲了什么是通知, 接下来学习通知的类型. @Around 就是其中一种通知类型, 表示环绕通知.
Spring中AOP的通知类型有以下几种:
• @Around: 环绕通知, 此注解标注的通知方法在目标方法前, 后都被执行
• @Before: 前置通知, 此注解标注的通知方法在目标方法前被执行
• @After: 后置通知, 此注解标注的通知方法在目标方法后被执行, 无论是否有异常都会执行
• @AfterReturning: 返回后通知, 此注解标注的通知方法在目标方法后被执行, 有异常不会执行
• @AfterThrowing: 异常后通知, 此注解标注的通知方法发生异常后执行
java
@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;
}
}
写一些测试程序
java
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1() {
return "t1";
}
@RequestMapping("/t2")
public boolean t2() {
int a = 10 / 0;
return true;
}
}

程序正常运行的情况下, @AfterThrowing 标识的通知方法不会执行
从上图也可以看出来, @Around 标识的通知方法包含两部分, 一个"前置逻辑", 一个"后置逻辑".其 中"前置逻辑" 会先于 @Before 标识的通知方法执行, "后置逻辑" 会晚于 @After 标识的通知方法执行

- 异常时的情况

程序发生异常的情况下

• @AfterReturning 标识的通知方法不会执行, @AfterThrowing 标识的通知方法执行了
• @Around 环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了(因为 原始方法调用出异常了)
注意事项:
• @Around 环绕通知需要调用 ProceedingJoinPoint.proceed() 来让原始方法执行, 其他 通知不需要考虑目标方法执行.
• @Around 环绕通知方法的返回值, 必须指定为Object, 来接收原始方法的返回值, 否则原始方法执 行完毕, 是获取不到返回值的
3.3 @PointCut
上面代码存在一个问题, 就是存在大量重复的切点表达式 execution(* com.example.demo.controller.*.*(..)) , Spring提供了 @PointCut 注解, 把公共的切点 表达式提取出来, 需要用到时引用该切入点表达式即可
java
@Slf4j
@Aspect
@Component
public class AspectDemo {
//定义切点(公共的切点表达式)
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){};
//添加环绕通知
@Around("pt()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Around 方法开始执行");
Object result = joinPoint.proceed();
log.info("Around 方法结束执行");
return result;
}
//前置通知
@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 方法");
}
}
3.4 切面优先级 @Order
当我们在一个项目中, 定义了多个切面类时, 并且这些切面类的多个切入点都匹配到了同一个目标方法.
当目标方法运行的时候, 这些切面类中的通知方法都会执行, 那么这几个通知方法的执行顺序是什么样的呢?
我们还是通过程序来求证
定义多个切面类:
java
public class AspectDemo2 {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void doBefore() {
log.info("执行 AspectDemo2 -> Before 方法");
}
//后置通知
@After("pt()")
public void doAfter() {
log.info("执行 AspectDemo2 -> After 方法");
}
}
java
public class AspectDemo3 {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void doBefore() {
log.info("执行 AspectDemo3 -> Before 方法");
}
//后置通知
@After("pt()")
public void doAfter() {
log.info("执行 AspectDemo3 -> After 方法");
}
}
java
public class AspectDemo4 {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void doBefore() {
log.info("执行 AspectDemo4 -> Before 方法");
}
//后置通知
@After("pt()")
public void doAfter() {
log.info("执行 AspectDemo4 -> After 方法");
}
}

通过上述程序的运行结果, 可以看出:
存在多个切面类时, 默认按照切面类的类名字母排序
• @Before 通知:字母排名靠前的先执行
• @After 通知:字母排名靠前的后执行
但这种方式不方便管理, 我们的类名更多还是具备一定含义的
Spring 给我们提供了一个新的注解, 来控制这些切面通知的执行顺序: @Order

@Order 注解标识的切面类, 执行顺序如下:
• @Before 通知:数字越小先执行
• @After 通知:数字越大先执行
@Order 控制切面的优先级, 先执行优先级较高的切面, 再执行优先级较低的切面, 最终执行目标方法

3.5 切点表达式
上面的代码中, 我们一直在使用切点表达式来描述切点. 下面我们来介绍一下切点表达式的语法.
切点表达式常见有两种表达方式
-
execution(......):根据方法的签名来匹配
-
@annotation(......) :根据注解匹配
3.5.1 @annotation
execution表达式更适用有规则的, 如果我们要匹配多个无规则的方法呢, 比如:TestController中的t1() 和UserController中的u1()这两个方法
这个时候我们使用execution这种切点表达式来描述就不是很方便了
我们可以借助自定义注解的方式以及另一种切点表达式 @annotation 来描述这一类的切点
-
编写自定义注解
-
使用 @annotation 表达式来描述切点
-
在连接点的方法上添加自定义注解
准备测试代码:
java
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1() {
return "t1";
}
@RequestMapping("/t2")
public boolean t2() {
return true;
}
}
java
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/u1")
public String u1(){
return "u1";
}
@RequestMapping("/u2")
public String u2(){
return "u2";
}
}
3.5.1.1 自定义注解 @MyAspect
创建一个注解类(和创建Class文件一样的流程, 选择Annotation就可以了)

java
package com.bit.book.springbookdemo.aspect;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {
}
代码简单说明, 了解即可. 不做过多解释
-
@Target 标识了 Annotation 所修饰的对象范围, 即该注解可以用在什么地方
-
@Retention 指Annotation被保留的时间长短, 标明注解的生命周期
3.5.1.2 切面类
使用 @annotation 切点表达式定义切点, 只对 @MyAspect 生效
java
@Slf4j
@Component
@Aspect
public class MyAspectDemo {
@Before("@annotation(com.bit.book.springbookdemo.aspect.MyAspect)")
public void before() {
log.info("MyAspect -> before ...");
}
@After("@annotation(com.bit.book.springbookdemo.aspect.MyAspect)")
public void after() {
log.info("MyAspect -> after ...");
}
}
3.5.1.3 添加自定义注解
在TestController中的t1()和UserController中的u1()这两个方法上添加自定义注解 @MyAspect , 其 他方法不添加
java
@MyAspect
@RequestMapping("/t1")
public String t1() {
return "t1";
}
@MyAspect
@RequestMapping("/u1")
public String u1(){
return "u1";
}

4. Spring AOP 原理
上面我们主要学习了Spring AOP的应用, 接下来我们来学习Spring AOP的原理, 也就是Spring 是如何实现AOP的.
Spring AOP 是基于动态代理来实现AOP的, 咱们学习内容主要分以下两部分
-
代理模式
-
Spring AOP源码剖析
4.1 代理模式
代理模式, 也叫委托模式
**定义:**为其他对象提供一种代理以控制对这个对象的访问. 它的作用就是通过提供一个代理类, 让我们 在调用目标方法的时候, 不再是直接对目标方法进行调用, 而是通过代理类间接调用.
在某些情况下, 一个对象不适合或者不能直接引用另一个对象, 而代理对象可以在客户端和目标对象之间起到中介的作用

代理模式的主要角色
-
Subject: 业务接口类. 可以是抽象类或者接口(不一定有)
-
RealSubject: 业务实现类. 具体的业务执行, 也就是被代理对象.
-
Proxy: 代理类. RealSubject的代理

代理模式可以在不修改被代理对象的基础上, 通过扩展代理类, 进行一些功能的附加与增强.
根据代理的创建时期, 代理模式分为静态代理 和动态代理
• 静态代理: 由程序员创建代理类或特定工具自动生成源代码再对其编译, 在程序运行前代理类的 .class 文件就已经存在了.
• 动态代理: 在程序运行时, 运用反射机制动态创建而成
4.1.1 静态代理
静态代理: 在程序运行前, 代理类的 .class文件就已经存在了. (在出租房子之前, 中介已经做好了相关的工作, 就等租户来租房子了)
1. 定义接口
java
public interface HouseSubject {
void rentHouse();
}
2. 实现接口
java
public class RealHouseSubject implements HouseSubject{
@Override
public void rentHouse() {
System.out.println("我是房东, 我出租房子");
}
}
3. 代理
java
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. 使用
java
public class StaticMain {
public static void main(String[] args) {
HouseSubject subject = new RealHouseSubject();
//创建代理类
HouseProxy proxy = new HouseProxy(subject);
//通过代理类访问目标方法
proxy.rentHouse();
}
}
上面这个代理实现方式就是静态代理(仿佛啥也没干).
接下来新增需求: 中介又新增了其他业务: 代理房屋出售
1. 接口定义修改
java
public interface HouseSubject {
void rentHouse();
void saleHouse();
}
2. 接口实现修改
java
public class RealHouseSubject implements HouseSubject{
@Override
public void rentHouse() {
System.out.println("我是房东, 我出租房子");
}
@Override
public void saleHouse() {
System.out.println("我是房东, 我出售房子");
}
}
问题:
1.为啥代理类中实现的是接口,而不是实现类呢?
这是代理模式的精髓所在:面向接口编程,而不是面向实现类编程。
好处:
- 可以代理别的类
- 降低代码耦合,符合开闭原则(对扩展开放,对修改关闭)
3. 代理类修改
java
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("我是中介, 代理结束");
}
@Override
public void saleHouse() {
//开始代理
System.out.println("我是中介, 开始代理");
//代理房东出租房子
houseSubject.saleHouse();
//代理结束
System.out.println("我是中介, 代理结束");
}
}
从上述代码可以看出, 我们修改接口(Subject)和业务实现类(RealSubject)时, 还需要修改代理类 (Proxy)
同样的, 如果有新增接口(Subject)和业务实现类(RealSubject), 也需要对每一个业务实现类新增代理类 (Proxy)
既然代理的流程是一样的, 有没有一种办法, 让他们通过一个代理类来实现呢?
这就需要用到动态代理技术了
4.1.2 动态代理
相比于静态代理来说,动态代理更加灵活
我们不需要针对每个目标对象都单独创建一个代理对象, 而是把这个创建代理对象的工作推迟到程序运 行时由JVM来实现. 也就是说动态代理在程序运行时, 根据需要动态创建生成.
-
JDK动态代理
-
CGLIB动态代理
JDK动态代理
JDK 动态代理类实现步骤
-
定义一个接口及其实现类(静态代理中的 HouseSubject 和 RealHouseSubject )
-
自定义 InvocationHandler 并重写 invoke 方法,在 invoke 方法中我们会调用目标方 法(被代理类的方法)并自定义一些处理逻辑
-
通过 Proxy.newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h) 方法创建代理对象
定义JDK动态代理类
实现 InvocationHandler 接口
java
public class JDKInvocationHandler implements InvocationHandler {
//目标对象即就是被代理对象
private Object target;
public JDKInvocationHandler(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;
}
}
创建一个代理对象并使用
java
public class DynamicMain {
public static void main(String[] args) {
HouseSubject target= new RealHouseSubject();
//创建一个代理类:通过被代理类、被代理实现的接口、方法调用处理器来创建
HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class[]{HouseSubject.class},
new JDKInvocationHandler(target)
);
proxy.rentHouse();
}
}
CGLIB 动态代理类实现步骤
-
定义一个类(被代理类)
-
自定义 MethodInterceptor 并重写 intercept 方法, intercept 用于增强目标方 法,和 JDK 动态代理中的 invoke 方法类似
-
通过 Enhancer 类的 create()创建代理类
接下来看下实现:
添加依赖
java
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
实现MethodInterceptor接口
java
public class CGLIBInterceptor implements MethodInterceptor {
//目标对象, 即被代理对象
private Object target;
public CGLIBInterceptor(Object target){
this.target = target;
}
@Override
public Object intercept(Object o, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
// 代理增强内容
System.out.println("我是中介, 开始代理");
//通过反射调用被代理类的方法
Object retVal = methodProxy.invoke(target, objects);
//代理增强内容
System.out.println("我是中介, 代理结束");
return retVal;
}
}
创建代理类, 并使用
java
public class DynamicMain {
public static void main(String[] args) {
HouseSubject target= new RealHouseSubject();
HouseSubject proxy= (HouseSubject)
Enhancer.create(target.getClass(),new CGLIBInterceptor(target));
proxy.rentHouse();
}
}
总结
-
AOP是一种思想, 是对某一类事情的集中处理. Spring框架实现了AOP, 称之为SpringAOP
-
Spring AOP常见实现方式有两种: 1. 基于注解@Aspect来实现 2. 基于自定义注解来实现, 还有一些 更原始的方式,比如基于代理, 基于xml配置的方式, 但目标比较少见
-
Spring AOP 是基于动态代理实现的, 有两种方式: 1. 基本JDK动态代理实现 2. 基于CGLIB动态代理 实现. 运行时使用哪种方式与项目配置和代理的对象有关