Spring Framework 学习笔记完全指南
这是一份从零开始的 Spring Framework 学习笔记,涵盖了 IoC、DI、AOP、事务管理以及 MyBatis / JUnit 整合等核心内容。由浅入深,适合作为博客归档或复习参考。
目录
- [一、Spring 简介](#一、Spring 简介)
- [二、IoC 与 DI(XML 版本)](#二、IoC 与 DI(XML 版本))
- [2.1 控制反转 IoC](#2.1 控制反转 IoC)
- [2.2 依赖注入 DI](#2.2 依赖注入 DI)
- [2.3 Bean 的基础配置](#2.3 Bean 的基础配置)
- [2.4 Bean 的实例化方式](#2.4 Bean 的实例化方式)
- [2.5 依赖注入的方式](#2.5 依赖注入的方式)
- [2.6 自动装配](#2.6 自动装配)
- [2.7 集合注入](#2.7 集合注入)
- [2.8 Bean 的生命周期](#2.8 Bean 的生命周期)
- 三、注解开发
- [3.1 Spring 总结回顾](#3.1 Spring 总结回顾)
- [3.2 @Component 与衍生注解](#3.2 @Component 与衍生注解)
- [3.3 纯注解配置](#3.3 纯注解配置)
- [3.4 Bean 作用范围与生命周期](#3.4 Bean 作用范围与生命周期)
- [3.5 注解版依赖注入](#3.5 注解版依赖注入)
- [3.6 管理第三方 Bean](#3.6 管理第三方 Bean)
- [3.7 注解开发总结](#3.7 注解开发总结)
- [3.8 MyBatis 整合](#3.8 MyBatis 整合)
- [3.9 JUnit 整合](#3.9 JUnit 整合)
- [四、AOP 面向切面编程](#四、AOP 面向切面编程)
- [4.1 AOP 概念](#4.1 AOP 概念)
- [4.2 AOP 初使用](#4.2 AOP 初使用)
- [4.3 AOP 工作流程](#4.3 AOP 工作流程)
- [4.4 切入点表达式](#4.4 切入点表达式)
- [4.5 通知类型](#4.5 通知类型)
- [4.6 案例:测试业务层接口执行效率](#4.6 案例:测试业务层接口执行效率)
- [4.7 案例:获取通知数据](#4.7 案例:获取通知数据)
- [4.8 案例:百度网盘密码数据兼容处理](#4.8 案例:百度网盘密码数据兼容处理)
- [五、Spring 事务管理](#五、Spring 事务管理)
- [5.1 事务概念](#5.1 事务概念)
- [5.2 Spring 事务使用](#5.2 Spring 事务使用)
- [5.3 事务角色与原理](#5.3 事务角色与原理)
一、Spring 简介
Spring Framework 是 Spring 生态圈中最基础的项目,是其他所有 Spring 项目的根基。
整体架构

Spring 框架采用分层架构,由约 20 个模块组成,核心容器(Core Container)包含了 Beans、Core、Context、SpEL 等核心模块。
两大核心思想
控制反转(IoC --- Inversion of Control) :这是一种思想以及实现。从前创建对象当业务需求改变的时候,创建的对象 new 的对象需要修改,耦合性强,所以有了 IoC 容器来解耦。
注意:IoC 容器管理的对象是 Bean。

依赖注入(DI --- Dependency Injection):这是控制反转中 IoC 容器使用里面的对象的方法,叫做依赖注入。

二、IoC 与 DI(XML 版本)
2.1 控制反转 IoC
引入 Spring 依赖:
xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
步骤一:创建好接口和实现类

步骤二:创建 IoC 容器的配置文件
在 resources 中右键创建 XML 配置文件:

在创建的 XML 中使用 <bean> 标签将我们被管理的 Bean 告知 IoC 容器:

id--- 引用名,后续通过它获取 Beanclass--- 类的全限定路径
步骤三:创建测试类
java
// 初始化对象,导入的是IOC容器的配置文件
ApplicationContext ctx = new ClassPathXmlApplicationContext("springContext.xml");
// 使用IOC容器对象直接提取bean,并强转使用私有方法
StudentDao studentDao = (StudentDao) ctx.getBean("studentDao");
studentDao.study();
2.2 依赖注入 DI
DI(依赖注入),本质上就是,一个方法里面需要调用另一个类的方法,频繁地创建对象耦合强,所以直接创建一个类型变量,然后使用 DI 的方式,直接注入给这个变量值。
步骤一:创建好接口和实现类

步骤二:在实现类中调用 StudentDao 的内容
java
public class StudentServicesImpl implements StudentService {
StudentDao studentDao;
@Override
public void save() {
studentDao.study();
System.out.println("保存操作");
}
@Override
public void setDao(StudentDao studentDao) {
this.studentDao = studentDao;
}
}
为了防止重复 new Student 对象,对外提供对应的 set 方法来赋值。
步骤三:传统方式测试(耦合强)
java
StudentDaoImpl studentDao = new StudentDaoImpl();
StudentService studentService = new StudentServicesImpl();
studentService.setDao(studentDao);
studentService.save();
Dao 对象的作用就是赋值进入 Service 中来调用一个方法,这太麻烦了耦合强。所以我们需要使用注入的方式,来实现在 Service 中调用 Dao 的方法。
步骤四:使用 XML 优化
xml
<bean id="studentDao" class="com.dongmianmao.dao.impl.StudentDaoImpl"/>
<bean id="studentService" class="com.dongmianmao.service.impl.StudentServicesImpl">
<property name="dao" ref="studentDao"/>
</bean>
我们先指定 Dao 的路径给 IoC 容器管理,然后第二步指定 Service 的路径给 IoC 容器。其中 <property> 标签:
name--- 这个实现类中要注入的变量的方法(即接口中定义的 set 方法),名字是去掉set后的名字

ref--- 上面交给 IoC 容器管理的 dao 的 id
测试类:
java
// 初始化对象,导入的是IOC容器的配置文件
ApplicationContext ctx = new ClassPathXmlApplicationContext("springContext.xml");
// 使用IOC容器对象直接提取bean,并强转使用私有方法
StudentService studentService = (StudentService) ctx.getBean("studentService");
studentService.save();
可以发现实现了同样的效果,并且不需要 new 一堆对象。但有个前提:需要提供 set 方法,才能注入进去。
2.3 Bean 的基础配置
起别名
name 属性用于起别名,可用逗号、空格、分号来进行分割多个别名:
xml
<bean id="studentDao" name="dao" class="com.dongmianmao.dao.impl.StudentDaoImpl"/>
ctx.getBean 就可以直接使用别名来进行获取:
java
// 初始化对象,导入的是IOC容器的配置文件
ApplicationContext ctx = new ClassPathXmlApplicationContext("springContext.xml");
// 使用IOC容器对象直接提取bean,并强转使用私有方法
StudentService studentService = (StudentService) ctx.getBean("dao");
studentService.save();
作用范围(scope)
指的是 Bean 的作用范围,即单例多例:
- 单例 → 反复拿取的对象都是同一个
- 多例 → 反复拿取的对象都是不同的
使用 scope 属性,有两个值:
| 值 | 含义 |
|---|---|
singleton |
单例(默认),多次获取的是同一个对象 |
prototype |
多例,每次获取都会创建新对象 |
xml
<bean id="..." class="..." scope="prototype"/>
为什么 Bean 默认为单例?


主要出于性能和资源考虑 --- 大多数情况下业务对象不需要频繁创建,单例可以减少内存开销,提高运行效率。
2.4 Bean 的实例化方式
获取 Bean 的实例化有四种方法,本质上都是为了交给 Spring 管理。
方式一:直接配置 Bean
创建好接口以及实现类后在 XML 配置:

xml
<bean id="userDao" class="com.dongmianmao.dao.impl.UserDaoImpl"/>
测试类:
java
// 初始化对象,导入的是IOC容器的配置文件
ApplicationContext ctx = new ClassPathXmlApplicationContext("springConfig.xml");
// 使用IOC容器对象直接提取bean,并强转使用私有方法
StudentService studentService = (StudentService) ctx.getBean("dao");
studentService.save();
方式二:静态工厂
静态工厂是一种设计模式,一个工具类来对外提供对象,一般是单例设计模式,多次获取的都是同一个对象。其实就是带 static 的获取对象的方法。
先创建一个工厂类:
java
public class StaticBookFactory {
public static BookDao getBookDao() {
return new BookDaoImpl();
}
}
在 XML 中配置:
xml
<bean id="userDao" class="com.dongmianmao.factory.StaticBookFactory" factory-method="getBookDao"/>
和第一点不同的是这里指定的不是实现类,而是工厂类。factory-method 则是指定工厂类中对外提供对象的方法。
测试类:
java
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-config.xml");
BookDao bookDao = (BookDao) applicationContext.getBean("userDao");
bookDao.add();
}
方式三:实例工厂
实例工厂和静态工厂的区别就是没有 static 修饰,因为没有 static 所以使用方式发生变化了。
先创建一个实例工厂:
java
public class InstanceBookFactory {
public BookDao getDao() {
return new BookDaoImpl();
}
}
此时在 XML 中:
xml
<bean id="instanceBookFactory" class="com.dongmianmao.factory.InstanceBookFactory"/>
<bean id="userDao" factory-bean="instanceBookFactory" factory-method="getDao"/>
逻辑理解:按正常逻辑,要拿到 getDao() 这个方法的内容,必须先创建 InstanceBookFactory 对象,再调用 getDao 方法。配置也是按照这个流程:
- 先将实例工厂交给 IoC 容器管理
- 指定一个 Bean,里面使用
factory-bean来指定实例工厂的 id,再使用factory-method来指定实例工厂里的方法
方式四:FactoryBean 接口(实例工厂升级版)
书写了上面的 XML 配置后就会发现,只需要一个对象,还需要写两个 Bean 才能配置好。所以 Spring 准备好了解决方案:只需要实现 Spring 提供的 FactoryBean 接口,Spring 就知道需要的不是这个对象,而是里面的方法。
创建一个类来实现 Spring 提供的接口,并在 getObject() 方法中去返回原本实例工厂中需要返回的对象:
java
public class MyBeanFactory implements FactoryBean {
@Override
public Object getObject() throws Exception {
return new BookDaoImpl();
}
@Override
public Class<?> getObjectType() {
return null;
}
}
在 XML 中,直接去指定实现了 Spring 接口的类即可:
xml
<bean id="userDao" class="com.dongmianmao.factory.MyBeanFactory"/>
测试类:
java
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-config.xml");
BookDao bookDao = (BookDao) applicationContext.getBean("userDao");
bookDao.add();
}
2.5 依赖注入的方式
首先先明白,依赖注入描述的是在容器中建立 Bean 与 Bean 之间的依赖关系,本质就是将一个类中和别的类解耦的方式------把别的类写在成员变量位置,再对外提供可以给成员变量赋值的方法,外界直接调用来给对应的类赋值。

依赖注入的方式对应 setter 和构造器分别有四种方式:

setter 注入
首先创建好一个 BookService 接口,再创建一个实现类。实现类中重写了 add 方法,以及简单类型和引用类型的 setter 方法。
注意:没有对外界提供赋值方法的话无法注入。
java
public class BookServiceImpl implements BookService {
BookDao bookDao;
Integer num;
@Override
public void add() {
bookDao.add();
System.out.println("this number = " + num);
System.out.println("BookService add");
}
public void setBookDao(BookDao bookDao) { this.bookDao = bookDao; }
public void setNum(Integer num) { this.num = num; }
}
此时 XML 配置:先单独配置 bookDao,再配置 bookService。因为里面有两个属性值需要注入,所以还需要分别配置两个标签------引用类型使用 ref 指定,简单类型使用 value 赋值。
xml
<bean id="bookDao" class="com.dongmianmao.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.dongmianmao.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
<property name="num" value="123"/>
</bean>
测试类:
java
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-config.xml");
BookService bookDao = (BookService) applicationContext.getBean("bookService");
bookDao.add();
构造器注入
实现类中包含简单类型和引用类型的构造方法:
java
public class BookServiceImpl implements BookService {
BookDao bookDao;
Integer num;
public BookServiceImpl(BookDao bookDao, Integer num) {
this.bookDao = bookDao;
this.num = num;
}
@Override
public void add() {
bookDao.add();
System.out.println("this number = " + num);
System.out.println("BookService add");
}
}
此时 XML 配置里,赋值的标签使用的是 constructor-arg(毕竟用的是构造器赋值,所以标签命名也和构造器有关):
xml
<bean id="bookDao" class="com.dongmianmao.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.dongmianmao.service.impl.BookServiceImpl">
<constructor-arg name="bookDao" ref="bookDao"/>
<constructor-arg name="num" value="123"/>
</bean>
<!-- 也可使用 type 和 index 进一步精确指定 -->
<bean id="bookDao" class="com.dongmianmao.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.dongmianmao.service.impl.BookServiceImpl">
<constructor-arg type="com.dongmianmao.dao.BookDao" name="bookDao" ref="bookDao"/>
<constructor-arg index="1" name="num" value="123"/>
</bean>
<!--
指定他们的入参类型type,以及下标索引index,不过写不写值都一样
-->
测试类同上。
依赖注入方式选择

- 强制依赖(对象必须依赖的属性)用构造器注入
- 可选依赖(可有可无的属性)用 setter 注入
2.6 自动装配
IoC 容器根据 Bean 所依赖的资源在容器中自动查找并注入 到 Bean 中的过程叫做自动装配。以往我们都是手动在 XML 中配置注入的对象(标签中使用 <property> 标签指定),这个叫手动装配。
自动装配的方式通过 <bean> 标签的 autowire 属性控制:

byType--- 按类型自动装配(setter 注入)byName--- 按名称自动装配(setter 注入)constructor--- 按构造方法装配no--- 不启用自动装配(默认)
除了构造方法的,都是基于 setter 注入。
使用示例:
xml
<!-- 手动装配(原来) -->
<bean id="bookDao" class="com.dongmianmao.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.dongmianmao.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
</bean>
<!-- 自动装配(byType) --- 不需要再指定 property,Spring 自己去 IoC 容器中寻找 -->
<bean id="bookDao" class="com.dongmianmao.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.dongmianmao.service.impl.BookServiceImpl" autowire="byType"/>
自动装配 vs 依赖注入(思想层面):
- DI 更注重解耦 --- 使用多态 + 依赖注入,当业务需求变更时不需要修改业务代码
- Autowire 更注重简化开发 --- 通过类型/名称匹配自动注入,减少配置量,提升开发效率
2.7 集合注入
集合有以下几种:数组、List、Set、Map、Properties。

注入都是使用 <property> 标签,<property> 下都有对应的容器标签(如 <list>、<set>、<map>、<props>)。
2.8 Bean 的生命周期
简单来说就是 Bean 从创建到销毁的过程。
方式一:XML 配置

在 <bean> 标签中配置 init-method(初始化做的事情)和 destroy-method(销毁前做的事情)。
方式二:实现接口
InitializingBean接口 → 实现afterPropertiesSet()替代 initDisposableBean接口 → 实现destroy()替代 destroy
实现这两个接口就可以不用配置 XML 了。
IoC 容器关闭方式

需要手动关闭容器才能触发 Bean 的销毁方法(ClassPathXmlApplicationContext 提供了 close() 方法,或者使用 registerShutdownHook() 注册钩子)。
三、注解开发
3.1 Spring 总结回顾
在学习注解开发之前,先回顾 XML 方式的核心知识点。
容器相关:

Bean 相关:

重点知道 autowire 和 lazy-init:
autowire--- 自动装配,有byType和byNamelazy-init--- 延迟加载,什么时候用什么时候加载
依赖注入: 记住两个,构造器注入和 setter 注入。简单类型用 value,引用类型用 ref。
3.2 @Component 与衍生注解
在以往,配置 Bean 给 IoC 容器需要写 XML 配置,缺点就是冗余、费时间,所以有了注解开发。
使用 @Component
场景:将 BookDao 的实现类 BookDaoImpl 加载进 IoC 容器。
java
/**
* @FileName BookDaoImpl
* @Description
* @Author zyw
* @date 2025-07-02
**/
@Component
public class BookDaoImpl implements BookDao {
@Override
public void update() {
System.out.println("BookDao update...");
}
}
在类的头上使用 @Component 注解来代替 <bean> 标签。
但是写了这个注解,JVM 怎么知道我们需要把这个注解的类给加载进入 IoC 容器呢?所以还需要在 XML 中配置对应的扫描路径。
配置 XML:
首先需要导入 Context 标签。配置好后在里面使用以下标签来进行类的扫描:
xml
<context:component-scan base-package="com.dongmianmao.dao"/>
base-package:可以写得更细,也可以只写整个包路径,把整个包带有 @Component 注解的实例化 Bean 加载进 IoC 容器。
使用类型的方式找到 Bean

可以看到,正确地使用类型的方式(字节码文件)在 IoC 中找到了这个 Bean。
使用别名的方式找到 Bean
只需要在原来的注解中加上别名即可:
java
@Component("bookDao")
public class BookDaoImpl implements BookDao {
@Override
public void update() {
System.out.println("BookDao update...");
}
}

默认 Bean 别名
当没有给 @Component 值的时候有两种情况的默认值:
1. 正常驼峰的命名
如果使用的是 BookDao,是正确的驼峰命名,那默认命名就是类名首字母小写,就可以加载到这个 Bean。

2. 不正常的驼峰命名
如 BOOKDaoImpl,这明显没有跟着驼峰来,所以这种的人家就不管了,直接你什么类名,就给你取什么类名。

@Component 的三个衍生注解
@Controller--- 用于表现层接口的 Bean 定义@Service--- 用于业务层的 Bean 定义@Repository--- 用于数据层 Bean 定义
这三个功能上都是一样的。为什么要衍生?目的一是为了优雅,目的二是为了某一层的 Bean 做特殊的处理,是为了标记区分分层。
3.3 纯注解配置
上面所描述的都是需要配置文件的参与,在 Spring 3.0 的时候,升级了纯注解开发,使用 Java 类来替代配置文件。
简单说,就是把右边的配置文件,由左边的配置类来替代:

先来爽一把:
java
/**
* @FileName SpringConfig
* @Description Spring的配置类
* @Author zyw
* @date 2025-07-02
**/
@Configuration
@ComponentScan("com.dongmianmao.dao")
public class SpringConfig {
}
@Configuration--- 设定当前类为配置类@ComponentScan--- 替代原来的<context:component-scan>标签,用来扫描对应的路径加载类。只能添加一次 ,多个数据使用数组格式,如@ComponentScan({"com.dmm.dao", "com.dmm.service", ...})
测试类:
java
// 加载配置类,加载IOC容器
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
// IOC容器拿取Bean
BookDao bean = (BookDao) ctx.getBean("bookDaoImpl");
bean.update();
AnnotationConfigApplicationContext 是因为现在已经不是加载 XML 了,所以就需要换子类来加载配置类。
小结

3.4 Bean 作用范围与生命周期
作用范围(@Scope)
在加载入 IoC 容器的类上使用 @Scope 注解。有两个值,默认是单例,不用记,记多例 prototype 就得了:
java
@Repository
@Scope("prototype")
public class BookDaoImpl implements BookDao {
@Override
public void update() {
System.out.println("BookDao update...");
}
}
生命周期(@PostConstruct / @PreDestroy)
Bean 的两个生命周期 --- init 和 destroy --- 都是在 XML 中配置方法的,现在改为注解配置:
java
/**
* @FileName BookDaoImpl
* @Description
* @Author zyw
* @date 2025-07-02
**/
@Repository
public class BookDaoImpl implements BookDao {
@Override
public void update() {
System.out.println("BookDao update...");
}
@PostConstruct
public void init() {
System.out.println("BookDao init...");
}
@PreDestroy
public void destroy() {
System.out.println("BookDao destroy...");
}
}
@PostConstruct--- 初始化方法(原init-method)@PreDestroy--- 销毁方法(原destroy-method)

小结

3.5 注解版依赖注入
在以往,配置依赖注入都是需要使用 XML 进行配置,会增加开发时间,很费时间,所以有了注解开发。
前置说明: 现在有一个数据层接口 BookDao 和一个实现类 BookDaoImpl,还有一个业务层接口 BookService 和一个实现类 BookServiceImpl。
java
/**
* @FileName BookServiceImpl
* @Description 业务层实现类
* @Author zyw
* @date 2025-07-02
**/
@Service
public class BookServiceImpl implements BookService {
private BookDao bookDao;
@Override
public void update() {
bookDao.update();
System.out.println("BookService update..");
}
}
这个类就是今天的主角,里面有一个成员变量 BookDao,使其和当前类产生依赖关系。
记得一定要写类上方的注入 IoC 容器的注解,不然 Spring 不给注入,因为找不到,会空指针异常。
依赖注入 --- 一个实现类(@Autowired)
java
/**
* @FileName BookServiceImpl
* @Description 业务层实现类
* @Author zyw
* @date 2025-07-02
**/
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
public void update() {
bookDao.update();
System.out.println("BookService update..");
}
}
成员上使用 @Autowired 注解来自动装配:

Q:上面提过,只能使用 setter 和构造器的方式来进行注入,为什么这里没有写 setter 和构造也能注入成功?
A:Spring 底层使用了反射暴力塞值,不需要显式提供 setter。
依赖注入 --- 多个实现类(@Qualifier)
当多个实现类的时候,暴力反射可能会找不到值,所以需要指定需要注入的类,不然会报错。
这里我们再创建一个 BookDao 实现类 BookDaoImpl2:
java
@Repository
public class BookDaoImpl2 implements BookDao {
@Override
public void update() {
}
}
然后使用原来的方式直接调 IoC 容器的 Bean,会出现以下报错:

但当我们给这两个类都命名 Bean 别名,最开始的类叫原来的名字,第二个就叫 bookDao2,再运行:

可以发现就不报错了,这是因为它会默认去找与变量名相同的别名的类。
当然,当一个接口有多个实现类时,我们可以去指定使用哪个类注入 ,使用 @Qualifier:
java
@Service
public class BookServiceImpl implements BookService {
@Autowired
@Qualifier("bookDao2")
private BookDao bookDao;
@Override
public void update() {
bookDao.update();
System.out.println("BookService update..");
}
}
@Qualifier 使用这个注解,去指定要注入的类的类型别名:

可以看到,根据我们使用的注解,找到了我们需要的 BookDao2。
注意:注入的时候最好使用无参构造,不要用有参构造,不然多个实现类的时候找不到使用哪一个来注入。
简单类型注入(@Value)
第一种方法:直接注入
java
@Value("123")
private String name;
System.out.print("name=" + name);

第二种方法:通过 properties/yml 文件注入
用于注入配置信息。假设现在有一个配置文件 jdbc.properties,用来注入配置数据库。在以往需要使用 properties 来加载配置,现在则使用注解开发。
步骤一:导配置文件加载进 JVM
在 Spring 配置类中引入配置文件路径:
java
@Configuration
@PropertySource("jdbc.properties")
public class SpringConfig {
}
@PropertySource 使用这个注解来导入配置文件。
步骤二:注入配置文件值
使用 ${key} 的方式来进行插值,来找加载进的配置文件的值:
java
@Service
public class BookServiceImpl implements BookService {
@Value("${url}")
private String url;
@Override
public void update() {
System.out.println("url=" + url);
System.out.println("BookService update..");
}
}

可以看到成功注入成功。当需要导入多个配置文件时,使用数组的方式进行引入。
小结

3.6 管理第三方 Bean
这里使用 Druid 来进行演示如何管理第三方 Bean。
xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
在之前使用 XML 进行导入 Bean 的时候,是直接使用 setter 或者构造器注入的。那改为注解开发后也不能直接在类上使用 @Component 注解,该如何使用注解开发来加载进入 IoC 容器呢?
基本使用
注入到 IoC 容器的目的就是为了能够自动装配 ,可以从 IoC 容器里面加载,本质目的就是为了拿到这个对象。所以只需要在配置类中设置一个方法,能够返回这个对象,再使用注解加载这个方法就可以了:
java
@Configuration
public class SpringConfig {
@Bean
public DruidDataSource dataSource() {
return new DruidDataSource();
}
}
@Bean--- 这个注解是将所引用的方法的返回对象来加载进 IoC 容器,默认别名是方法名
获取 Bean 的方式
- 类型 --- 字节码
- 别名 ---
name
这里使用别名方式:

要自己起别名直接 @Bean("别名") 就得了。
配置分离
正常情况下,如果什么都写进 SpringConfig 这个总体的配置文件里,那就会特别乱。所以应该另外新建一个 Config,再让 SpringConfig 来加载这个 Config。
我们把刚刚的 @Bean 抽离出来,新建一个 DaoConfig:
java
public class DaoConfig {
@Bean("data")
public DruidDataSource dataSource() {
return new DruidDataSource();
}
}
方式一(导入式):使用 @Import
java
@Configuration
@Import(DaoConfig.class)
public class SpringConfig {
}
@Import 使用这个注解,值是字节码文件,来导入这个 Config:

方式二(扫描式):使用 @Configuration + @ComponentScan
Dao 配置类:
java
@Configuration
public class DaoConfig {
@Bean("data")
public DruidDataSource dataSource() {
return new DruidDataSource();
}
}
主配置类:
java
@Configuration
@ComponentScan("com.dongmianmao")
public class SpringConfig {
}
@ComponentScan("com.dongmianmao") 这个注解会去扫描这个包下的所有注解,会把上面的 Dao 配置类注解也给扫描加载进入 Spring 中。
配置第三方资源所需要的参数
直接定义对应的成员变量,导入配置文件(直接在主配置文件导就得了),再使用 @Value 注解来进行注入配置即可:

引用类型注入(第三方依赖的依赖注入)
直接方法参数给,Spring 会自己去 IoC 根据这个类型来找并注入:

3.7 注解开发总结

3.8 MyBatis 整合
先看一下以往使用 MyBatis 的时候是如何使用的:


这些都是原生的 MyBatis 操作。自从 Spring 出来后,作为主流框架,它允许很多别的框架整合 Spring 它本身,但是这些整合不会是 Spring 自己写,而是需要整合的框架自己写。所以说需要导入对应整合框架的依赖。
记得:整合的框架内容得基于这个框架内容。意思就是,要导依赖的时候,不仅要导原来的依赖,还要导入整合 Spring 的依赖。
所需依赖:
xml
<!-- Spring 核心 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!-- Spring 中简化的 JDBC 操作 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.16</version>
</dependency>
<!-- MyBatis 整合 Spring 的依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.1.1</version>
</dependency>
<!-- Druid 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
项目结构

JdbcConfig --- 配置数据源
主要用来配置 Druid 数据库连接池,从配置文件注入配置信息,然后再加载进入 IoC 容器:
java
/**
* @FileName JdbcConfig
* @Description 配置Druid数据源
* @Author zyw
* @date 2025-07-04
**/
@Configuration
public class JdbcConfig {
@Value("${driver}")
private String driver;
@Value("${url}")
private String url;
@Value("${name}")
private String name;
@Value("${password}")
private String password;
/**
* 查看是否注入成功
*/
@PostConstruct
public void printConfig() {
System.out.println("Driver: " + driver);
System.out.println("URL: " + url);
System.out.println("Name: " + name);
System.out.println("Password: " + password);
}
/**
* 创建DruidDataSource对象,使用数据源
* @return
*/
@Bean("dataSource")
public DruidDataSource getDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(driver);
druidDataSource.setUrl(url);
druidDataSource.setUsername(name);
druidDataSource.setPassword(password);
return druidDataSource;
}
}
重点:使用成员变量注入的方式来给成员变量赋值。
MybatisConfig --- 配置 MyBatis
主要是用来配置 MyBatis 的 SqlSessionFactoryBean(这个类是 MyBatis 为了和 Spring 进行整合衍生的类),还配置 MyBatis 的 Mapper 扫描路径的 Bean MapperScannerConfigurer:
java
/**
* @FileName MybatisConfig
* @Description Mybatis配置类 配置SqlSessionFactory
* @Author zyw
* @date 2025-07-04
**/
@Configuration
public class MybatisConfig {
/**
* 获取SqlSession对象,使用Druid数据源来使用Mybatis
* @param dataSource
* @return
*/
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setTypeAliasesPackage("com.dongmianmao.pojo");
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean;
}
/**
* 创建MapperScannerConfigurer对象,用来加载Dao接口
* Mybatis会自己去扫描这个包的接口,自己创建代理类对象加载IOC容器
* 所以直接拿Dao的Mapper的时候才能直接从IOC中拿出来,@Mapper的底层
* @return
*/
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
mapperScannerConfigurer.setBasePackage("com.dongmianmao.dao");
return mapperScannerConfigurer;
}
}
这里主要理解的是:
- 配置 MyBatis 的
SqlSessionFactoryBean的时候,使用的引用型注入是放在形参位置的,然后 Spring 会自己去 IoC 中根据类型去寻找这个类并注入 MapperScannerConfigurer这个类是 MyBatis 用来扫描 Mapper 接口的一个 Bean,配置好路径后,它会给对应路径的 Mapper 接口去生成代理类 并加载进入 IoC 容器 --- 所以能够让对应的 Mapper 接口能够通过byType和byName的方式去进行注入 Bean
SpringConfig --- 主配置
java
@Configuration
@ComponentScan("com.dongmianmao")
@PropertySource("classpath:jdbc.properties")
public class SpringConfig {
}
重点注意:使用 @PropertySource 的时候,里面的路径前缀 classpath: 是规范。
3.9 JUnit 整合
首先要明白一点,原生的 JUnit 是无法完成依赖注入 的操作的。如果只使用原始的 JUnit,那一定会爆一堆 NullPointerException。所以它整合的目的是为了能够实现依赖注入。
因为要使用整合的依赖,就依赖于原依赖。
所需依赖:
xml
<!-- JUnit 整合 Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
接下来需要知道的是测试规范:只需要知道,在 Test 目录下的所有要测试的东西,都需要和 main 目录一致,是为了在 main 目录下能找到对应的类。
使用
首先创建好一个对应路径的测试类后,需要加上以下注解:
java
/**
* @FileName Account
* @Description AccountMapper测试类
* @Author zyw
* @date 2025-07-04
**/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountDaoTest {
@Autowired
private AccountMapper mapper;
@Test
public void testFindAccountToIdTest() {
System.out.println(mapper.findAccountToId(1));
}
}
@RunWith(SpringJUnit4ClassRunner.class)--- 使用 Spring 整合 JUnit 的专用类加载器,SpringJUnit4ClassRunner就是整合了 Spring 的类@ContextConfiguration(classes = SpringConfig.class)--- 加载 Spring 配置类,因为 Test 目录是在另一个目录,它无法将 main 的 IoC 容器给加载进来,所以需要这个注解。记住classes参数跟着的是 Spring 主配置类的字节码文件就得了
测试结果:

关于 Mapper 爆红问题
看上图也注意到了,mapper 这个变量爆红了,而程序还能运行。这个是因为这个 Mapper 接口没有实现类,IDEA 就觉得你可能写错了,就爆红了。
但实际上,Bean 扫描 Mapper 扫描到这个接口的时候,会创建一个代理类并加载进 IoC 容器,所以实际上我们拿到的,是一个代理类。报红的根本原因也只是没有实现类而已。

要是实在在意,就加个 @Mapper 注解在接口上,这个注解底层会生成一个实现类,这样就不爆红了。
四、AOP 面向切面编程
4.1 AOP 概念
AOP(Aspect Oriented Programming):面向切面编程,一种编程规范,指导开发者如何组织程序结构。
作用: 在不惊动原始设计的基础上,为其进行功能增强。
典型应用场景:
- 在工程运行慢的过程,对目标方法进行运行耗时统计
- 对目标方法添加事务管理
- 对目标方法添加权限访问控制
AOP vs 动态代理:
在原来一开始的时候,就学过 Proxy,也是对原有方法进行扩展。看下图,使用 Proxy 和使用 AOP 的区别 --- 简化了原来动态代理的代理创建:

AOP 的好处:
- 简化开发 --- 不需要手动写 Proxy,简化了原始动态代理的代理创建
- 灵活性强 --- 可以统一对一批方法进行 AOP
- Spring 事务就是基于 AOP 实现的
4.2 AOP 初使用
引入 AspectJ 依赖(AOP 由 AspectJ 来进行整合的,所以 Spring 直接用他们的依赖):
xml
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
设计 AOP 的三要素:
- 对哪些内容做增强(切入点)
- 在什么时候进行增强(开始前、结束后)
- 增强什么内容
场景
现如今有一个 Dao 层,需要对已有的保存操作进行统计运行时间的功能。这是目标实现类:
java
/**
* @FileName UserDaoImpl
* @Description 接口UserDao的实现类
* @Author zyw
* @date 2025-07-05
**/
@Repository
public class UserDaoImpl implements UserDao {
@Override
public void save() {
System.out.println("UserDao save");
}
}
我们要在不改变代码的情况下进行扩展,就两种方法:Proxy 或 AOP。这里我们使用 AOP。
第一步:创建通知类(Advice)
我们对一个类进行扩展,必然是新创建一个类来关联原有的类,这些 AOP 类统称 Advice(通知/消息):

通知类代码:
java
/**
* @FileName MyAdvice
* @Description 通知类
* @Author zyw
* @date 2025-07-05
**/
@Aspect
@Component
public class MyAdvice {
private long start;
private long end;
// 对谁做增强(切入点表达式)
@Pointcut("execution(void com.dongmianmao.dao.UserDao.save())")
public void pt() {}
// 什么时候增强(前置通知)
@Before("pt()")
public void before() {
// 增强的内容
start = System.currentTimeMillis();
}
// 什么时候增强(后置通知)
@After("pt()")
public void after() {
// 增强的内容
end = System.currentTimeMillis();
System.out.println(String.format("共运行了%dms", end - start));
}
}
几个重要点:
1. 对谁做增强 --- @Pointcut(切入点表达式)
java
@Pointcut("execution(void com.dongmianmao.dao.UserDao.save())")
public void pt() {}
@Pointcut 这个注解下的方法作为切入点 ,无方法体,无参数,仅作标记。而它的参数 execution,需要的是切入目标方法的路径。
2. 什么时候增强 --- @Before / @After
AOP 的这些通知就和 Proxy 动态代理差不多这样的结构:

3. 标记为 AOP 切面类
java
@Aspect
@Component
@Aspect--- 标记为 AOP 切入类@Component--- 注入到 IoC 容器。因为实际调用出来的并不是原来的类,而是代理类,所以得把 Bean 给加载进入 IoC

第二步:配置 Spring 为允许 AOP
java
@Configuration
@EnableAspectJAutoProxy // 开启AOP功能
@ComponentScan("com.dongmianmao")
public class SpringConfig {
}
@EnableAspectJAutoProxy 无参数,设置后表示允许使用 AOP。
第三步:测试

AOP 专业术语



切面:对谁做增强,什么时候做,做了哪些内容 --- 切面就是这三个的整合。
4.3 AOP 工作流程


Spring AOP 的核心流程:
- Spring 容器启动时,扫描所有切面类
- 根据切入点表达式匹配目标方法
- 为目标方法所在的 Bean 创建代理对象
- 调用目标方法时,实际调用的是代理对象,代理对象在执行目标方法前后会调用相应的通知方法
4.4 切入点表达式
切入点:要进行增强的方法。
切入点表达式:要进行增强的方法描述方式。
标准格式
动作关键字(访问修饰符 返回值 包名.类/接口.方法名(参数) 异常名)
示例:
java
@Pointcut("execution(public User com.dongmianmao.dao.UserDao.findUserById(int))")
- 动作关键字:描述切入点的行为动作,例如
execution表示执行到指定切入点(固定写法) - 访问修饰符:
public、private等,可以省略 - 返回值
- 包名
- 类/接口名
- 方法名
- 参数
- 异常名:方法定义中抛出指定异常,可以省略
记忆方式:
execution(public User com.dongmianmao.dao.UserDao.findUserById(int))
可以看到,这 execution 里面包含的其实和方法的定义一致,只不过方法名精确到包名。
通配符
* --- 单个独立的任意符号
可以独立出现,也可以作为前缀或后缀的匹配符出现:
java
execution(public * com.dongmianmao.*.UserService.find*(*))
- 匹配
com.dongmianmao包下的任意包中的UserService类/接口中所有find开头的带有一个参数的方法 - 其实更多的是把返回值设置为这个通配符
- 注意方法参数使用这个通配符,找的是有参数的方法(任意参数)
.. --- 多个连续的任意符号
用于简化包名与参数的书写:
java
execution(public * com..UserService.findUserById(..))
- 匹配
com包下的任意包中的UserService类接口中所有名称为findUserById的方法 (..)表示任意参数(包括无参)
+ --- 专用于匹配子类类型
子类指的是实现类/继承子类:
java
execution(* *..*Service+.*(..))
*Service表示的是前缀为任意,但后缀是 Service 的类/接口,如UserService、AccountService,表示的是对一个群体的类进行增强Service+会匹配这个 Service 为后缀的接口/类的所有子类(实现类和继承子类),这样的好处是子类的特有方法也会一起被增强
书写规范/技巧

企业开发中一般切入到业务层(Service)的方法,粒度适中。
4.5 通知类型
AOP 通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置。
AOP 的通知分为五种类型:
- 前置通知
- 后置通知
- 环绕通知(重点)
- 返回后通知(了解)
- 抛出异常后通知(了解)
前置通知 --- @Before
在切入点方法执行前执行的方法:
java
@Before("pt()")
public void before() {
// 增强操作
}
后置通知 --- @After
在切入点方法执行后执行的方法:
java
@After("pt()")
public void after() {
// 增强操作
}
环绕通知(重点)--- @Around
字如其名,把这个切入点方法包起来(前后)。可以看到这个 @Around 的执行是在 @After 之前、@Before 之后的。为什么重点?因为它能干 After 和 Before 两个能干的事情:

java
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("around before");
Object result = joinPoint.proceed();
System.out.println("around after");
return result;
}
ProceedingJoinPoint--- 这个入参对象是切入点的代理类方法,环绕通知必须加这个参数joinPoint.proceed()--- 是执行这个切入点代理类方法

返回后通知 --- @AfterReturning(了解)
在切入点方法执行完毕后,在 @After 执行之前执行:
java
@AfterReturning("pt()")
public void afterReturning() {
System.out.println("After Returning");
}
抛出异常后通知 --- @AfterThrowing(了解)
在切入点方法抛出异常的时候通知,未发生异常不执行:
java
@AfterThrowing("pt()")
public void afterThrowing() {
System.out.println("AfterThrowing");
}
注意事项
@AfterReturning和@AfterThrowing互斥:抛异常就不会成功返回,成功返回就不会抛异常。
@Around 注意事项:

- 重点注意返回值。如果原需要增强的方法有返回值,需要在环绕通知中设置返回
Object返回值类型 - 当拿到原需要增强类的方法的返回值的时候,是可以对这个返回值做出修改再返回出去的
4.6 案例:测试业务层接口执行效率
使用环绕通知统计方法执行耗时:
java
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取签名
Signature signature = joinPoint.getSignature();
// 获取接口路径
String type = signature.getDeclaringTypeName();
// 获取方法名字
String name = signature.getName();
long before = System.currentTimeMillis();
// 执行万次方法
for (int i = 0; i < 10000; i++) {
joinPoint.proceed();
}
long end = System.currentTimeMillis();
System.out.println(String.format("万次方法执行了:%s.%s ===> %dms", type, name, (end - before)));
}
重点知道使用 joinPoint.getSignature() 获取签名,然后基于签名,去获得接口路径名和接口方法名。
4.7 案例:获取通知数据
在开始之前,先看一张图来了解这篇文章所要讲的内容和能获取的信息:

现如今有一个实现类:
java
@Repository
public class UserDaoImpl implements UserDao {
public Integer save(String name, Integer age) {
System.out.println(String.format("UserDao name:%s , age:%d", name, age));
System.out.println("UserDao save");
return 100;
}
}
获取切入点方法的参数
1. 环绕通知(ProceedingJoinPoint)
环绕通知只能使用 ProceedingJoinPoint,不然无法使用 joinPoint.proceed() 方法来执行原方法:
java
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
System.out.println("Save方法的请求参数值 :" + Arrays.toString(args));
Object proceed = joinPoint.proceed();
return proceed;
}
// 执行结果
/*
Save方法的请求参数值 :[张三, 5]
UserDao name:张三 , age:5
UserDao save
*/
joinPoint.getArgs() 通过这个方法来拿到参数数组。
2. 其他通知(JoinPoint)
除了环绕通知以外的四个通知不是不能加参数,而是需要使用的时候才加 ,而环绕通知是必须加。这里拿 @Before 做示范:
java
@Before("pt()")
public void before(JoinPoint joinPoint) {
System.out.println("Before args:" + Arrays.toString(joinPoint.getArgs()));
}
同 ProceedingJoinPoint 一致,有 getArgs() 方法来获取参数数组。
获取切入点方法的返回值
1. 环绕通知
java
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object proceed = joinPoint.proceed();
return proceed;
}
执行原来的方法直接赋值就得了。
2. 返回后通知(@AfterReturning)
java
@AfterReturning(value = "pt()", returning = "num")
public void afterReturning(Integer num) {
System.out.println("AfterReturning " + num);
System.out.println("AfterReturning");
}
returning = "num"--- 绑定的是注解下的方法的入参名,这个入参绑定的是原方法最后的返回值- 切记,这个地方定义的入参,需要和原方法的入参类型一致
回顾一下:在注解中,我们之所以不需要写 value 这个前缀,是因为只有一个参数的时候是不需要写的。但当有第二个参数的时候,就需要把每个参数的前缀都写完整。
获取切入点的异常通知
1. 环绕通知
在使用 joinPoint.proceed() 的时候,会提示抛出异常或捕获异常,这是因为它不清楚运行你的这个原方法是否会发生异常。所以直接在里面捕获异常就可以处理异常了:
java
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) {
Object proceed = null;
try {
proceed = joinPoint.proceed();
} catch (Throwable e) {
System.out.println("Save 异常");
}
return proceed;
}
2. 抛出异常后通知(@AfterThrowing)
java
@AfterThrowing(value = "pt()", throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("AfterThrowing:" + t);
}
这里重点讲一下 Throwable 类型:其实这个就是 try-catch 的那个捕获异常类型:
java
try {
// ...
} catch (Throwable e) {
// ...
}
总结
关于获取值入参的定义:
ProceedingJoinPoint--- 环绕通知JoinPoint--- 前置通知、后置通知- 根据返回参数定义类型并注解参数指定 --- 返回后通知、抛出异常后通知
重点就是环绕通知,因为前置后置能干的环绕通知都能干。

4.8 案例:百度网盘密码数据兼容处理
引入:

这一篇主要知道的是 trim() 方法 --- 去除前后空格的方法。
直接看核心业务代码:
java
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
// 判断是否为String,是的话就去除空格
for (int i = 0; i < args.length; i++) {
if (args[i].getClass().equals(String.class)) {
args[i] = ((String) args[i]).trim();
}
}
// 直接调用原方法,把处理后的参数数组丢进去
return joinPoint.proceed(args);
}
关键两点:
trim()是 String 的专属方法- 在调用原方法之前,可以先经过环绕通知处理参数格式,再把参数数组丢进去执行方法,不需要单独取出每个参数
五、Spring 事务管理
5.1 事务概念
最本质的本质,我们得清楚:Spring 事务的目的就是为了保证 SQL 语句执行的原子性,同 SQL 中的事务的目的。
- 事务的作用: 在数据层保障一系列的数据库操作同成功同失败(要不一起成功要不一起失败)
- Spring 事务作用: 在数据层(Dao)或业务层(Service)保障一系列的数据库同成功同失败
直接用案例来说明:

5.2 Spring 事务使用
一、搭建基础业务
1. SQL 准备
sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `tbl_account`;
CREATE TABLE `tbl_account` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`money` double DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
INSERT INTO `tbl_account` VALUES (1, 'Tom', 1000);
INSERT INTO `tbl_account` VALUES (2, 'Jerray', 1000);
SET FOREIGN_KEY_CHECKS = 1;
2. Dao 层
根据对应的表准备好 AccountMapper 接口:
java
@Mapper
@Repository
public interface AccountMapper {
// 减钱
@Update("update tbl_account set money = money - #{money} where name = #{name}")
void outMoney(@Param("name") String name, @Param("money") Double money);
// 加钱
@Update("update tbl_account set money = money + #{money} where name = #{name}")
void inMoney(@Param("name") String name, @Param("money") Double money);
}
3. Service 层
java
@Service
public class TransferServiceImpl implements TransferService {
@Autowired
private AccountMapper accountMapper;
/**
* 转钱方法
* @param out 转出者
* @param in 转入者
* @param money 金额大小
*/
public void transfer(String out, String in, Double money) {
accountMapper.outMoney(out, money);
accountMapper.inMoney(in, money);
}
}
4. 正常测试(Tom 向 Jerray 转 50 元)
java
public static void main(String[] args) {
ApplicationContext tcx = new AnnotationConfigApplicationContext(SpringConfig.class);
TransferService transferService = (TransferService) tcx.getBean("transferServiceImpl");
transferService.transfer("Tom", "Jerray", 50.0);
}

可以看到直接执行完成转账操作。但这只是正常的情况。
二、异常情况
我们稍微修改一下 Service 的业务代码,来模拟转钱后发生异常的情况:
java
@Service
public class TransferServiceImpl implements TransferService {
@Autowired
private AccountMapper accountMapper;
/**
* 转钱方法
*/
public void transfer(String out, String in, Double money) {
accountMapper.outMoney(out, money);
int i = 1 / 0; // 模拟异常
accountMapper.inMoney(in, money);
}
}
在减钱和加钱中间加了个异常情况。因为任何常数都不能除 0,这一定是异常情况:

可以看到,这一步中 Tom 的钱被减了,但是 Jerray 的钱没有增加。因为 RuntimeException 导致 JVM 终止了 ,所以执行不到加钱的操作,这违背了原子性的同成功同失败的理念。
所以需要增加事务来保障,这个事务就是 Spring 事务。
三、Spring 事务配置
1. 加载事务管理器到 IoC 容器
Spring 提供的接口叫 PlatformTransactionManager,里面有两个方法来进行 rollback 和 commit 操作。不过这些都不需要我们进行调用,只需要加载这个 Bean 即可:
java
// 将数据源引用参数传递进去
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
// 创建事务对象
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
// 将数据源交给Spring事务
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
2. 开启 Spring 事务驱动
在 Spring 的主配置类中设置开启事务才能使用事务的功能:
java
@Configuration
@EnableTransactionManagement
@ComponentScan("com.dongmianmao")
@PropertySource("classpath:jdbc.properties")
public class SpringConfig {
}
@EnableTransactionManagement --- 设置 Spring 事务驱动开启。
3. 使用 @Transactional 注解
在完成上述操作后,Spring 提供了一个注解 @Transactional,只需要在 Service 层或者 Dao 层需要进行事务管理的接口方法上使用就得了。一般用在 Service 层:
java
@Service
public class TransferServiceImpl implements TransferService {
@Autowired
private AccountMapper accountMapper;
@Transactional
public void transfer(String out, String in, Double money) {
accountMapper.outMoney(out, money);
int i = 1 / 0;
accountMapper.inMoney(in, money);
}
}
4. 测试
java
public class App {
public static void main(String[] args) {
ApplicationContext tcx = new AnnotationConfigApplicationContext(SpringConfig.class);
TransferService transferService = (TransferService) tcx.getBean("transferServiceImpl");
transferService.transfer("Tom", "Jerray", 50.0);
}
}
执行后意料之中报错:

看一下数据库表,可以看到当转账过程中出现错误就不会提交这个事务,保证了事务的同成功同失败:

这样就完成了 Spring 事务的使用。
四、底层实现原理粗略了解
@Transactional 注解底层实现
这个接口其实本质就跟 MyBatis 中的 SqlSession 的 rollback 和 commit 方法一样,出了异常就直接 rollback:
java
Connection conn = dataSource.getConnection(); // 获取连接
try {
conn.setAutoCommit(false); // 手动开启事务
// 执行SQL
conn.commit(); // 提交事务
} catch (Exception e) {
conn.rollback(); // 回滚事务
} finally {
conn.close();
}
事务管理器:

5.3 事务角色与原理


Spring 事务管理中有两个关键角色:
- 事务管理器(PlatformTransactionManager) --- 负责事务的创建、提交、回滚等核心操作
- 事务定义(TransactionDefinition) --- 定义事务的属性,如传播行为、隔离级别、超时、只读等
两者配合完成完整的事务管理功能。开发者日常使用中,只需关注 @Transactional 注解的属性配置即可。
总结
Spring Framework 的学习路线可以概括为:
- IoC / DI(控制反转 / 依赖注入)--- Spring 的核心基石,理解容器管理 Bean 的思想
- 注解开发 --- 简化配置,提升开发效率,是实际项目中的主流开发方式
- 整合第三方技术 --- Spring 作为粘合剂,整合 MyBatis、JUnit 等框架
- AOP --- 面向切面编程,在不改变原有代码的情况下增强功能
- 事务管理 --- 基于 AOP 实现,保障数据一致性
对于初学者来说,推荐先理解 XML 方式的原理,再过渡到注解开发方式,这样对底层的理解会更加扎实。