Spring-全面详解(学习总结)

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 --- 引用名,后续通过它获取 Bean
  • class --- 类的全限定路径

步骤三:创建测试类

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 方法。配置也是按照这个流程:

  1. 先将实例工厂交给 IoC 容器管理
  2. 指定一个 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() 替代 init
  • DisposableBean 接口 → 实现 destroy() 替代 destroy

实现这两个接口就可以不用配置 XML 了。

IoC 容器关闭方式

需要手动关闭容器才能触发 Bean 的销毁方法(ClassPathXmlApplicationContext 提供了 close() 方法,或者使用 registerShutdownHook() 注册钩子)。


三、注解开发

3.1 Spring 总结回顾

在学习注解开发之前,先回顾 XML 方式的核心知识点。

容器相关:

Bean 相关:

重点知道 autowirelazy-init

  • autowire --- 自动装配,有 byTypebyName
  • lazy-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 的两个生命周期 --- initdestroy --- 都是在 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;
    }
}

这里主要理解的是:

  1. 配置 MyBatis 的 SqlSessionFactoryBean 的时候,使用的引用型注入是放在形参位置的,然后 Spring 会自己去 IoC 中根据类型去寻找这个类并注入
  2. MapperScannerConfigurer 这个类是 MyBatis 用来扫描 Mapper 接口的一个 Bean,配置好路径后,它会给对应路径的 Mapper 接口去生成代理类 并加载进入 IoC 容器 --- 所以能够让对应的 Mapper 接口能够通过 byTypebyName 的方式去进行注入 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 的三要素:

  1. 对哪些内容做增强(切入点)
  2. 在什么时候进行增强(开始前、结束后)
  3. 增强什么内容
场景

现如今有一个 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 的核心流程:

  1. Spring 容器启动时,扫描所有切面类
  2. 根据切入点表达式匹配目标方法
  3. 为目标方法所在的 Bean 创建代理对象
  4. 调用目标方法时,实际调用的是代理对象,代理对象在执行目标方法前后会调用相应的通知方法

4.4 切入点表达式

切入点:要进行增强的方法。

切入点表达式:要进行增强的方法描述方式。

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

示例:

java 复制代码
@Pointcut("execution(public User com.dongmianmao.dao.UserDao.findUserById(int))")
  • 动作关键字:描述切入点的行为动作,例如 execution 表示执行到指定切入点(固定写法)
  • 访问修饰符:publicprivate 等,可以省略
  • 返回值
  • 包名
  • 类/接口名
  • 方法名
  • 参数
  • 异常名:方法定义中抛出指定异常,可以省略

记忆方式:

复制代码
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 的类/接口,如 UserServiceAccountService,表示的是对一个群体的类进行增强
  • 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,里面有两个方法来进行 rollbackcommit 操作。不过这些都不需要我们进行调用,只需要加载这个 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 中的 SqlSessionrollbackcommit 方法一样,出了异常就直接 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 的学习路线可以概括为:

  1. IoC / DI(控制反转 / 依赖注入)--- Spring 的核心基石,理解容器管理 Bean 的思想
  2. 注解开发 --- 简化配置,提升开发效率,是实际项目中的主流开发方式
  3. 整合第三方技术 --- Spring 作为粘合剂,整合 MyBatis、JUnit 等框架
  4. AOP --- 面向切面编程,在不改变原有代码的情况下增强功能
  5. 事务管理 --- 基于 AOP 实现,保障数据一致性

对于初学者来说,推荐先理解 XML 方式的原理,再过渡到注解开发方式,这样对底层的理解会更加扎实。

相关推荐
Volunteer Technology1 小时前
Spring AI MCP案例
java·开发语言·数据库
紫琪软件工作室1 小时前
SpringBoot Java邮件发送工具类
java·spring boot·spring
神明9311 小时前
CSS 背景图滑动切换:纯 CSS 实现右进左出轮播效果
jvm·数据库·python
星栈1 小时前
投影挂了怎么办?我的 CQRS 三层容错方案
数据库·后端·开源
东风破1371 小时前
DM8数据库读写分离集群安装部署
数据库·oracle·dm达梦数据库
IT研究所1 小时前
从系统选型到ITR智能服务流落地的关键一步
大数据·运维·服务器·数据库·人工智能·科技·自动化
wang3zc1 小时前
CSS如何让最后一行项目左对齐_利用flex布局配合伪元素空项填充
jvm·数据库·python
2303_821287381 小时前
如何用 Chrome 的 Rendering 面板监控页面的重排频率
jvm·数据库·python
m0_631529821 小时前
C#怎么解析XML文件 C#如何用XmlDocument和LINQ to XML读写XML数据【基础】
jvm·数据库·python