封面画师:T5-茨舞(微博)     封面ID:78990961_p0

参考视频:孙哥说Spring5 全部更新完毕 完整笔记、代码看置顶评论链接~学不会Spring? 因为你没找对人

1. 工厂模式与代码耦合

1.1 耦合的再理解

在【从 0 开始的 Spring 5.x 学习】一文中,我们也讲到了程序的耦合,我们是使用 Java 操作 MySQL 数据库来讲解的,或许文字描述过多难以抓住重点,那究竟什么是耦合呢?

简单来说:耦合就是指 代码间的强关联关系,一方的改变会影响到另一方

比如在 UserController 类中极有可能存在以下代码:

1
2
3
4
public class UserController {
UserService userService = new UserServiceImpl();
// ...
}

将接口的实现类硬编码在程序中。如果有一天更换这个实现类,那么就需要修改 UserController 类中的代码,但是在实际生产环境中,更换某一接口的实现类往往会涉及到很多类,直接修改这些类是十分危险的,极不利于代码的维护,而这就是程序之间的耦合。

1.2 反射与工厂模式

我们知道创建对象除了使用 new 关键字外,还可以使用反射进行创建,比如下面两种创建对象的方式就是等价的:

1
2
3
4
5
// 使用 new 关键字
UserService userService = new UserServiceImpl();
// 使用反射
Class clazz = Class.forName("indi.mofan.service.UserServiceImpl");
UserService userService = (UserService)clazz.newInstance();

因此在 UserController 类中,我们就可以使用第二种方式利用反射来创建对象,但是这样耦合就消除了吗?

其实并没有。因为如果我们更换 UserService 接口的实现类,Class.forName() 方法中的参数还是要发生改变,这里依然存在着耦合。

也就是说我们现在要消除耦合就是要消除 Class.forName() 方法的参数耦合。我们可以使用一个“小”配置文件来解决。

比如定义一个 bean.properties 文件来保存实现类的全路径信息,在 properties 文件中,要求 key 一直不变,其 value 为对应的实现类全路径信息。

然后使用工厂模式解析 properties 文件,并提供根据 key 获取对应 value 的方法,这样的话就可以在 UserController 类中利用反射、工厂模式来创建 UserService 实现类对象。

如果需要修改实现类,那么只需要修改 properties 文件中某个 key 对应的 value 即可,无需再更改其他代码,这样就降低了程序间的耦合。

具体实现可以参考【从 0 开始的 Spring 5.x 学习】一文中的讲解。

2. Spring 工厂初理解

2.1 ApplicationContext

Spring 提供了工厂 ApplicationContext 用于对象的创建,以降低程序之间的耦合。

ApplicationContext 是一个接口,接口可以屏蔽实现的差异。Spring 主要为我们提供了两种类型的工厂:

1、在非 Web 环境下:ClassPathXmlApplicationContext

2、在 Web 环境下:XmlWebApplicationContext(需要导入 spring-webmvc 依赖)

注意: ApplicationContext 工厂是一个重量级资源,其工厂对象会占用大量的内存,因此不会频繁地创建工厂对象,而是一个应用只会创建一个工厂对象,正因如此,这个工厂一定是线程安全的,可以在多线程环境下被并发访问。

2.2 工厂常用方法

所用依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>

涉及到的非核心类

1
2
package indi.mofan.domain;
public class Person { }
1
2
3
4
5
6
7
8
9
10
11
package indi.mofan.domain;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class User {
private String username;
private String password;
}

工厂常用方法测试

在 Spring 的配置文件中存在以下配置:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="person" class="indi.mofan.domain.Person"/>

<bean id="user" class="indi.mofan.domain.User"/>

</beans>

利用上述配置信息对常用方法进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Test
public void testApplicationContestMethod() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
Person p1 = (Person) applicationContext.getBean("person");
System.out.println(p1);

Person p2 = applicationContext.getBean("person", Person.class);
System.out.println(p2);

// 此时配置文件中只能有一个 <bean> 标签的 class 是 Person 类型
Person p3 = applicationContext.getBean(Person.class);
System.out.println(p3);

// 获取 Spring 配置文件中所有 <bean> 标签的 id 值
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
System.out.println("beanDefinitionName = " + beanDefinitionName);
}

// 根据类型获得 Spring 配置文件中对应的 id 值
String[] beanNamesForType = applicationContext.getBeanNamesForType(Person.class);
for (String beanName : beanNamesForType) {
System.out.println("beanName = " + beanName);
}

// 用于判断 Spring 配置文件中是否存在指定 id 值的 Bean
boolean containPerson = applicationContext.containsBeanDefinition("person");
System.out.println("id 为 person 的 Bean 是存在的?" + containPerson);
boolean notContainPeople = applicationContext.containsBeanDefinition("people");
System.out.println("id 为 people 的 Bean 是存在的?" + notContainPeople);

// 用于判断 Spring 配置文件中是否存在指定 id 值的 Bean
containPerson = applicationContext.containsBean("person");
System.out.println("id 为 person 的 Bean 是存在的?" + containPerson);
notContainPeople = applicationContext.containsBean("people");
System.out.println("id 为 people 的 Bean 是存在的?" + notContainPeople);
}

运行测试方法后,控制台打印出:

indi.mofan.domain.Person@5bfbf16f
indi.mofan.domain.Person@5bfbf16f
indi.mofan.domain.Person@5bfbf16f
beanDefinitionName = person
beanDefinitionName = user
beanName = person
id 为 person 的 Bean 是存在的?true
id 为 people 的 Bean 是存在的?false
id 为 person 的 Bean 是存在的?true
id 为 people 的 Bean 是存在的?false

2.3 配置文件细节分析

细节分析一

在上述配置文件中有这样一段配置:

1
<bean id="person" class="indi.mofan.domain.Person"/>

那么我们是否可以只配置 class,而不配置 id 呢?

答案是可以的。如果我们只配置 id,Spring 会使用其自身的算法生成一个 id,比如:

indi.mofan.domain.Person#0

如果这个 Bean 只使用一次,那么在配置时可以省略 id 的值;如果这个 Bean 会使用多次,或者会被其他 Bean 引用,则还是需要设置 id 值。

细节分析二

在配置文件的 <bean> 标签中还有一个名为 name 的属性,使用这个属性可以为 Bean 设置别名。

