前言

Spring 有两大核心:一个是前几天我们重点攻克的 IoC/DI,另一个就是今天要深入学习的 AOP(面向切面编程)

还记得那句话吗?“AOP 是在不改变原有代码的前提下对其进行功能增强”。听起来很神奇对吧?今天我们就来揭开它神秘的面纱,还会学到 AOP 最重要的实际应用——声明式事务管理。掌握了事务,你的程序才能真正做到数据安全可靠。

💬 课前唠一唠:你有没有遇到过这种场景——想在每个方法执行前后都加个日志或计时,结果发现要改几十个地方?今天学完 AOP,你可以用几行代码就搞定这件事。你最想用 AOP 解决什么重复代码?评论区许个愿,说不定今天的案例就能帮你实现。


一、AOP 简介

1.1 什么是 AOP?

  • AOP(Aspect Oriented Programming)面向切面编程,是一种编程范式,指导开发者如何组织程序结构。
  • 我们熟悉的 OOP(面向对象编程)是另一种编程范式,两者互为补充。

1.2 AOP 的作用

在不惊动原始设计的基础上,为方法进行功能增强。

说白了就是:原来有一段代码已经写好了,现在想给它加点功能(比如打印日志、统计时间),但又不想改原有代码,AOP 就是干这个的。

💡 本质:Spring AOP 底层采用的是代理模式,后面我们会验证。

1.3 AOP 核心概念

我们来通过一个场景理解几个关键术语:

假设 BookDaoImplsave()update()delete()select() 四个方法。我们想给 updatedelete 增加“计算万次执行时间”的功能,但不改原代码。

概念 解释 举例
连接点(JoinPoint) 程序执行过程中能插入增强的任意位置 BookDaoImpl 中的所有方法
切入点(Pointcut) 真正需要增强的方法(匹配连接点的式子) update()delete()
通知(Advice) 抽取出来的共性功能(要增强的内容) “计算万次执行时间”的方法
通知类 定义通知的类 MyAdvice
切面(Aspect) 描述通知与切入点的对应关系 “在 update()delete() 上应用计时的通知”

🧠 一句话记忆:连接点是“地点”,切入点是“我选中的地点”,通知是“要干的事”,切面是“在哪里干什么”。


二、AOP 入门案例

需求:在方法执行前打印当前系统时间,不改原代码。

2.1 环境准备

创建 Maven 项目,添加 spring-context 依赖,创建 BookDaoBookDaoImplSpringConfig 配置类和 App 运行类。项目结构如下:

src/main/java/com/itheima
├── config/SpringConfig
├── dao/BookDao (接口)
├── dao/impl/BookDaoImpl (实现类)
└── App (运行类)

2.2 AOP 实现步骤

步骤 1:导入 AOP 依赖

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
</dependency>

说明:spring-context 已包含 spring-aop,这里只需导入 AspectJ 的织入包。Spring 整合 AspectJ 是目前最常用的 AOP 开发方式。

步骤 2:定义通知类与通知

@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());
    }
}

步骤 3:在配置类上开启 AOP 注解功能

@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy     // 开启注解格式 AOP 功能
public class SpringConfig { }

📝 关键注解速查

  • @EnableAspectJAutoProxy:开启 AOP 注解支持
  • @Aspect:声明切面类
  • @Pointcut:定义切入点
  • @Before:前置通知

三、AOP 工作流程

3.1 工作流程四步走

  1. Spring 容器启动:加载需要增强的类和通知类(此时 bean 还未创建)
  2. 读取切入点配置:只读取被实际使用的切入点(没被通知绑定的切入点忽略)
  3. 初始化 bean,匹配切入点
    • 匹配失败 → 创建原始对象
    • 匹配成功 → 创建代理对象(Proxy)
  4. 获取 bean 执行方法
    • 拿到的原始对象 → 直接调用
    • 拿到的代理对象 → 运行代理逻辑,在原始方法前后插入增强

3.2 验证:容器中的对象是代理还是原始?

BookDao bookDao = ctx.getBean(BookDao.class);
System.out.println(bookDao.getClass());  // 增强后打印的是代理类,未增强是原始类

⚠️ 注意:不要直接用 System.out.println(bookDao),因为 toString() 被重写过,看不出区别。用 getClass() 才能看到真实类型。

3.3 新增核心概念

概念 解释
目标对象(Target) 被代理的原始对象
代理(Proxy) 对目标对象增强后生成的对象,包含原始方法 + 通知逻辑

🔄 总结:Spring AOP 的本质 = 代理模式。匹配切入点的方法会生成代理对象,不匹配的就是原始对象。


四、AOP 配置管理

4.1 切入点表达式

4.1.1 语法格式

标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)

