一、AOP 简介
在学习 Spring 时,我们知道 Spring 有两个核心概念:IOC/DI 和 AOP。在系统学习了 IOC/DI 之后,我们来深入学习另一个核心内容------AOP。
AOP 的核心定义是:AOP 是在不改变原有代码的前提下对其进行增强,本文将围绕这句话展开,带大家学习 AOP 核心概念与作用。
1.1 什么是 AOP?
- AOP (Aspect Oriented Programming):面向切面编程,是一种编程范式,用于指导开发者如何组织程序结构。
- OOP (Object Oriented Programming):面向对象编程。
OOP 是我们熟悉的编程思想,而 AOP 同样是一种编程思想,二者是不同的编程范式,解决不同维度的问题。
1.2 AOP 作用
AOP 的核心作用是:在不惊动原始设计(代码)的基础上,为其进行功能增强 ,底层通过代理模式实现。
1.3 AOP 核心概念
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
// 记录程序当前执行时间(开始时间)
Long startTime = System.currentTimeMillis();
// 业务执行万次
for (int i = 0;i<10000;i++) {
System.out.println("book dao save ...");
}
// 记录程序当前执行时间(结束时间)
Long endTime = System.currentTimeMillis();
// 计算时间差
Long totalTime = endTime-startTime;
// 输出信息
System.out.println("执行万次消耗时间:" + totalTime + "ms");
}
public void update(){
System.out.println("book dao update ...");
}
public void delete(){
System.out.println("book dao delete ...");
}
public void select(){
System.out.println("book dao select ...");
}
}
在这个案例中,save() 方法有万次执行耗时统计,而 update()/delete()/select() 没有。我们希望在不修改 update()/delete() 代码的前提下,给它们也加上耗时统计,这就是 AOP 的典型应用场景。
我们来拆解 AOP 核心概念:
| 概念 | 定义 | Spring AOP 中的理解 |
|---|---|---|
| 连接点 (JoinPoint) | 程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等 | 理解为方法的执行 ,案例中 save()/update()/delete()/select() 都是连接点 |
| 切入点 (Pointcut) | 匹配连接点的式子 | 用于描述「需要增强的方法」,一个切入点可以匹配一个或多个方法;切入点是连接点的子集,只有被增强的连接点才是切入点 |
| 通知 (Advice) | 在切入点处执行的操作,也就是共性功能 | 案例中「统计万次执行耗时」就是通知,在 Spring AOP 中以方法的形式呈现 |
| 通知类 | 定义通知的类 | 存放通知方法的类 |
| 切面 (Aspect) | 描述通知与切入点的对应关系 | 将通知绑定到切入点,告诉 Spring「哪个通知要增强哪个方法」 |
二、AOP 入门案例
2.1 需求分析
我们简化需求:使用 Spring AOP 注解方式,在方法执行前打印当前系统时间 ,在不修改 update() 方法的前提下,为其添加打印时间的功能。
2.2 思路分析
- 导入依赖(pom.xml)
- 制作连接点(原始操作,Dao 接口与实现类)
- 制作共性功能(通知类与通知)
- 定义切入点
- 绑定切入点与通知关系(切面)
- 将通知类配给容器并标识其为切面类
- 开启注解格式 AOP 功能
- 运行程序
2.3 环境准备
-
创建 Maven 项目
-
pom.xml 添加 Spring 依赖
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency> </dependencies> -
添加 BookDao 接口与 BookDaoImpl 实现类
public interface BookDao {
public void save();
public void update();
}@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("book dao save ...");
}
public void update(){
System.out.println("book dao update ...");
}
} -
创建 Spring 配置类
@Configuration
@ComponentScan("com.itheima")
public class SpringConfig {
} -
编写 App 运行类
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.save();
}
}
2.4 AOP 实现步骤
步骤1:添加依赖(已完成)
步骤2:定义接口与实现类(已完成)
步骤3-6:定义通知、切入点、切面并标识切面类(整合代码)
// 让通知类被 Spring 容器扫描并管理
@Component
// 标识当前类为 AOP 切面类,用于承载通知和切入点
@Aspect
public class MyAdvice {
// 定义切入点:匹配 com.itheima.dao.BookDao 接口的 update() 方法
// 切入点依托一个无实际业务意义的私有方法 pt(),仅用于承载 @Pointcut 注解
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
// 绑定切入点 pt(),使用 @Before 注解指定:该通知在切入点方法【执行前】运行
// 通知方法就是抽取的共性功能:打印当前系统时间
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
步骤7:开启注解格式 AOP 功能
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy // 开启注解格式 AOP 功能,支持 @Aspect、@Pointcut 等注解解析
public class SpringConfig {
}
步骤8:运行程序
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}
运行结果:
1618998676535
book dao update ...
2.5 核心注解详解
| 注解 | 类型 | 位置 | 作用 |
|---|---|---|---|
@EnableAspectJAutoProxy |
配置注解 | 配置类上方 | 开启注解格式 AOP 功能 |
@Aspect |
类注解 | 切面类定义上方 | 设置当前类为 AOP 切面类 |
@Pointcut |
方法注解 | 切入点方法定义上方 | 设置切入点方法,value 为切入点表达式 |
@Before |
方法注解 | 通知方法定义上方 | 设置通知在切入点方法执行前运行 |
三、AOP 工作流程
AOP 基于 Spring 容器管理的 Bean 做增强,完整工作流程从 Spring 加载 Bean 开始:
3.1 AOP 工作流程
流程1:Spring 容器启动
容器启动时,会加载所有需要被增强的类(如 BookDaoImpl)和通知类(如 MyAdvice),此时 Bean 对象尚未创建完成。
流程2:读取所有切面配置中的切入点
@Component
@Aspect
public class MyAdvice {
// 未被使用的切入点,不会被读取
@Pointcut("execution(void com.itheima.dao.BookDao.save())")
private void ptx(){}
// 被使用的切入点,会被读取
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
流程3:初始化 Bean
- 匹配失败 :创建原始对象,无需增强,直接调用原始方法。
- 匹配成功 :创建原始对象 + 代理对象,最终运行的是代理对象的方法。
流程4:获取 Bean 执行方法
- 获取的 Bean 是原始对象:直接调用方法执行。
- 获取的 Bean 是代理对象:运行原始方法 + 增强的通知。
3.2 验证:容器中是原始对象还是代理对象
我们通过代码验证结论:方法被增强 → 容器中是代理对象;方法不被增强 → 容器中是原始对象本身。
验证步骤 1:不增强时(切入点匹配失败)
修改 MyAdvice 的切入点,让 update() 方法不被匹配:
@Component
@Aspect
public class MyAdvice {
// 切入点改为 update1(),与 BookDao 的 update() 不匹配
@Pointcut("execution(void com.itheima.dao.BookDao.update1())")
private void pt(){}
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
修改 App 类,打印对象类型:
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
System.out.println(bookDao);
System.out.println(bookDao.getClass());
}
}
运行结果:
com.itheima.dao.impl.BookDaoImpl@279fedbd
class com.itheima.dao.impl.BookDaoImpl
此时容器中是原始对象本身。
验证步骤 2:增强时(切入点匹配成功)
修改 MyAdvice 的切入点,让 update() 方法被匹配:
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
运行结果:
com.itheima.dao.impl.BookDaoImpl@4e50c791
class com.sun.proxy.$Proxy19
此时容器中是代理对象,验证了我们的结论。
3.3 核心总结
- Spring AOP 的本质是代理模式,底层通过动态代理实现。
- 只有被切入点匹配到的方法,才会被创建代理对象并增强;未匹配的方法直接使用原始对象。
- AOP 实现了「无侵入式增强」,在不修改原始业务代码的前提下,灵活添加共性功能。