如果我们将设置的 id 值理解为 Bean 的大名,那么 name 指定的就是 Bean 的小名。比如:

1
<bean id="person" name="p" class="indi.mofan.domain.Person"/>

可以这样使用:

1
2
3
4
5
6
7
@Test
public void testName() {
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("applicationContext.xml");
Person p1 = (Person) applicationContext.getBean("p");
System.out.println(p1); // 依旧能够顺利打印出信息
}

我们发现使用 ApplicationContext#getBean() 方法既可以传入 Bean 的 id,也可以传入 Bean 的 name,这样的话 idname 的使用方式很类似,那它们有什么区别呢?

1、name 指定的别名可以定义多个,但 id 只能有一个值,比如:

1
<bean id="person" name="p,p1,p2" class="indi.mofan.domain.Person"/>

2、使用 ApplicationContext#containsBeanDefinition() 方法和 ApplicationContext#containsBean() 方法可以判断是否存在指定的 Bean 对象。当配置文件中显式定义了 id 时,containsBeanDefinition() 方法只判断 id,不会判断 name;如果没有显式定义 id,那么此方法也可以判断 name;而对 containsBean() 方法而言,无论是否显式定义,都可以判断 id 和 name。

3、在很久以前,XML 文件规定 id 属性的值必须以字母开头而不能以特殊符号开头(比如 /),内部可以包含字母、数字、下划线和连字符,而对 name 属性值命名没有要求。因此在那时,name 属性可以应用在特殊命名的场景下,但 XML 发展至今,对 id 属性值的限制已经不复存在了(甚至可以使用 emoji 表情作为 id 值)。

2.4 Spring 工厂阶段性理解

Spring 工厂创建对象的流程(简易版,阶段性理解)

1、Spring 通过 ClassPathXmlApplicationContext 工厂读取配置文件 applicationContext.xml;

2、Spring 获得 <bean> 标签的相关信息,比如 id 和 class 信息,然后通过反射创建对应的对象;

3、Spring 利用反射创建对象时底层也会调用对象自己的构造方法,就算这个构造方式是私有的。

在未来开发过程中,所有的对象都要交给 Spring 工厂创建吗

理论上是的,但也有特例。实体对象(entity)是不会交给 Spring 创建的,它是由持久层创建进行创建的。

3. 整合日志框架

Spring 与日志框架整合后,可以在控制台输出 Spring 框架运行过程中的一些重要信息,以便了解 Spring 框架的运行过程和调试。

在 Spring 5.x 默认整合了 logback 和 log4j2 两个日志框架,在这里我们不使用它们,使用 log4j 日志框架。

首先引入相关依赖:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 日志门面,取消Spring默认的日志 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<!-- log4j 依赖 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

然后在 resources 目录下创建 log4j.properties 文件,并追加以下内容:

1
2
3
4
5
6
7
# 配置根
log4j.rootLogger = debug,console
# 日志输出到控制台显示
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Target=System.out
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

4. 依赖注入补充

4.1 Set 注入

所谓注入,就是通过 Spring 的配置文件,为成员变量赋值。

依赖注入分为 Set 注入和构造注入。Set 注入就是 Spring 调用属性的 Set 方法,通过配置文件为成员变量赋值;构造注入就是 Spring 调用类的构造方法,通过配置文件为成员变量赋值。

简单来说:Spring 通过底层调用对象属性对应的 Set 方法,完成成员变量的赋值,这种方式我们也称之为 Set 注入。

Set 注入分为 JDK 内置类型的注入和用户自定义类型的注入。

在此可以回忆下:JDK 中 8 种基本类型、String 类型、数组类型、Set 集合、List 集合、Map 集合、Properties 集合和用户自定义类型应该怎么使用 XML 进行 Set 注入。

具体注入方式可以参考【从 0 开始的 Spring 5.x 学习】一文。

4.2 命名空间 P

命名空间 P 是 Set 注入方式的一种简化。

有这样两个用于测试的实体类:

1
2
3
4
5
@Getter
@Setter
public class Person {
private String name;
}
1
2
3
4
5
6
7
8
@Getter
@Setter
public class User {
private String username;
private String password;

private Person person;
}

在配置文件中对这两个对象进行依赖注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!--注意导入命名空间 P 的约束-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="person" name="p" class="indi.mofan.domain.Person">
<property name="name" value="yang"/>
</bean>

<bean id="user" class="indi.mofan.domain.User" p:username="mofan" p:person-ref="person"/>

</beans>

测试代码:

1
2
3
4
5
6
7
8
@Test
public void testNameSpaceP() {
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("applicationContext.xml");
User user = (User) applicationContext.getBean("user");
Assert.assertEquals("mofan", user.getUsername());
Assert.assertEquals("yang", user.getPerson().getName());
}

运行测试方法后,测试通过。

4.3 构造方法注入

使用构造方法注入时,需要使用到 <constructor-arg> 标签,具体注入方式可以参考【从 0 开始的 Spring 5.x 学习】一文。

构造方法重载

1、当构造方法参数个数不同时,通过控制 <constructor-arg> 标签的数量进行区分;

2、当构造方法参数个数相同时,通过在 <constructor-arg> 标签中引入 type 属性进行类型的区分。

4.4 注入的选择

按大类来划分,依赖注入有两种,分别是:Set 注入和构造方法注入。如果在实际开发中需要使用配置文件进行依赖注入,那么应该选那种呢?

通常会选择 Set 注入。

原因如下:

1、使用 Set 注入更加简单,使用构造方法注入还需要考虑构造方法重载的情况;

2、在 Spring 内部也大量采用了 Set 注入的方式进行依赖注入;

3、我们自己在编写代码时,对属性的赋值也通常选择使用 Set 方法,而不是构造方法。

4.5 控制反转与依赖注入

控制反转(IoC,Inverse of Control)

控制,指的是对于成员变量的控制权。

当不使用 Spring 时,会直接在代码中完成对成员变量的赋值,这时对成员变量复制的控制权在代码,这将造成一定的耦合。

在使用了 Spring 后,对于成员变量赋值的控制权由代码交给 Spring 配置文件和 Spring 工厂,做到了解耦合,而这就是控制反转。控制反转的底层实现是工厂设计模式。

依赖注入(DI,Dependency Injection)

注入:通过 Spring 的工程及配置文件,为对象(Bean,组件)的成员变量赋值。