execution(public User com.itheima.service.UserService.findById(int))
//        ↑访问修饰符 ↑返回值  ↑包名       ↑类名       ↑方法名  ↑参数
  • 访问修饰符、异常名可以省略
  • 写在接口上和写在实现类上都能匹配到(调用接口最终走的还是实现类)
4.1.2 通配符
符号 含义 示例
* 单个独立的任意符号 execution(* com.*.service.*.find*(*))
.. 多个连续的任意符号 execution(* com..service.*.*(..))
+ 匹配子类类型 *Service+(用得少,了解即可)
4.1.3 书写技巧
  • 描述接口,不描述实现类(避免紧耦合)
  • 访问修饰符通常省略(接口方法都是 public
  • 查询方法返回值用 *,增删改用精准类型
  • 包名尽量不用 ..,用 * 做单级匹配
  • 接口名用 *Service 表示业务层模块
  • 方法名保留动词(getfind),名词用 *

🎯 实战套路:业务层全部方法 → execution(* com.itheima.service.*Service.*(..))

4.2 AOP 通知类型

共 5 种通知类型,下面通过一张图理解它们的位置:

方法执行流程:
                    ┌─ 后置通知(After):成功/异常都执行 ─┐
前置通知(Before)→ → 原始方法执行 → 返回后通知(AfterReturning):正常返回才执行
                    └─ 异常后通知(AfterThrowing):抛异常才执行 ─┘
环绕通知(Around):= 前置 + 后置,能完全控制方法执行
通知类型 注解 执行时机
前置通知 @Before 方法执行前
后置通知 @After 方法执行后(无论是否异常)
返回后通知 @AfterReturning 正常返回后
异常后通知 @AfterThrowing 抛出异常后
环绕通知 @Around 前后都可以,最强大

4.3 环绕通知详解(重点)

环绕通知能实现其他所有通知类型的功能,是实际开发中最常用的。

@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("around before advice ...");   // 前置部分
    Object ret = pjp.proceed();                       // 调用原始方法
    System.out.println("around after advice ...");    // 后置部分
    return ret;
}

环绕通知注意事项(重要!)

  1. 必须依赖形参 ProceedingJoinPoint,否则无法调用原始方法
  2. 如果不调用 pjp.proceed(),原始方法根本不会执行
  3. 如果原始方法有返回值,通知方法最好返回 Object,并把 pjp.proceed() 的返回值 return 出去
  4. 原始方法无返回值时,通知方法可设为 voidObject
  5. 必须处理 Throwable 异常

⚠️ 坑点pjp.proceed() 有两个重载版本。无参版本会自动传入原始参数;有参版本 pjp.proceed(args) 可以修改参数后传入。后面案例会用到这个特性。

4.4 AOP 通知获取数据

4.4.1 获取参数
  • 非环绕通知:在方法签名中加 JoinPoint 参数

    @Before("pt()")
    public void before(JoinPoint jp) {
        Object[] args = jp.getArgs();
        System.out.println(Arrays.toString(args));
    }
    
  • 环绕通知:用 ProceedingJoinPoint(它是 JoinPoint 的子类)

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();  // 获取参数
        args[0] = 666;                  // 可以修改参数
        return pjp.proceed(args);       // 传入修改后的参数
    }
    
4.4.2 获取返回值
  • 环绕通知:直接接收 pjp.proceed() 的返回值
  • 返回后通知:用 returning 属性
    @AfterReturning(value = "pt()", returning = "ret")
    public void afterReturning(Object ret) {
        System.out.println("afterReturning advice ..." + ret);
    }
    
4.4.3 获取异常
  • 环绕通知:用 try-catch 捕获
  • 抛出异常后通知:用 throwing 属性
    @AfterThrowing(value = "pt()", throwing = "t")
    public void afterThrowing(Throwable t) {
        System.out.println("afterThrowing advice ..." + t);
    }
    

五、AOP 总结

概念 说明
AOP 作用 不惊动原代码,为方法增强功能(无侵入式)
本质 代理模式(生成代理对象)
切入点表达式中最实用写法 execution(* com.xxx.service.*Service.*(..))
最常用通知 环绕通知 @Around
环绕通知核心 ProceedingJoinPoint.proceed() 调用原始方法
可获取数据 参数(getArgs())、返回值(ret)、异常(try-catch

六、AOP 事务管理(重点)

事务是 AOP 最重要的实际应用场景。Spring 通过 AOP 实现了声明式事务管理,让我们只需要一个注解就能搞定复杂的数据库事务。

6.1 为什么需要事务?转账案例分析

场景:Tom 给 Jerry 转账 100 元。

  • Tom 账户减 100
  • Jerry 账户加 100

两个操作必须同时成功或同时失败。如果减钱成功、加钱失败,100 块就凭空消失了。

问题:数据层每个操作都有自己独立的事务,业务层的 transfer 方法没有事务,出现异常时无法统一回滚。

解决方案:用 Spring 事务管理,让 transfer 方法开启一个大事务,把减钱和加钱两个操作纳入同一个事务中。

6.2 Spring 事务管理三步走

步骤 1:在需要事务的方法上加 @Transactional

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;

    @Transactional
    public void transfer(String out, String in, Double money) {
        accountDao.outMoney(out, money);
        // int i = 1 / 0;   // 出现异常,事务会自动回滚
        accountDao.inMoney(in, money);
    }
}