依赖注入:当一个类需要另一个类时,就以为着依赖,就可以把另一个类作为本类的成员变量,最终通过 Spring 配置文件进行注入(赋值),以解耦合。

5. Spring 工厂创建复杂对象

5.1 什么是复杂对象

简单对象:可以直接通过 new 关键字进行创建的对象,比如 UserService、Person 等等。

复杂对象:不直接通过 new 关键字进行创建的对象,比如 JDBC 中 Connection 对象、MyBatis 中 SqlSessionFactory 对象等等。

5.2 使用 FactoryBean 接口创建

1、使用 FactoryBean 接口创建复杂对象,首先需要使用类实现这个接口并重写其中的方法。

FactoryBean 接口的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface FactoryBean<T> {
String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

@Nullable
T getObject() throws Exception;

@Nullable
Class<?> getObjectType();

default boolean isSingleton() {
return true;
}
}

其中,getObject() 方法用于书写创建复杂对象的代码,并把复杂对象作为方法的返回值返回;getObjectType() 方法用于返回所创建复杂对象的 Class 对象;isSingleton() 方法用于设置创建的复杂对象是否是单例的。

假设新建 ConnectionFactoryBean 类并实现 FactoryBean 接口,重写三个方法用于创建 JDBC 中的 Connection 对象。

2、在 Spring 配置文件中进行如下配置:

1
<bean id = "conn" class = "indi.mofan.factorybean.connectionFactoryBean"/>

需要注意的是,如果创建的是简单对象,使用 ctx.getBean("person") 获得的是 Person,但这里创建的是复杂对象,使用 ctx.getBean("conn") 获取的不再是 ConnectionFactoryBean 这个类的对象,由于该类实现了 FactoryBean 接口,因此获取的是 Connection 对象。

小总结:在 Spring 配置文件,如果 class 中指定的类实现了 FactoryBean 接口,那么通过 id 获得的是这个类所创建的复杂对象。

FactoryBean 细节分析

细节一:如果就想获得 FactoryBean 类型的对象,该怎么办呢?对上述案例而言,可以使用 ctx.getBean("&conn") 的方式来获取 ConnectionFactoryBean 对象。

细节二:通过重写 FactoryBean 接口中的 isSingleton() 方法可以控制创建的对象是否是单例的。

细节三:如果创建的复杂对象与某些信息存在依赖的关系,那么可以把这些信息设置为成员变量,然后在配置文件中使用 Set 注入的方式进行依赖注入。

FactoryBean 的简易版实现原理

当我们使用 ctx.getBean("conn") 来获取 Connection 对象时,Spring 框架会先读取配置文件,获取 id 为 conn 的 <bean/> 标签的相关信息,并判断其 class 是否是 FactroyBean 接口的子类,如果是,就会调用其重写的 getObject() 方法来获取创建的复杂对象。

5.3 使用实例工厂创建

既然可以使用 FactoryBean 来创建复杂对象了,为什么还要使用实例工厂来创建呢?

使用实例工厂创建可以避免 Spring 框架的侵入,还可以整合遗留系统。

假设现在有一个名为 ConnectionFactory 的类,在这个类中存在一个名为 getConnection() 的方法,使用这个方法可以获取 Connection 对象,此时应该怎么做呢?

只需要在 Spring 配置文件中书写以下配置:

1
2
<bean id = "connFactory" class = "indi.mofan.factorybean.ConnectionFactory"/>
<bean id = "conn" factroy-bean = "connFactory" factory-method = "getConnection" />

最后使用 ctx.getBean("conn") 即可获取到 Connection 对象。

5.4 使用静态工厂创建

使用静态工厂创建和使用实例工厂创建的原因是一样的,只不过使用静态工厂创建时,创建的方法是 静态的

假设现在有一个名为 StaticConnectionFactory 的类,在这个类中存在一个名为 getConnection()静态 方法,要使用这个方法获取 Connection 对象,只需要在 Spring 配置文件中书写以下配置:

1
<bean id = "conn" class = "indi.mofan.factorybean.StaticConnectionFactory" factory-method = "getConnection"/>

同样的最后使用 ctx.getBean("conn") 即可获取到 Connection 对象。

6. 对象的生命周期

6.1 对象生命周期概述

什么是对象的生命周期

对象的生命周期指的是一个对象的创建、存活和消亡的完整过程。

为什么要学习对象的生命周期

因为面试官 TM 的要问!

因为在 Spring 中,将由 Spring 负责对象的创建、存活和消亡,了解生命周期,有利于我们使用好 Spring 为我们创建的对象。

6.2 创建阶段

创建阶段指的是 Spring 工厂何时创建对象。

当需要创建对象的 scope 指定为 singleton (若未显式指定,默认为 singleton)时,当 Spring 工厂创建的同时,就会创建对象;当 scope 指定为 prototype 时,Spring 工厂会在创建对象的同时创建对象。

如果想要 Spring 工厂在创建对象时才创建 scope 为 singleton 的对象,可以在 XML 配置文件中增加 lazy-init = true 的配置。比如:

1
<bean id="product" class="indi.mofan.life.Product" lazy-init="true"/>

6.3 初始化阶段

初始化阶段指的是 Spring 工厂在创建完对象后,调用对象的初始化方法,完成对应的初始化操作。

初始化方法是由程序员根据需求提供,然后由 Spring 工厂进行调用,最终完成初始化操作。

第一种提供初始化方法的方式

如果需要程序员提供初始化方法,可以让被创建的对象实现 InitializingBean 接口,并重写 afterPropertiesSet() 方法。比如:

1
2
3
4
5
6
7
8
9
10
public class Product implements InitializingBean {
public Product() {
System.out.println("Product.Product");
}

@Override
public void afterPropertiesSet() throws Exception {
System.out.println("Product.afterPropertiesSet");
}
}

配置文件:

1
<bean id="product" class="indi.mofan.life.Product"/>

测试方法:

1
2
3
4
5
@Test
public void testObjectLife() {
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("applicationContext.xml");
}

运行测试方法后可以在控制台看到:

Product.Product
Product.afterPropertiesSet

第二种提供初始化方法的方式

第一种提供初始化方法的方式虽然可以达成目的,但是那种方式与 Spring 有较高的耦合。

我们可以在被创建的对象所在类中自定义一个 非静态 void 返回值 的方法,如:

1
2
3
4
5
6
7
8
9
10
public class Product {
public Product() {
System.out.println("Product.Product");
}


public void myInitMethod() {
System.out.println("Product.myInitMethod");
}
}

同时修改配置文件,让 Spring 知道调用哪个方法完成初始化工作:

1
<bean id="product" class="indi.mofan.life.Product" init-method="myInitMethod"/>

测试方法不变,再次运行测试方法依旧可以在控制台看到:

Product.Product
Product.myInitMethod

细节分析

1、如果需要被创建的对象既实现了 InitializingBean 接口,又提供了普通的初始化方法,Spring 会先执行重写的 InitializingBean 接口中的 afterPropertiesSet() 方法,然后再执行普通的初始化方法。

2、依赖注入一定是发生在初始化操作之前的,这也是为什么 InitializingBean 接口中提供的方法叫作 afterPropertiesSet 的原因。

3、什么叫初始化操作?比如数据库、IO、网络等资源的初始化就可以使用使用初始化操作,但在实际开发过程中对初始化操作的应用是比较少的。

6.4 销毁阶段

销毁阶段指的是 Spring 在销毁对象前,会调用对象的销毁方法,完成销毁操作。

那 Spring 什么时候销毁所创建的对象呢?Spring 会在工厂关闭前销毁所创建的对象。

销毁方法是由程序员根据自己的需求,定义销毁方法,完成销毁操作。与初始化方法一样,定义的销毁方法将由 Spring 工厂进行调用。

第一种提供销毁方法的方式

让被创建的对象实现 DisposableBean 接口,然后重写 destroy() 方法。比如:

1
2
3
4
5
6
public class Product implements DisposableBean {
@Override
public void destroy() throws Exception {
System.out.println("Product.destroy");
}
}

在进行测试时应当显式关闭工厂,如:

1
2
3
4
5
6
7
8
@Test
public void testObjectLife() {
ClassPathXmlApplicationContext applicationContext =
new ClassPathXmlApplicationContext("applicationContext.xml");
Product product = applicationContext.getBean("product", Product.class);
// 显式关闭工厂
applicationContext.close();
}

第二种提供销毁方法的方式

在被创建的对象所在类中自定义一个 非静态 void 返回值 的方法,如:

1
2
3
public void myDestroyMethod() {
System.out.println("Product.myDestroyMethod");
}

同样需要在配置文件中指定:

1
<bean id="product" class="indi.mofan.life.Product"  destroy-method="myDestroyMethod"/>

测试方法与“第一种提供销毁方法的方式”中的一样。

细节分析

1、与初始化阶段一样,Spring 会先执行重写的 DisposableBean 接口中的 destroy() 方法,然后再执行普通的销毁方法。

2、销毁方法的操作只适用于 scope 为 singleton 的对象,即单例对象。而对于 scope 为 prototype 的多例对象,当对象长时间不用且没有别的对象引用时,将由 Java 的垃圾回收器回收。

3、什么叫做销毁操作?销毁操作主要指的是资源的释放操作。

7. 自定义类型转换器

7.1 自定义类型转换器

Spring 会通过类型转换器把配置文件中字符串类型的数据转换成对象中成员变量对应的数据类型,进而完成了注入。

但并不是说 Spring 为每种类型都预置了类型转换器,当 Spring 内部没有预置特定类型转换器,而程序员在应用中又需要使用时,就需要程序员自己定义类型转换器。

如果需要自定义类型转换器,需要:

1、实现 Converter 接口;

2、在 Spring 配置文件进行注册。

自定义类型转换器

比如将 String 类型 yyyy-MM-dd 格式转换为 Date 类型,可以这样自定义类型转换器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyDateConverter implements Converter<String, Date> {

@Override
public Date convert(String s) {
Date date = null;
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
try {
date = format.parse(s);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}

编写好自定义类型转换器后,需要将其交给 Spring 工厂,由 Spring 工厂创建,然后还需要进行类型转换器的注册,告知 Spring 框架,我们自定义的 MyDateConverter 类是一个类型转换器。

比如:

1
2
3
4
5
6
7
8
9
10
11
<!--  Spring 创建 类型对象  -->
<bean id="myDateConverter" class="indi.mofan.converter.MyDateConverter"/>

<!-- 用于注册类型转换器 -->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<ref bean="myDateConverter"/>
</set>
</property>
</bean>

7.2 自定义类型转换器细节

1、MyDateConverter 中的日期格式可以通过依赖注入的方式由配置文件完成赋值。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Getter
@Setter
public class MyDateConverter implements Converter<String, Date> {

private String pattern;

@Override
public Date convert(String s) {
Date date = null;
SimpleDateFormat format = new SimpleDateFormat(pattern);
try {
date = format.parse(s);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}
1
2
3
<bean id="myDateConverter" class="indi.mofan.converter.MyDateConverter">
<property name="pattern" value="yyyy-MM-dd"/>
</bean>

2、为 ConversionServiceFactoryBean 定义 id 属性时,值必须为 conversionService

3、Spring 框架内置了日期类型的转换器,只不过其格式为 yyyy/MM/dd

8. 后置处理 Bean

8.1 BeanPostProcessor 接口

后置处理 Bean 指的是名为 BeanPostProcessor 的接口,这个接口可以对 Spring 工厂所创建的对象进行再加工。

1
2
3
4
5
6
7
8
9
10
11
public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}

@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}

在这个接口中有两个默认方法,当 Spring 工厂使用反射调用对象的构造方法创建好对象并完成依赖注入后,就会调用 postProcessBeforeInitialization() 方法,对所创建的对象进行第一次加工。加工完成后进行对象的初始化操作,而在初始化操作完成后就会调用 postProcessAfterInitialization() 方法,对所创建的对象进行第二次加工。

这两个方法的参数都是一样的,第一个参数表示所创建的 bean 对象,第二个参数表示创建 bean 的名称(对应配置文件中 <bean> 标签的 id 属性值)。他们的返回值也是一样的,需要将加工后的对象进行返回。

在实战中,很少进行 bean 的初始化操作,因此在没有进行 bean 初始化操作的时候,接口内的两个默认方法的作用是一样的,但更习惯于实现 After 方法。

8.2 后置处理 Bean 编码

在自定义后置处理 Bean 时,需要实现 BeanPostProcessor 接口,然后再 Spring 的配置文件中进行配置即可。

需要注意的是,BeanPostProcessor 会对 Spring 工厂创建的所有对象进行加工。

1
2
3
4
5
6
7
8
9
10
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof Category) {
Category category = (Category) bean;
category.setName("Mobile Phone");
}
return bean;
}
}
1
2
3
4
5
6
<bean id="c" class="indi.mofan.beanpost.Category">
<property name="id" value="123"/>
<property name="name" value="Computer"/>
</bean>