步骤 2:配置事务管理器(JdbcConfig 类中)

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    DataSourceTransactionManager manager = new DataSourceTransactionManager();
    manager.setDataSource(dataSource);
    return manager;
}

因为 MyBatis 底层用的是 JDBC,所以这里用 DataSourceTransactionManager

步骤 3:在配置类上开启事务注解

@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class, MybatisConfig.class})
@EnableTransactionManagement    // 开启注解式事务
public class SpringConfig { }

📝 关键注解速查

  • @Transactional:声明方法需要事务管理
  • @EnableTransactionManagement:开启事务注解支持

6.3 事务角色

角色 说明 对应
事务管理员 发起事务的一方 业务层开启事务的方法(如 transfer
事务协调员 加入事务的一方 数据层方法(如 outMoneyinMoney

开启 Spring 事务后,协调员的事务会加入到管理员的事务中,形成一个整体。任何一个环节出异常,整个事务都会回滚。

6.4 事务属性

@Transactional 有丰富的属性可配置:

属性 作用 示例
readOnly 只读事务(查询用 true,增删改用 false @Transactional(readOnly = true)
timeout 超时时间(秒),-1 为永不超时 @Transactional(timeout = -1)
rollbackFor 指定哪些异常回滚(默认只回滚 RuntimeException 和 Error) @Transactional(rollbackFor = {IOException.class})
noRollbackFor 指定哪些异常不回滚 @Transactional(noRollbackFor = {NullPointerException.class})
isolation 事务隔离级别 @Transactional(isolation = Isolation.DEFAULT)
propagation 事务传播行为 @Transactional(propagation = Propagation.REQUIRES_NEW)

⚠️ 重要:Spring 默认只对 RuntimeExceptionError 回滚。受检异常(如 IOException)需要手动用 rollbackFor 指定才会回滚。

6.5 事务传播行为(Propagation)

场景:转账操作中,不管转账是否成功,都需要记录日志。但日志操作不能因为转账失败而回滚。

解决:让日志方法用 REQUIRES_NEW 传播行为,独立开启一个新事务。

@Service
public class LogServiceImpl implements LogService {
    @Autowired
    private LogDao logDao;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void log(String out, String in, Double money) {
        logDao.log("转账操作由" + out + "到" + in + ",金额:" + money);
    }
}
传播行为 管理员有事务 管理员无事务
REQUIRED(默认) 加入管理员事务 新建事务
REQUIRES_NEW 新建独立事务 新建事务
SUPPORTS 加入管理员事务 无事务运行
NOT_SUPPORTED 挂起管理员事务,无事务运行 无事务运行
MANDATORY 加入管理员事务 报错
NEVER 报错 无事务运行
NESTED 设置回滚点(savePoint) 新建事务

日常开发中 REQUIRED(默认)能满足大部分需求,REQUIRES_NEW 用于像日志这种需要独立事务的场景。


七、总结

今天我们系统学习了 Spring 的第二个核心大模块——AOP 和声明式事务。

模块 核心要点
AOP 核心概念 连接点、切入点、通知、切面、目标对象、代理
切入点表达式 execution(* com.xxx.service.*Service.*(..))
五种通知 前置 @Before、后置 @After、返回后 @AfterReturning、异常后 @AfterThrowing环绕 @Around
数据获取 参数(getArgs())、返回值(proceed() 返回 / returning)、异常(try-catch / throwing
事务管理 @Transactional + 事务管理器 + @EnableTransactionManagement
事务传播 REQUIRED(默认,加入现有事务)、REQUIRES_NEW(始终新建事务)

🎤 结课小调查:今天的内容量不小,你最想在项目里试一试的是哪个?
A. 用环绕通知给所有 Service 方法加日志
B. 用 AOP 统一处理参数空格
C. 用 @Transactional 管理数据库事务
D. 写个独立的日志事务(REQUIRES_NEW)

评论区告诉我你的选择,也欢迎分享你在事务管理上踩过的坑,我们一起避雷!


本文为 Spring Framework 第四天授课内容整理。AOP 和事务管理是 Spring 的精髓所在,掌握了它们,你的 Java 开发能力将迈上一个大台阶。如果觉得有帮助,欢迎点赞、收藏、关注,我们下节课见!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