<bean id="myBeanPostProcessor" class="indi.mofan.beanpost.MyBeanPostProcessor"/>
1
2
3
4
5
6
@Test
public void testBeanPostProcessor() {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
Category category = applicationContext.getBean("c", Category.class);
Assert.assertEquals("Mobile Phone", category.getName());
}

9. Spring 动态代理

9.1 Spring 动态代理开发

使用 Spring 动态代理可以为原始类(目标类)增加额外功能,以便利于原始类(目标类)的维护。

导入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.8</version>
</dependency>

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

Spring 动态代理的开发步骤:

1、创建原始对象(目标对象):

1
2
3
4
5
6
7
8
9
10
11
12
public class UserServiceImpl implements UserService {
@Override
public void register(User user) {
System.out.println("UserServiceImpl.register 业务运算 + DAO 调用");
}

@Override
public boolean login(String username, String password) {
System.out.println("UserServiceImpl.login");
return false;
}
}
1
<bean id="userService" class="indi.mofan.proxy.UserServiceImpl"/>

2、自定义 MethodBeforeAdvice 接口的实现类,并在其中添加额外功能,它会在原始方法运行之前执行:

1
2
3
4
5
6
7
8
9
public class Before implements MethodBeforeAdvice {
/**
* 把运行在原始方法之前运行的额外功能写在 before 方法内
*/
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("----- method before advice log -----");
}
}
1
<bean id="before" class="indi.mofan.dynamic.Before"/>

3、定义切入点(额外功能加入的位置)与组装,由程序员根据自己的需求,决定额外功能加给哪个原始方法:

1
2
3
4
5
6
7
<aop:config>
<!-- 所有方法都作为切入点加入额外功能 -->
<aop:pointcut id="pc" expression="execution(* *(..))"/>

<!-- 组装 -->
<aop:advisor advice-ref="before" pointcut-ref="pc"/>
</aop:config>

4、测试调用:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testSpringDynamicProxy() {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext3.xml");
/*
* Spring 工厂通过原始对象的 id 值得到的是代理对象
* 获得代理对象后,可以通过声明接口类型,进行对象的存储
*/
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.login("mofan", "123456");
userService.register(new indi.mofan.proxy.User());
}

运行测试方法后,控制台打印出:

----- method before advice log -----
UserServiceImpl.login
----- method before advice log -----
UserServiceImpl.register 业务运算 +  DAO 调用

9.2 细节分析

1、Spring 创建的动态代理类在哪呢?

Spring 框架在运行时,通过动态字节码技术在 JVM 创建的,运行在 JVM 内部。 当程序结束后,会和 JVM 一起消失。

动态字节码技术:通过第三方动态字节码框架,在 JVM 中创建对应类的字节码进而创建对象。当虚拟机结束时,动态字节码也会跟着消失。

动态代理不需要定义类文件,都是 JVM 在运行过程中动态创建的,所以不会造成像静态代理那样的“类爆炸”,影响项目管理。

2、在额外功能不改变的情况下,创建其他目标类(原始类)的代理对象时,只需要指定目标(原始)对象即可。

3、使用动态代理可以增强额外功能的维护性。

4、Spring 对 AOP 的实现是利用动态代理生成代理对象,但并不是说会为所有被切入点函数的覆盖的类都生成代理对象,这些类还必须交由 Spring 管理。以切入点函数 @annotation 为例,它能够增强具有指定注解的方法,但并不是说只要被指定注解标记的方法都会被增强,这些方法的所在类还必须已经交由 Spring 管理。@Transactional 注解实现声明式事务管理也是利用了 AOP,但如果被标记的类或方法所在的类没有交由 Spring 管理,那么添加的 @Transactional 注解是无效的。

9.3 MethodBeforeAdvice

MethodBeforeAdvice 接口的作用:额外功能运行在原始方法之前,进行额外功能操作。

在 MethodBeforeAdvice 接口中,有这样的抽象方法:

1
void before(Method method, Object[] args, @Nullable Object target) throws Throwable;

第一个参数 method 表示给哪个原始方法增加额外功能。

第二个参数 args 表示原始方法的参数列表。

第三个参数 target 表示原始方法所在的原始对象。

9.4 MethodInterceptor

1
2
3
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation methodInvocation) throws Throwable;
}

MethodInterceptor 接口中存在一个 invoke() 的方法,合理使用此方法,可以使额外功能运行在原始方法之前、之后、前后、抛出异常时等场景。

参数 methodInvocation 表示增加了额外功能的那个原始方法,调用其 proceed() 方法就表示执行了原始方法,这时就可以在原始方法之前、之后等场景增加额外功能。proceed() 方法的返回值表示原始方法的返回值,如果原始方法的返回值类型是 voidproceed() 方法就返回 null

MethodInterceptor 接口的 invoke() 方法的返回值是 Object 类型的,它也表示原始方法的返回值,因此我们常常将 proceed() 方法的返回值也作为 invoke() 方法的返回值。当 invoke() 方法的返回值不使用 proceed() 方法的返回值时,可能会影响原始方法的返回值。

9.5 切入点详解

官方文档:Declaring a Pointcut

切入点决定额外功能加入的位置(方法)。

切入点表达式参考【从 0 开始的 Spring 5.x 学习】一文即可。

切入点函数

1、execution:功能最全的切入点函数,使用它可以完成方法切入点表达式、类切入点表达式和包切入点表达式。

2、args:主要用于方法参数的匹配。比如匹配两个参数都是 String 类型的方法,可以这样写:args(String, String)

3、within:主要用于类、包切入点表达式的匹配。比如想要匹配 UserServiceImpl 类中的所有方法,那么可以写成 within(*..UserServiceImpl),再比如想要匹配 indi.mofan.proxy 包下所有的方法,可以写成 within(indi.mofan.proxy..*)

4、@annotation:为具有特殊注解的方法添加额外功能。比如为被 @Log 标记的方法添加额外功能,可以写成 @annotation(indi.mofan.annotation.Log)

切入点函数的逻辑运算

1、and 与操作

假设需要匹配有且仅有两个 String 类型参数的 login() 方法,可以这样写:

execution(* login(String, String))
或者
execution(* login(..) and args(String, String))

注意: 与操作不能用于同类型的切入点函数。

2、or 或操作

假设需要匹配 register()login() 方法,可以这样写:

execution(* login(..)) or execution(* register(..))

9.6 再会 BeanPostProcessor

在 Spring AOP 中,如果对某个类进行了代理,依然可以在配置文件中通过配置原始类的 id 值和类路径获取到代理类,而这就是使用了 BeanPostProcessor 来完成的。

代码示例

1
2
3
4
5
public interface UserService {
void login(String name, String password);

void register(User user);
}
1
2
3
4
5
6
7
8
9
10
11
public class UserServiceImpl implements UserService{
@Override
public void login(String name, String password) {
System.out.println("UserServiceImpl.login");
}

@Override
public void register(User user) {
System.out.println("UserServiceImpl.register");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ProxyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), bean.getClass().getInterfaces(), (proxy, method, args) -> {
System.out.println("------ new Log ------");
return method.invoke(bean, args);
});
}
}
1
2
3
4
5
6
7
8
<bean id="userService" class="indi.mofan.factory.UserServiceImpl"/>

<!--
1. 实现 BeanPostProcessor 进行加工
2. 配置文件对 BeanPostProcessor 进行配置
-->

<bean id="proxyBeanPostProcessor" class="indi.mofan.factory.ProxyBeanPostProcessor"/>
1
2
3
4
5
6
7
@Test
public void testProxyBeanPostProcessor() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext4.xml");
indi.mofan.factory.UserService userService = applicationContext.getBean("userService", indi.mofan.factory.UserService.class);
userService.login("mofan", "666");
userService.register(new User());
}

运行测试方法后,控制台打印出:

------ new Log ------
UserServiceImpl.login
------ new Log ------
UserServiceImpl.register

9.7 JDK 与 CgLib 代理的切换

Spring AOP 底层有两种实现方式,一种是通过 JDK 动态代理,另一种是通过 CgLib 动态代理。

默认情况下,Spring AOP 底层使用 JDK 动态代理的方式。

如果要切换为 CgLib 动态代理,在基于注解 AOP 开发下:

1
<aop:aspectj-autoproxy proxy-target-class="true"/>

在传统的 AOP 开发下:

1
<aop:config proxy-target-class="true">

9.8 ApplicationContextAware

在实际业务中很可能会遇到以下场景:在同一个业务类中进行业务方法间的彼此调用,如果这个业务类中的所有方法都使用了 Spring AOP 进行代理,那么此时就只有最外层的方法加入了额外功能,内部调用的方法是没有加入额外功能的。比如:

1
2
3
4
5
6
7
8
9
10
11
12
public class PersonServiceImpl implements PersonService {
@Override
public void register(User user) {
// ...
login("mofan", "123456");
}

@Override
public void login(String name, String password) {
// ...
}
}

假设对 PersonServiceImpl 类使用了 AOP 进行代理,对其中的每个业务方法都添加了额外功能。在 register() 方法中调用了 login() 方法,执行 register() 方法时,只会看到 register() 方法被添加了额外功能,而其内部调用的 login() 方法是没有添加额外功能的。

原因也很简单:在 register() 方法内部调用 login() 方法是一种省略了 this 关键词的书写方式,其完整书写形式应该是 this.login()this 关键词指的是当前类,即原始类,而且代理类,因此在此调用的 login() 方法是没有添加额外功能的。

那在 new 一个 Spring 工厂(ApplicationContext),然后再 getBean() 拿到代理对象,最后调用 login() 方法不就行了吗?

可以是可以,但是 Spring 工厂是一个重量级工厂,不建议在程序中多次创建。为了满足这个要求,可以让原始类实现 ApplicationContextAware 接口并重写其中的方法即可:

1
2
3
public interface ApplicationContextAware extends Aware {
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class PersonServiceImpl implements PersonService, ApplicationContextAware {
private ApplicationContext ctx;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException{
this.ctx = applicationContext;
}

@Override
public void register(User user) {
// ...
UserService userService = ctx.getBean("userService", UserService.class);
userService.login("mofan", "123456");
}

@Override
public void login(String name, String password) {
// ...
}
}

10. 事务补充

10.1 与 MyBatis 整合细节

当 Spring 与 MyBatis 进行整合(使用了连接池 Druid)并且 xxxDao 交由 Spring 工厂进行创建后,仅仅调用 xxxDao 中的数据插入方法,不进行事务的提交也能将数据成功插入进数据库。 这是为什么呢?

这和连接对象 Connection 有关,因为连接对象控制着事务,而谁控制着连接对象,也就变向着控制着事务。本质上,控制着连接对象的是连接池。

仅仅使用 MyBatis 时,连接池对象由 MyBatis 提供,并由这个连接池创建连接对象。这个连接对象默认将 AutoCommit 设置为 false,手动控制事务,因此在操作完成后需要手动提交事务。

而使用连接池 Druid 和 Spring 进行整合后,连接对象由 Durid 创建。这个连接对象默认将 AutoCommit 设置为 true,保持自动控制事务,也就是在执行一条 SQL 语句后就会自动提交事务。这也是为什么只单单调用数据插入方法后可以不用再手动提交事务的原因。

注意: 在实际开发过程中,常常需要多条 SQL 语句一起成功或者一起失败,因此仍需要手动提交事务,而在 Spring 中通过事务控制解决了这个问题。

Spring 事务控制的具体内容参考【从 0 开始的 Spring 5.x 学习】一文。

10.2 失效的 @Transactional

在某些情况下,就算使用了 @Transactional 注解也不能完成事务控制,下面对这些情况进行梳理,避免在实际工作中犯这样的错误。

在非 public 修饰的方法上使用

代理对象内部类的 invoke()(JDK 的动态代理)和 intercept()(CGLib 的动态代理)方法会检查目标方法的修饰符是否是 public,如果不是 public 则不会获取 @Transactional 注解的属性配置信息,进而导致 @Transactional 注解失效。

public 修饰的方法上使用 @Transactional 注解并不会报错,只是没有任何效果。

事务传播行为 propagation 配置错误

Spring 中定义了七种事务传播行为,具体内容不在这里介绍,可以参考【从 0 开始的 Spring 5.x 学习】一文。

如果给 @Transactional 注解的 propagation 属性设置了以下三种事务传播行为,那么就有可能导致事务控制不符合原本的预期:

  • TransactionDefinition.PROPAGATION_SUPPORTS:外部存在事务,就融合到外部事务中;外部不存在事务,则不开启事务。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:外部存在事务,挂起外部事务;外部不存在事务,则不开启事务。
  • TransactionDefinition.PROPAGATION_NEVER:外部存在事务,抛出异常;外部不存在事务,则不开启事务。

子类 Service 重写父类 Service 中被 @Transactional 注解标记的方法,并且在重写的方法上也使用了 @Transactional 注解,还将 propagation 属性设置为 REQUIRES_NEW,重写的方法中也调用了父类方法。在执行子类中被重写的方法时,由于子类中的注解覆盖了父类的注解,Spring 不会在父类的方法中启动事务,父类方法被执行时,事务不会被启动,执行异常也不会回滚。

rollbackFor 配置错误

在默认情况下,抛出运行时异常(继承至 RuntimeException)和错误(继承至 Error)才会回滚事务,但抛出受检异常时,并不会回滚事务。如果想要在这种情况下仍要回滚事务,可以设置 rollbackFor = Exception.class

同一个类中方法的相互调用

方法的相互调用可能会导致事务失效,但不是说任何情况下方法的相互调用都会导致事务失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* @author mofan
* @date 2022/11/9 22:37
*/
@Service
public class StudentService {
@Autowired
private StudentDao studentDao;

public Long saveAndUpdateButNoTransactional(Student student) {
studentDao.insert(student);
// 在未被 @Transactional 注解标记的方法中调用被 @Transactional 注解标记的方法
student.setAge(student.getAge() + 1);
this.updateButThrowException(student);
return student.getId();
}

@Transactional(rollbackFor = Exception.class)
public Long saveAndUpdateButHasTransactional(Student student) {
studentDao.insert(student);
// 在被 @Transactional 注解标记的方法中调用被 @Transactional 注解标记的方法
student.setAge(student.getAge() + 1);
updateButThrowException(student);
return student.getId();
}

@Transactional(rollbackFor = Exception.class)
public Long saveAndUpdateButHasTransactional_2(Student student) {
studentDao.insert(student);
// 在被 @Transactional 注解标记的方法中调用未被 @Transactional 注解标记的方法
student.setAge(student.getAge() + 1);
updateButThrowExceptionButNoTransactional(student);
return student.getId();
}

@Transactional(rollbackFor = Exception.class)
public int updateButThrowException(Student student) {
int i = studentDao.updateById(student);
// 模拟抛出异常
int a = 100 / 0;
return i;
}

public int updateButThrowExceptionButNoTransactional(Student student) {
int i = studentDao.updateById(student);
// 模拟抛出异常
int a = 100 / 0;
return i;
}

public Student selectById(Long id) {
return studentDao.selectById(id);
}

}

使用上述 StudentService 中的 saveAndUpdateXXX() 方法,哪些方法的事务会生效,哪些方法的事务会失效呢?

saveAndUpdateButNoTransactional() 方法的事务会失效,其余两个方法的事务会生效。

  • 针对 saveAndUpdateButNoTransactional() 方法来说,这个方法没有开启事务,同时又使用 this 关键字调用了同一个类中的方法,被调用的方法虽然被 @Transactional 标记,但调用时使用了 this 关键字,并没有调用被代理的方法,因此事务失效;
  • 针对 saveAndUpdateButHasTransactional() 方法来说,无论是该方法还是该方法被调用的方法,都被 @Transactional 标记,默认的事务传播行为是 REQUIRED,外部不存在事务时,开启事务,外部存在事务时,融合到外部事务,因此事务生效;
  • 针对 saveAndUpdateButHasTransactional_2() 方法来说,尽管它调用的方法没有被 @Transactional 标记,但是它自己被 @Transactional 标记,事务会向下传播,因此事务生效。

要解决这种事务失效的场景有三种方式:

1、将需要事务生效的方法都使用 @Transactional 标记,因此建议将 @Transactional 加在类上,针对那些无需提交事务的操作设置 readOnly 属性。比如:

1
2
3
4
5
6
7
8
9
10
@Service
@Transactional(rollbackFor = Exception.class)
public class StudentService {
// ...

@Transactional(readOnly = true)
public Student selectById(Long id) {
return studentDao.selectById(id);
}
}

2、暴露代理对象,调用代理对象的方法。比如:

1
2
3
4
5
6
7
public Long saveAndUpdateRollBackSuccess(Student student) {
studentDao.insert(student);
student.setAge(student.getAge() + 1);
StudentService proxy = (StudentService) AopContext.currentProxy();
proxy.updateButThrowException(student);
return student.getId();
}

使用这种方式时,需要引入 spring-boot-starter-aop 依赖,并在主启动类上添加 @EnableAspectJAutoProxy(exposeProxy = true) 注解。

3、所在类自己注入自己,然后调用。比如:

1
2
3
4
5
6
7
8
9
10
@Autowired
private StudentService studentService;

public Long saveAndUpdateRollBackSuccess_2(Student student) {
studentDao.insert(student);
student.setAge(student.getAge() + 1);
// SpringBoot 从 2.6 开始默认不允许出现 Bean 循环引用,需要在配置文件中显式配置
studentService.updateButThrowException(student);
return student.getId();
}

只不过 SpringBoot 从 2.6 开始 默认不允许 出现 Bean 循环引用,需要在配置文件中显式配置:

1
2
3
spring:
main:
allow-circular-references: true

其实上述三种解决方式的目的都一样:不直接使用当前类中的方法,而是使用代理类中的方法。

实际开发中推荐使用第一种方式,使用第一种方式时,被调用的方法发生异常,由于事务的传播行为,主方法内的操作也会被一起回滚;使用第二种、第三种方式时,不仅需要额外的配置,而且主方法没有开启事务,仅仅是被调用方法的事务生效,被调用的方法发生异常时,主方法的操作不会被回滚,只有被调用方法的操作被回滚。

Spring Cache 中的 @Cacheable 也会因为这种情况导致失效,解决方法类似。

异常被捕获

1
2
3
4
5
6
7
8
@Transational
public void methodC() {
try {
// 业务逻辑
} catch(Exception e) {
// 异常处理
}
}

Spring 的事务是在调用方法前开始的,方法执行完成后才进行提交或回滚,是否回滚又取决于是否抛出了非受检异常,如果异常被捕获就会导致事务失效。

当然,一般不会在业务方法中 catch 异常,如果非要 catch 异常,应该在异常处理时再抛出非受检异常或手动回滚事务,比如:

1
2
3
4
// 抛出非受检异常
throw new RuntimeException();
// 手动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

其他情况

  • 数据库引擎不支持事务
  • 事务内开启新的线程去执行其他事务方法(不同线程拥有的 ThreadLocal 不一样,多个线程中的事务属于多个事务)
  • 方法被 staticfinal 修饰,导致没被代理
  • 方法所在的类未交由 Spring 管理
  • 在多数据源的情况下,数据库管理器 @Transactional(transactionManager = "xxx") 指定错误,比如操作的是 A 数据库,但指定的数据库管理器是数据库 B 的
  • @Transactional 注解的 readOnly 配置为 true,表示是一个只读事务,更新、删除、新增操作时会抛出异常
  • @Transactional 注解的 timeout 配置太小,如果事务在指定时间内无法完成,则报事务超时
  • 未开启事务。使用 @EnableTransactionManagement 注解开启事务,如果导入了 spring-boot-starter-jdbcspring-boot-starter-jdbc 依赖,默认开启事务,无需显式声明。

10.3 一些小技巧

避免提交大事务

提交大事务后可能造成的危害:

  • 数据库死锁
  • 在并发量大的情况下,数据库连接被打满
  • 数据回滚时间过长
  • 服务的性能大大降低(接口超时,数据库主从超时)

避免提交大事务的方法:

  • 将不需要被事务管理的操作抽取出来,比如查询;将需要被事务管理的操作也抽取出来,进行单独管理,比如增加、更新、删除操作。在未被事务管理的方法中调用被事务管理的方法,达到事务处理粒度的控制(注意别让事务失效)。
  • 使用编程式事务管理。比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Integer avoidBigTransaction(Student student) {
// 无需返回值,使用 TransactionCallbackWithoutResult
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
studentDao.insert(student);
}
});

assert studentDao.selectById(student.getId()) != null;

// 需要返回值,直接使用 Lambda 表达式
return transactionTemplate.execute(status -> {
student.setName("aaa");
// 模拟异常
int a = 1 / 0;
return studentDao.updateById(student);
});
}

执行上述方法后,student 对象插入成功,但是其 name 并没有被更新成 aaa,证明在发生异常时事务进行了回滚。

除此之外,还需要:

  • 远程调用的响应时间可能比较长,因此在事务中要避免远程调用,当然也包括发送 MQ 消息、连接 Redis、MongoDB 保存数据等一系列操作;
  • 事务中避免一次性处理太多数据,否则可能会出现大量数据锁等待,可以对需要处理的数据进行分页处理,以减少大事务出现的情况;
  • 可以选择性地将不重要的操作不交由事务管理,比如操作日志的插入、数据量的统计等等,当然,这需要对当前实际业务进行梳理才能得出结果;
  • 思考事务中的所有方法是否都需要同步执行,合理地使用异步处理可以减少大事务出现的情况。

事务成功进行回调

现在有这样一个场景:在对数据进行增加、删除、更新操作时,发送 MQ 消息。

如果数据操作过程中存在异常,应该回滚对数据的操作,这是没有异议的。除此之外,发出去的 MQ 消息也应该收回,但能收回来吗?如果还没收回来,已经被消费了呢?

因此在 事务提交成功后来回调发送 MQ 消息 才是最好的,可以编写一个工具类来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* @author mofan
* @date 2022/11/10 10:56
*/
public class TransactionUtil {

public static void doAfterCompletion(Runnable runnable) {
// 上下文中存在事务,注册同步器
if (TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.registerSynchronization(new DoSomethingTransactionComplete(runnable));
}
}

private static class DoSomethingTransactionComplete implements TransactionSynchronization {

private final Runnable runnable;

public DoSomethingTransactionComplete(Runnable runnable) {
this.runnable = runnable;
}

@Override
public void afterCompletion(int status) {
// 事务提交成功才回调
if (status == TransactionSynchronization.STATUS_COMMITTED) {
runnable.run();
}
}
}
}

使用方式:

1
2
3
4
5
6
7
8
9
10
11
@Transactional(rollbackFor = Exception.class)
public void doAfterComplete(Student student) {
studentDao.insert(student);
// 模拟随机失败
int random = (int) (Math.random() * 6);
if (random == 2) {
int a = 1 / 0;
}
// 事务成功调用
TransactionUtil.doAfterCompletion(() -> System.out.println("事务成功后调用..."));
}

测试方法:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testDoAfterComplete() {
Student student = init();

try {
studentService.doAfterComplete(student);
Assertions.assertNotNull(studentService.selectById(student.getId()));
} catch (Exception e) {
Assertions.assertTrue(e instanceof ArithmeticException);
}
}

11. 整合 YAML

导入以下配置,最低版本不得低于 1.18:

1
2
3
4
5
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.26</version>
</dependency>

编写一个名为 init.yaml 的 YAML 文件:

1
2
3
4
name: mofan
age: 20

list: 1,2,3

编写配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
@ComponentScan(basePackages = "indi.mofan.yml")
public class YmlAutoConfiguration {
@Bean
public PropertySourcesPlaceholderConfigurer configurer() {
YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
yamlPropertiesFactoryBean.setResources(new ClassPathResource("init.yaml"));
Properties properties = yamlPropertiesFactoryBean.getObject();

PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
configurer.setProperties(Objects.requireNonNull(properties));
return configurer;
}
}

编写进行测试的 bean:

1
2
3
4
5
6
7
8
9
10
11
@Setter
@Getter
@Component
public class Person {
@Value("${name}")
private String name;
@Value("${age}")
private Integer age;
@Value("#{'${list}'.split(',')}")
private List<Integer> list;
}

测试方法:

1
2
3
4
5
6
7
8
@Test
public void testYamlConfiguration() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(YmlAutoConfiguration.class);
indi.mofan.yml.Person person = applicationContext.getBean("person", indi.mofan.yml.Person.class);
Assert.assertEquals("mofan", person.getName());
Assert.assertEquals(20, person.getAge().intValue());
Assert.assertEquals(3, person.getList().size());
}