零、Spring 简介

Spring 是分层的 Java SE/EE 应用轻量级全栈开源框架,以 IoC(反转控制)和 AOP(面向切面编程)为内核。

提供了展现层 Spring MVC和持久层 Spring JDBCTemplate 以及业务层事务管理等众多的企业级应用技术,还能整合开源世界众多著名的第三方框架和类库,逐渐成为使用最多的Java EE 企业应用开源框架。

Spring 的优势

  1. 方便解耦,简化开发;

    通过 Spring 框架提供的Bean IoC容器,可以将对象间的依赖关系交由Spring控制,从而降低耦合度。

  2. AOP思想的支持;

  3. 声明式事务的支持;

    传统的编程式事务处理繁琐、事务管理代码冗杂,spring通过声明式配置多种事务的管理,提高效率

  4. 方便程序的测试

  5. 方便集成其他优秀框架

Spring 结构 (ver 4.x)

image-20220616104824734
  1. Core Container : 核心容器

    • 控制反转 (Inversion of Control)
    • 依赖注入 (Dependency Injection)
  2. Aspect Oriental Program (AOP) : 面向切面编程思想

    AspectsJ : AOP思想的实现

  3. Data Access / Data Integration : 数据访问与数据集成(与数据库的操作有关)

  4. Web : Web 开发相关

  5. Test : 测试相关

一、Spring 核心容器

1. 控制反转 IoC 与依赖注入 DI

我们先从一个简单的例子讲起:

1
2
3
4
5
6
package DAO;
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("bookDao save ...");
}
}
1
2
3
4
5
6
7
package Service;
public class BookServiceImpl implements BookService {
private BookDao bookDao = new BookDaoImpl();
private void save(){
bookDao.save();
}
}
  • 耦合度偏高:当我们修改 BookDaoImpl 这个类的内容时,相应的,BookServiceImpl 类需要重新编译。

此时我们就可以想到解决方法:为了使业务与数据解耦,即使用对象时,在程序中不要主动使用 new 产生对象,而是由外部提供对象。将对象的创建由程序转移到外部,这种思想称为 IoC 控制反转。

  • Spring 提供了一个容器,称为 IoC 容器(核心容器),用来充当 IoC 思想中的“外部”。
  • IoC 容器负责对象的创建、初始化等一系列工作,被创建或被管理的对象在 IoC 容器中统称为 Bean
graph LR
subgraph "IoC容器"
Sercive --依赖--> Dao
end

在 IoC 容器中建立 BeanBean 之间的依赖关系的过程,被称为依赖注入 (DI)

注意 ⚠️

我们已经分别解释了控制反转和依赖注入的概念。有些人会把控制反转和依赖注入等同,但实际上它们有着本质上的不同。

  • **控制反转 (IoC)**是一种思想
  • **依赖注入 (DI)**是一种设计模式

IoC 入门案例

整体思路:

  1. 添加 Spring 的坐标(pom.xml
  2. IoC 容器管理什么?(Bean,即对象)
  3. 如何将对象“放入” IoC 容器中?(通过配置文件)
  4. 如何得到 IoC 容器?(接口)
  5. 如何得到 IoC 容器中的对象?(接口方法)
  1. 导入 Spring 的坐标 (pom.xml)

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.3.5.RELEASE</version>
    </dependency>
  2. 编写 Dao 接口和实现类

    1
    2
    3
    4
    package Interface;
    public interface UserDao {
    public void save();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    package DAO;
    import Interface.UserDao;
    public class UserDaoImpl implements UserDao {
    @Override
    public void save() {
    System.out.println("save running...");
    }
    }
  3. 创建 Spring 配置文件(ApplicationContext.xml

  4. 在 Spring 配置文件中配置 UserDaolmpl

    1
    2
    3
    4
    5
    6
    7
    <?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="userDao" class="DAO.UserDaoImpl"></bean>
    </beans>
  5. 实现 Spring 提供的接口得到 IoC 容器,并从容器获得 Bean 实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class UserDaoTest {
    public static void main(String[] args) {
    // 获取 Ioc 容器
    ApplicationContext context =
    new ClassPathXmlApplicationContext("ApplicationContext.xml");

    // 获取 Bean 对象
    UserDao userDao = (UserDao) context.getBean("userDao");
    userDao.save();
    }
    }

DI 入门案例

通过刚才的例子,我们已经成功地将对象的管理放在了 IoC 容器中。那么该如何才能不使用 new 关键字来进行操作对象间的依赖关系呢?

整体思路:

  1. 基于 IoC 管理对象 Bean
  2. 不使用 new 关键字来创建对象
  3. 如何将需要依赖的对象传入当前对象呢?(提供 setXxx(Object obj) 方法)
  4. 这种依赖关系该如何描述?(通过配置)
  1. BookServiceImpl 类放入 IoC 容器

    1
    <bean id="bookServiceImpl" class="Service.BookServiceImpl"></bean>
  2. 删去 BookServiceImpl 类中使用 new 关键字创建对象的语句

  3. 并提供 setBookDao(BookDao bookDao) 方法传入一个 BookDao 对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package Service;
    public class BookServiceImpl implements BookService {
    // 2.
    private BookDao bookDao;
    // 3.
    public void setBookDao(BookDao bookDao) {
    this.bookDao = bookDao;
    }
    public void save(){
    bookDao.save();
    }
    }
  4. 通过配置描述依赖关系

    1
    2
    3
    4
    5
    <bean id="bookServiceImpl" class="Service.BookServiceImpl">
    <!-- 配置依赖关系 -->
    <!-- name="属性字段的标识符" ref="Bean 对象的 id" -->
    <property name="bookDao" ref="bookDao"/>
    </bean>

BeanFactory 的简单实现

IoC 的主要实现是得益于一个可以动态生成对象的工厂 BeanFactory,它通过读取配置文件中的对象的配置信息,通过反射 (reflexion) 机制返回一个实体对象。

  1. 首先定义一个外部的配置文件

    1
    UserDao=fr.spring.gdai.impl.UserDaoImpl
  2. Java程序读取这个配置文件

    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
    public class BeanFactory {
    private static Properties properties;
    private static Map<String,Object> cache = new HashMap<>();

    static {
    properties = new Properties();
    properties.load(BeanFactory.class.getClassLoader().
    getResourceAsStream("factory.properties"))
    }

    // 多例 prototype
    public static Object getUserDaoPrototype(String property) {
    String value = properties.getProperty(property);
    Class clazz = Class.forName(value);
    Object userDao = clazz.getConstructor(null).newInstance(null);
    return userDao;
    }

    // 单例 singleton,spring默认
    public static Object getUserDaoSingleton(String property) {
    // 先判断缓存中是否存在该对象
    if(!cache.containsKey(property)){
    // 在多线程下确保一致性,加锁
    synchronized (BeanFactory.class){
    // 双重检测缓存中是否存在该对象
    if(!cache.containsKey(property)){
    // 反射机制创建对象
    String value = properties.getProperty(property);
    Class clazz = Class.forName(value);
    Object object = clazz.getConstructor(null).newInstance(null);
    // 将对象存入缓存
    cache.put(property,object);
    }
    }
    }
    return cache.get(property);
    }
    }
  3. 测试 Object getUserDao(String property) 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Test
    public void userDaoPrototypeTest() {
    UserDao userDao1 = (UserDao) BeanFactory.getUserDaoPrototype("UserDao");
    UserDao userDao2 = (UserDao) BeanFactory.getUserDaoPrototype("UserDao");
    System.out.println(userDao1);
    System.out.println(userDao2);
    }
    /*
    fr.spring.gdai.impl.UserDaoImpl2@3f8d7a8c
    fr.spring.gdai.impl.UserDaoImpl2@1d3a1b2c
    */
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Test
    public void userDaoSingletonTest() {
    UserDao userDao1 = (UserDao) BeanFactory.getUserDaoSingleton("UserDao");
    UserDao userDao2 = (UserDao) BeanFactory.getUserDaoSingleton("UserDao");
    System.out.println(userDao1);
    System.out.println(userDao2);
    }
    /*
    fr.spring.gdai.impl.UserDaoImpl2@3f8d7a8c
    fr.spring.gdai.impl.UserDaoImpl2@3f8d7a8c
    */

2. Bean 对象

<Bean> 标签的基本配置

1
<bean id="唯一性标识符" class="全类名(包名+类名)" scope="singleton 或 prototype"/>
作用范围 scope
  1. singleton单例,默认值
  2. prototype多例
  3. request:WEB 项目中,Spring 创建一个 Bean 对象,将其放入 request 域中
  4. session:WEB 项目中,Spring 创建一个 Bean 对象,将其放入 session 域中
  5. global session:WEB 项目中,Spring创建一个Bean对象,将其放入global session域中
singletonprototype 的区别
实例化个数 实例化时机 生命周期
singleton
默认
1 加载 AppliContext.xml
实例化所有配置的 Bean 实例
- 创建:当应用加载,创建容器时,对象就被创建了
- 运行:只要容器在,对象一直活着
- 销毀:当应用卸载,销毁容器时,对象就被销毁了
prototype 多个 当调用 getBean()方法时 - 创建:当使用对象时,创建新的对象实例
- 运行:只要对象在使用中,就一直活着
- 销毁:当对象长时间不用时,被 Java 的垃圾回收器回收
Bean 生命周期配置

创建对象(内存分配)\to 执行构造方法 \to 执行属性注入(setXxx()\to 执行 Bean 的初始化方法 init-method \to 使用 Bean \to 执行 Bean 的销毁方法 destroy-method

1
2
3
4
5
6
7
8
9
10
11
public class UserDaoImpl implements UserDao {
public UserDaoImpl() { System.out.println("UserDaoImpl 创建成功"); }

public void init(){ System.out.println("初始化方法..."); }
public void destroy(){ System.out.println("销毁方法..."); }

@Override
public void save() {
System.out.println("save running...");
}
}
1
2
3
<bean id="userDao" class="impl.UserDaoImpl" 
init-method="init" destroy-method="destroy">
</bean>
  • init-method:指定类中的初始化方法
  • destroy-method:指定类中的销毁方法
关闭销毁 IoC 容器

那么,如何关闭销毁 IoC 容器呢?

  1. 调用 ClassPathXmlApplicationContext 对象的 close() 方法可以暴力地关闭销毁 IoC 容器;

    1
    ((ClassPathXmlApplicationContext) context).close();
  2. 调用 ClassPathXmlApplicationContext 对象的 registerShutdownHook() 方法可以在 JVM 结束运行前关闭销毁 IoC 容器

    1
    ((ClassPathXmlApplicationContext) context).registerShutdownHook();

Bean 实例化的三种方式

  • 无参默认构造实例化 Bean 对象

    1
    public UserDaoImpl() {}
    1
    <bean id="userDaoByConstructor" class="impl.UserDaoImpl"/>
  • 静态工厂实例化 Bean 对象

    1
    2
    3
    4
    5
    public class StaticFactory {
    public static UserDao getUserDaoInstance(){
    return new UserDaoImpl();
    }
    }
    1
    2
    3
    4
    5
    <!-- 使用静态工厂返回Bean -->
    <bean id="userDaoStaticFactory"
    class="factory.StaticFactory"
    factory-method="getUserDaoInstance">
    </bean>
  • 动态工厂实例化 Bean 对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class DynamicFactory {
    public UserDao getUserDao(){
    return new UserDaoImpl();
    }
    }
    /**
    * 因为getUserDao()不是静态方法,所以调用的时候需要实例化一个工厂对象
    * 相应的,bean的配置也是如此
    **/
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!-- 使用动态工厂返回Bean -->
    <!-- 1. 先创建一个动态工厂实例 -->
    <bean id="DynamicFactory"
    class="factory.DynamicFactory">
    </bean>
    <!-- 2. 再通过动态工厂实例获得一个userDao对象 -->
    <bean id="userDaoDynamicFactory"
    factory-bean="DynamicFactory"
    factory-method="getUserDao">
    </bean>
  • 使用 FactoryBean 实例化 Bean 对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class UserDaoFactoryBean implements FactoryBean<UserDao> {

    @Override
    public UserDao getObject() throws Exception {
    return new UserDaoImpl();
    }

    @Override
    public Class<?> getObjectType() {
    return UserDao.class;
    }
    }
    1
    <bean id="userDaoByBeanFactory" class="factory.UserDaoFactoryBean"/>

Bean 的依赖注入

因为 UserServiceUserDao 都在 Spring容器中,而最终程序直接使用的是 UserService,所以可以在 Spring 容器中,将 UserDao 设置到 UserService 内部 (依赖注入)

graph LR
subgraph "IoC容器"
Sercive --依赖--> Dao
end

在编写程序时,通过控制反转,把对象的创建交给了 Spring,但是代码中不可能出现没有依赖的情况。

IoC 解耦只是降低他们的依赖关系,但不会消除。例如:业务层仍会调用持久层的方法

那这种业务层和持久层的依赖关系,在使用 Spring 之后,就让 Spring 来维护了。简单的说,就是让框架把持久层对象传入业务层,而不用我们自己去获取。

怎么将 UserDao 怎样注入到 UserService 内部呢?

依赖传入方式

思考:

依赖注入描述了在容器中建立 Bean 对象之间依赖关系的过程。那如果需要建立的依赖关系是 String基本数据类型(如 int, char)呢?

  • setXxx()方法 - 引用对象
    1. 在代码中提供可访问的 setter 方法
    2. 在配置中使用 <u><property></u> 标签,ref 属性注入对象(可注入多个:多个 <property> 即可)
1
2
3
4
5
<bean id="userDao1" class="impl.UserDaoImpl"></bean>
<bean id="userService" class="service.impl.UserServiceImpl">
<!-- 注入依赖关系 -->
<property name="userDao" ref="userDao1"></property>
</bean>
1
2
3
4
5
6
7
8
9
10
public class UserServiceImpl implements UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void save() {
userDao.save();
}
}
  • setXxx()方法 - 引用基本数据类型
    1. 在代码中提供可访问的 setter 方法
    2. 在配置中使用 <u><property></u> 标签,value 属性注入参数(可注入多个:多个 <property> 即可)
    1
    2
    3
    4
    <bean id="userDao1" class="impl.UserDaoImpl">
    <property name="username" value="gdai"></property>
    <property name="age" value="24"></property>
    </bean>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class UserDaoImpl implements UserDao {
    private String username;
    private int age;

    public void setUsername(String username) { this.username = username; }
    public void setAge(int age) { this.age = age; }

    public UserDaoImpl() {
    System.out.println("UserDaoImpl 创建成功");
    }

    @Override
    public void save() {
    System.out.println("[UserDao] : "+username+", "+age+"save running...");
    }
  • 有参构造方法 - 引用对象
    1. 构造器中存在参数
    2. 在配置中使用 <constructor-arg> 标签,name 是形参名,ref 属性注入对象(可注入多个:多个 <property> 即可)
    1
    2
    3
    4
    5
    <bean id="userDao1" class="impl.UserDaoImpl"></bean>-->
    <bean id="userServiceByConstructor" class="service.impl.UserServiceImpl">
    <!-- 注入依赖关系 name=形参名 -->
    <constructor-arg name="userDao" ref="userDao1"></constructor-arg>
    </bean>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class UserServiceImpl implements UserService {
    private UserDao userDao;
    public UserServiceImpl(UserDao userDao) {
    this.userDao = userDao;
    }

    @Override
    public void save() {
    userDao.save();
    }
    }
  • 有参构造方法 - 引用基本数据类型
  1. 构造器中存在参数
  2. 在配置中使用 <constructor-arg> 标签,name 是形参名,value 属性注入参数(可注入多个:多个 <property> 即可)
1
2
3
4
5
6
7
8
9
10
<bean id="userDao1" class="impl.UserDaoImpl">
<constructor-arg name="username" value="gdai"/>
<constructor-arg name="age" value="24"/>
<!-- 使用类型匹配,解决参数名的耦合 -->
<constructor-arg type="java.lang.String" value="gdai"/>
<constructor-arg type="int" value="24"/>
<!-- 使用位置索引匹配,解决同类型参数的问题 -->
<constructor-arg index="0" value="gdai"/>
<constructor-arg index="1" value="24"/>
</bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserDaoImpl implements UserDao {
private String username;
private int age;


public UserDaoImpl(String username, int age) {
this.username = username;
this.age = age;
System.out.println("UserDaoImpl 创建成功");
}

@Override
public void save() {
System.out.println("[UserDao] : "+username+", "+age+"save running...");
}
自动装配 autowire

自动装配引用对象主要有 3 种方式:按类型,按名称,按构造方法。关键字是 autowire

其中按类型 和 按名称 的自动装配基于 setter 方法

  • autowire=byType:根据 setter 方法中 this 参数的类型,自动装配(同类型的 bean 对象要唯一);
  • autowire=byName:根据 setter 方法中 this 参数的名称,自动装配;
集合注入

对于集合对象(Arrays, List, Set, Map, Properties 等),他们的注入会相对繁琐一些。如下所述

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
<bean id="bookDao" class="impl.BookDaoImpl">
<property name="array">
<array>
<value>100</value>
<value>200</value>
<value>300</value>
</array>
</property>

<property name="list">
<list>
<value>aaa</value>
<value>bbb</value>
<value>ccc</value>
</list>
</property>

<property name="map">
<map>
<entry key="country" value="china"/>
<entry key="province" value="sichuan"/>
<entry key="city" value="chengdu"/>
</map>
</property>

<property name="set">
<set>
<value>xxx</value>
<value>xxx</value>
<value>yyy</value>
<value>zzz</value>
</set>
</property>

<property name="properties">
<props>
<prop key="country">china</prop>
<prop key="province">sichuan</prop>
<prop key="city">chengdu</prop>
</props>
</property>
</bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class BookDaoImpl implements BookDao {
private int[] array;
private List<String> list;
private Set<String> set;
private Map<String, String> map;
private Properties properties;

public void setArray(int[] array) {this.array = array;}
public void setList(List<String> list) {this.list = list;}
public void setSet(Set<String> set) {this.set = set;}
public void setMap(Map<String, String> map) {this.map = map;}
public void setProperties(Properties properties) {this.properties = properties;}

@Override
public void print() {
System.out.println("数组: "+Arrays.toString(array));
System.out.println("list: "+list);
System.out.println("set: "+set);
System.out.println("map: "+map);
System.out.println("properties: "+properties);
}
}

第三方数据源 Bean 管理

我们这里使用 c3p0 连接池做示范:

  1. 首先我们先导入 c3p0maven 坐标;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
    <dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.5.2</version>
    </dependency>

    <!-- JDBC -->
    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.29</version>
    </dependency>

  2. 然后我们想使用 bean 对象来帮我们维护一个 dataSource 对象用来连接数据库。因为 c3p0 提供了 setter 方法,所以我们可以使用 setter 注入;

    1
    2
    3
    4
    5
    6
    <bean name="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <property name="driverClass" value="com.mysql.jdbc.Driver"/>
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"/>
    <property name="user" value="root"/>
    <property name="password" value="root"/>
    </bean>

其实,在 c3p0 中可以使用外置配置文件 .properties 来配置连接池信息。那么该如何使用 bean 来维护呢?

加载 .properties 文件

c3p0.properties

1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=root
  1. 开启 context 命名空间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    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
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    ">
  2. 使用 context 空间加载 demo.properties 文件,可使用 *.properties 加载所有配置文件

    1
    <context:property-placeholder location="classpath:c3p0.properties"/>
  3. 使用 ${} 占位符读取加载的属性值

    1
    2
    3
    4
    5
    6
    <bean name="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <property name="driverClass" value="${jdbc.driver}"/>
    <property name="jdbcUrl" value="${jdbc.url}"/>
    <property name="user" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
    </bean>

引入其他配置文件(分模块开发)

实际开发中,Spring的配置内容非常多,这就导致Spring配置很繁杂且体积很大,所以,可以将部分配置拆解到其他配置文件中,而在Spring主配置文件通过impor标签进行加载

1
<import resource="applicationContext-xxx.xml"/>

Spring的重点配置

<bean>标签

  • id属性:在容器中Bean实例的唯一标识,不允许重复
  • class属性:要实例化的Bean的全类名
  • scope属性:Bean的作用范国,常用是 singleton (默认)和 prototype
  • <constructor-arg> 标签:构造器注入
  • <property> 标签:setter 注入
    • name属性:厲性名称
    • value属性:注入的普通属性值
    • ref属性:注入的对象引用值
    • <list>标签
    • <map>标签
    • <properties> 标签

<import>标签:导入其他的 spring 的分文件

二、Spring 注解开发

Spring是轻代码而重配置的框架,配置比较繁重,影响开发效率,所以注解开发是一种趋势,注解代替 .xml 配置文件可以简化配置,提高开发效率。

注解 说明
@Component("name") 使用在类上用于实例化 Bean
@Controller 使用在 web 层类上用于实例化 Bean
@Service 使用在 service 层类上用于实例化 Bean
@Repository 使用在 dao 层类上用于实例化 Bean
@Autowired 使用在字段上用于根据【类型】依赖注入
@Qualifier 结合@Autowired一起使用用于根据【名称】进行依赖注入
@Resource 相当于@Autowired + @Qualifier,按照【名称】进行注入
@Value 注入普通属性
@Scope 标注Bean的作用范围
@PostConstruct 使用在方法上标注该方法是Bean的初始化方法
@PreDestroy 使用在方法上标注该方法是Bean的销毁方法

1. Bean 对象的管理

接下来,我们将一步一步的将基于配置文件bean 改为基于注解bean

1
<bean id="userDao" class="dao.UserDaoImpl"/>
1
2
3
4
5
6
7
8
package dao;
public class UserDaoImpl implements UserDao {
public UserDaoImpl() {System.out.println("UserDaoImpl 创建成功");}

@Override
public void save() {
System.out.println("[UserDao] : "+this+"save running...");
}

将其改为

1
2
3
4
5
6
7
8
9
package dao;
@Component("userDao")
@Scope("singleton")
public class UserDaoImpl implements UserDao {
public UserDaoImpl() {System.out.println("UserDaoImpl 创建成功");}

@Override
public void save() {System.out.println("[UserDao] : "+this+"save running...");}
}

为了让 spring 容器知道这个叫 userDao 的组件(Component),我们需要在配置文件中设置 <context:component-scan> 来“扫描”代码里的 @Component

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
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
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<!-- 配置组建扫描 -->
<context:component-scan base-package="dao"></context:component-scan>
</beans>

我们能否使用纯注解开发,来代替上述的 .xml 中设置呢?可以。

  1. 首先我们使用一个带有 @Configuration 注解的类来代替 .xml 中的骨架部分;
  2. 然后在用 @ComponentScan("dao") 来代替“配置组件扫描”的语句;
  3. 最后在测试中通过 new AnnotationConfigApplicationContext(SpringConfig.class),使 ApplicationContext context 容器实例化
1
2
3
@Configuration
@ComponentScan("dao")
public class SpringConfig {}
1
2
3
4
5
6
7
@Test
public void userDaoAnnotationTest() {
ApplicationContext context =
new AnnotationConfigApplicationContext(SpringConfig.class);
UserDaoImpl userDao = context.getBean(UserDaoImpl.class);
userDao.save();
}

我们可以在任意方法上添加 @PostConsturt@PreDestory 来指定初始化操作销毁操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package dao;
@Component("userDao")
@Scope("singleton")
public class UserDaoImpl implements UserDao {
public UserDaoImpl() {System.out.println("UserDaoImpl 创建成功");}

// 初始化操作
@PostConstruct
public void init(){System.out.println("初始化方法...");}

// 销毁操作
@PreDestroy
public void destroy(){System.out.println("销毁方法...");}

@Override
public void save() {System.out.println("[UserDao] : "+this+" SAVE RUNNING...");}
}

2. 依赖注入

自动装配

注入引用对象

在之前我们学习过,基于 .xml 配置文件是通过 [<bean ... autowire="byXxx"/>](#自动装配 autowire) 配置自动装配的。这里我们介绍注解方式的自动装配 @Autowired

  1. 我们在 SpringConfig 配置类里添加一个 ComponentScan 的路径,可以用数组来储存。

    1
    2
    3
    @Configuration
    @ComponentScan({"dao","service"})
    public class SpringConfig {}
  2. 编写一个新的 UserServiceImpl 类,其依赖于 UserDao 这个类,可以不提供 setter 方法,spring 会通过暴力反射 强行设置。我们就可以使用 @Autowired 配置自动装配。

    • 如果有多个相同类型的 bean,我们使用 @Qualifier("name") 来指定引用类型的名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service("userService")
public class UserServiceImpl implements UserService {

// 按照数据类型从spring容器中进行匹配。当同一个类型有多个不同名字的bean对象时,需要按照名称匹配(注入)
@Autowired
@Qualifier("userDao") // 按照名称匹配
private UserDao userDao;

public void setUserDao(UserDao userDao) {this.userDao = userDao;}

@Override
public void save() {
System.out.println("[UserService]: "+this+" SAVE RUNNING...");
userDao.save();
}
}
注入基本数据类型

如果我们想将一个基本数据类型注入 bean 对象中,可以使用 @Value(val) 可以实现简单类型的注入。

1
2
3
4
5
6
7
8
@Repository("bookDao")
public class BookDaoImpl implements BookDao {
@Value("gdai")
private String name;

@Override
public String toString() { return "[BookDao]: name="+name; }
}
1
2
3
4
5
6
7
@Test
public void bookDaoAnnotationTest() {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = context.getBean(BookDaoImpl.class);
assert bookDao.toString().equals("[BookDao]: name=gdai");
}

如果我们想通过加载外部的配置文件,动态配置注入的基本数据类型。我们同样可以使用 ${} 占位符来读取加载的属性值。

配置文件 test.properties

1
bookDao.name=gdai
  1. 我们在 SpringConfig 配置类里添加一个 @PropertySource("filename") 注解来加载配置文件,多文件时可以使用数组。

    1
    2
    3
    4
    @Configuration
    @ComponentScan({"dao","service"})
    @PropertySource("classpath:test.properties")
    public class SpringConfig {}
  2. bean 对象中使用 ${} 占位符来读取加载的属性值

    1
    2
    3
    4
    5
    6
    7
    8
    @Repository("bookDao")
    public class BookDaoImpl implements BookDao {
    @Value("${bookDao.name}")
    private String name;

    @Override
    public String toString() { return "[BookDao]: name="+name; }
    }

3. 第三方 Bean 对象

和之前一样,我们依旧使用 c3p0 连接池做示范:

  1. 首先我们先导入 c3p0maven 坐标;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
    <dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.5.2</version>
    </dependency>

    <!-- JDBC -->
    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.29</version>
    </dependency>

  2. 思路相同,我们需要使用 bean 对象来帮我们维护一个 dataSource 对象用来连接数据库。但是这里需要我们自己配置这个 bean 对象,并将这个对象返回,同时 @Bean 注解定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class DataSourceUtil {
    @Bean
    public DataSource dataSource() throws PropertyVetoException {
    ComboPooledDataSource dataSource = new ComboPooledDataSource();
    dataSource.setDriverClass("com.mysql.jdbc.Driver");
    dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/moviesdata?serverTimezone=Asia/Shanghai");
    dataSource.setUser("root");
    dataSource.setPassword("root");
    return dataSource; // 返回的这个对象作为 bean
    }
    }
  3. 而在 SpringConfig 配置类中,我们这需要将其使用 @Import 注解导入。

    1
    2
    3
    4
    5
    @Configuration
    @Import(DataSourceUtil.class)
    public class SpringConfig {

    }
  4. 当然,这里也可以添加一个 @PropertySource("filename") 注解来加载配置文件。然后使用 ${} 占位符来使第三方 bean 对象的参数从配置文件中加载获得。

    c3p0.properties

    1
    2
    3
    4
    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/moviesdata?serverTimezone=Asia/Shanghai
    jdbc.username=root
    jdbc.password=root
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @PropertySource("classpath:c3p0.properties")
    public class DataSourceUtil {
    @Bean
    public DataSource getDataSource() throws PropertyVetoException {
    ComboPooledDataSource dataSource = new ComboPooledDataSource();
    dataSource.setDriverClass("${jdbc.driver}");
    dataSource.setJdbcUrl("${jdbc.url}");
    dataSource.setUser("${jdbc.username}");
    dataSource.setPassword("${jdbc.password}");
    return dataSource; // 返回的这个对象作为 bean
    }
    }

第三方 Bean 依赖注入

在上面的例子中,第 4 步其实就是对于第三方 Bean 的基本类型注入。在此,我们可以给出更加一般(规范)的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@PropertySource("classpath:c3p0.properties")
public class DataSourceUtil {
@Value("${jdbc.driver}")
String jdbcDriver;

@Value("${jdbc.url}")
String jdbcURL;

@Value("${jdbc.username}")
String jdbcUser;

@Value("${jdbc.password}")
String jdbcPassword;

@Bean
public DataSource getDataSource() throws PropertyVetoException {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setDriverClass(jdbcDriver);
dataSource.setJdbcUrl(jdbcURL);
dataSource.setUser(jdbcUser);
dataSource.setPassword(jdbcPassword);
return dataSource; // 返回的这个对象作为 bean
}
}

对于引用类型,可以直接将引用的对象通过【形参】传入 @Bean 这个对象中,spring 会通过类型匹配传入。

1
2
3
4
5
6
@Bean
public DataSource getDataSource(BookDao bookDao) {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
System.out.println(bookDao);
return dataSource; // 返回的这个对象作为 bean
}

三、Spring 中的 AOP

课程回顾:代理模式

1 AOP 简介

AOP (Aspect Oriented Program),即面向切面编程。与 OOP 类似,AOP 也是一种编程思想,在不改动原始代码的基础上为其进行功能增强。使用动态代理机制实现。

1
2
3
4
5
6
public interface Calculate {
public int add(int num1, int num2);
public int sub(int num1, int num2);
public int multi(int num1, int num2);
public int div(int num1, int num2);
}
1
2
3
4
5
6
7
8
9
10
public class CalculateImpl implements Calculate{
@Override
public int add(int num1, int num2) {
System.out.println("Func_add : " + num1 + " + " + num2); // 日志打印
int result = num1 + num2; // 业务代码
System.out.println("Result : " + result); // 日志打印
return result;
}
// 我们是否能将"日志打印"和"业务代码"分离开呢?
}

我们可以看到,"日志打印"和"业务代码"紧密的耦合在一起。那么我们是否能将"日志打印"和"业务代码"分离开呢?

我们试图找到一种方法,如下所示:使得"业务代码"作为一个类,"日志打印"单独作为一个特殊的类:

1
2
3
4
5
6
7
8
9
public class CalculateImpl implements Calculate{
@Override
public int add(int num1, int num2) { // 连接点: 所有方法
// 切入点: 需要"追加功能"的方法
// before, 从此处"切开", 插入日志打印
int result = num1 + num2; // 业务代码
// after, 再次从此处"切开", 插入日志打印
return result;
}
1
2
3
4
5
6
public class Aspect {												// 通知类: Aspect
before: // 通知
System.out.println("Func_add : " + num1 + " + " + num2); // 日志打印
after: // 通知
System.out.println("Result : " + result); // 日志打印
}

AOP 要做的就是将日志代码全部抽象出去统一进行处理,计算器方法中只保留核心的业务代码。做到核心业务和非业务代码的解耦合。

术语解释

  1. 横切关注点(逻辑概念):

    从每个方法中抽取出来的相同的非核心业务。在同一个项目中,我们可以使用多个横切关注点对方法进行增强。

    image-20220624131911703
  2. 通知(Advice):

    每个横切关注点要做的事情都需要写一个方法来实现,这样的方法就叫做通知方法。

    • 前置通知:在被代理的目标方法执行

    • 后置通知:在被代理的目标方法***后 (finally {} 块内)***执行

    • 返回通知:在被代理的目标方法返回值之后执行

    • 异常通知:在被代理的目标方法异常结束之后执行

    • 环绕通知:在被代理的目标方法前、后执行

      ‼️ 后置通知返回通知的区别 : 前者是 try {} 内部方法调用成功返回后 , 而后置通知是 finally {} 里面的代码.

  3. 切面对象:封装通知方法的类。

  4. 目标对象:需要功能增强的对象。

  5. 代理对象:向目标对象应用通知后的代理对象,有 AOP 通过动态代理实现

    image-20220624132501714
  6. 连接点(JointPoint):抽取横切关注点的***位置(在 SpringAOP 中是一个方法)***

  7. 切入点 (Pointcut):有些接入点不需要“切入”通知方法,那么这个连结点就不是切入点;反之,接入点需要“切入”通知方法,那么就是切入点。

    • 在 SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法
    • 切入点 \subset 连接点。连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点。

总结 : AOP 重点编写切面类 ( 先抽取代码 ),通过切点表达式声明的方式(再套用到目标类上)告诉Spring 框架我要将我注解到的通知应用到那个类的那个连接点上。

2 AOP 入门案例

2.0 技术说明

image-20221113133001400
  • AspectJ : 本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态.。weaver 就是织入器,Spring 只是借用了 AspectJ 中的注解
  • JDK 动态代理:要求必须要有接口,最终生成的代理类在 com.sun.Proxy 包下,名为 $Proxy2,其和目标类实现的是相同接口。
  • CGLIB 动态代理:最终生成的代理类会继承目标类,并且和目标类在相同的包下。
  • 当被代理类没有接口的时候使用 CGLIB,有接口则使用 JDK

2.1 准备工作

  • 导入 maven 坐标
1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.5.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.20</version>
</dependency>
  • 制作连接点(接口及实现类)
1
2
3
4
5
6
7
8
package fr.spring_aop.gdai.Interfaces;

public interface Calculate {
public int add(int num1, int num2);
public int sub(int num1, int num2);
public int multi(int num1, int num2);
public int div(int num1, int num2);
}
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
package fr.spring_aop.gdai.Implements;

@Component
public class CalculateImpl implements Calculate{

@Override
public int add(int num1, int num2) {return num1 + num2}

@Override
public int sub(int num1, int num2) {return num1 - num2}

@Override
public int multi(int num1, int num2) {return num1 * num2}

@Override
public int div(int num1, int num2) {
int result = 0;
try {
result = num1 / num2;
} catch (Exception e) {
throw e;
} finally {
System.out.print("Finally : ");
}
return result;
}
}
  • 配置 Spring 的参数(注解形式

    • 开启容器扫描 ComponenetScan,以及确定扫描位置

    • 开启基于注解的 AOP EnableAspectJAutoProxy

1
2
3
4
5
@Configuration
@ComponentScan({"fr.spring_aop.gdai"})
@EnableAspectJAutoProxy
public class SpringConfig {
}

2.2 实现切面类

  • @Aspect 注解:声明该类为切面类
@Pointcut 切入点表达式

@Pointcut() 注解:声明切入点表达式。当一个切入点表达式被多次使用时,我们可以定义一个公共的 Pointcut,使得其他通知方法也可以使用这个切入点表达式而不用重复定义。

1
@Pointcut("execution(访问修饰符 返回值类型 全类名.方法名(传入参数的类型))")

其中:

​ (1)可以使用通配符 * 来代替访问修饰符返回值类型

​ (2)可以使用通配符 * 来代替当前包下的任意类

​ (3)可以使用通配符 * 来代替在某一类中的任意方法

​ (4)可以使用通配符 .. 来代替某一方法中的任意参数列表(可变参数)

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
@Component
@Aspect
public class LogAspect {
@Pointcut("execution(* fr.spring_aop.gdai.Implements.*.*(..))")
public void pointCut(){}

@Before(value = "pointCut()")
public void before(JoinPoint joinpoint){
System.out.println("前置通知");
}

@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result){
System.out.println("返回通知");
}

@After(value = "pointCut()")
public void after(JoinPoint joinPoint) {
System.out.println("后置通知");
}

@AfterThrowing(value = "pointCut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
System.out.println("异常通知");
}
}
JoinPoint 对象

JoinPoint 对象:可以在该通知方法中将 JoinPoint 对象作为参数传入方法,以获取(目标对象中的)连接点的信息

其中:

(1)Signature joinPoint.getSignature():获取连接点对应方法的方法名

(2)Object[] joinPoint.getArgs():获取连接点对应方法的参数

@Before 前置通知

@Before() 注解:声明一个切入点的前置通知方法,其 value 值为切入点表达式,具体参数如下:

1
@Before(value="execution(访问修饰符 返回值类型 全类名.方法名(传入参数的类型))")

例:

1
2
3
4
5
6
7
8
9
10
@Component
@Aspect
public class LogAspect {

@Before(value = "execution(public int fr.spring_aop.gdai.Implements.CalculateImpl.div(int, int)))")
public void before(){
String name = joinpoint.getSignature().getName();
System.out.println("Func_" + name + " : " + Arrays.toString(joinpoint.getArgs()));
}
}
1
2
3
4
5
6
@Test
public void testDiv() {
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
Calculate calculate = context.getBean(Calculate.class);
calculate.div(2,1);
}

注意

如果我们这里直接使用 getBean(CalculateImpl.class) 时,Spring 会抛出 No qualifying bean of type 'xxx.CalculateImpl' available 的异常,说明在有代理类的情况下,不能直接访问实现类。符合代理的初衷。

1
Func_div : [1, 2]
@After 后置通知

@After() 注解:声明一个切入点的后置通知方法,其在目标对象方法的 finally{} 子句中执行。

1
2
3
4
5
6
7
8
9
@Component
@Aspect
public class LogAspect {

@After(value = "execution(public int fr.spring_aop.gdai.Implements.CalculateImpl.div(int, int)))")
public void after() {
System.out.println("后置通知");
}
}
1
2
3
4
5
6
@Test
public void testDiv() {
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
Calculate calculate = context.getBean(Calculate.class);
calculate.div(2,1);
}
1
2
Func_div : [2, 1]
Finally : 后置通知
@AfterReturning 返回通知

@AfterReturning() 注解:声明一个切入点的返回通知方法,在被代理的目标方法返回值之后执行。

  • returning = "value" 属性:可以将目标方法的返回值以 value 的形式作为参数传入返回通知方法中
1
2
3
4
5
6
7
8
9
@Component
@Aspect
public class LogAspect {

@AfterReturning(value = "execution(public int fr.spring_aop.gdai.Implements.CalculateImpl.div(int, int)))", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result){
System.out.println("Result : " + result);
}
}
1
2
3
4
5
6
@Test
public void testDiv() {
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
Calculate calculate = context.getBean(Calculate.class);
calculate.div(2,1);
}
1
2
3
Func_div : [2, 1]
Finally : 后置通知
Result : 2
@AfterThrowing 异常通知

@AfterReturning() 注解:声明一个切入点的异常通知方法,在被代理的目标方法抛出异常之后执行。

  • throwing = "exception" 属性:可以将目标方法的抛出的异常exception 的形式作为参数传入返回通知方法中
1
2
3
4
5
6
7
8
9
@Component
@Aspect
public class LogAspect {
@AfterThrowing(value = "pointCut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
Signature signature = joinPoint.getSignature();
System.out.println("出现异常,方法名 : "+signature.getName()+"\t异常信息 : "+e);
}
}
1
2
3
4
5
6
@Test
public void testDiv() {
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
Calculate calculate = context.getBean(Calculate.class);
calculate.div(2,0);
}
1
2
3
Func_div : [2, 0]
Finally : 后置通知
出现异常,方法名 : div 异常信息 : java.lang.ArithmeticException: / by zero

@Around 环绕通知

@Around() 注解:声明一个切入点的环绕通知方法。顾名思义,即目标方法的前后执行的通知方法(以上四种通知方法的结合)。

  • ProceedingJoinPoint 对象:由于是对于一个切入点的环绕通知,那么我们就需要知道这个接入点 JoinPoint 是在何时执行。即需要 ProceedingJoinPoint 对象的 proceed() 方法来表示接入点方法的执行位置
  • 环绕通知方法的返回值必须与目标方法的返回值 一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
@Aspect
public class LogAspect {
@Around("execution(* fr.spring_aop.gdai.Implements.CalculateImpl.div(..))")
public Object around(ProceedingJoinPoint joinPoint) {
Object result = null;
try {
System.out.println("@Around_前置通知 : Func_" + joinPoint.getSignature().getName() + Arrays.toString(joinPoint.getArgs()));
result = joinPoint.proceed();
System.out.println("@Around_返回通知 : Result = " + result);
} catch (Throwable e) {
System.out.println("@Around_异常通知 : " + e);
} finally {
System.out.println("@Around_后置通知 : 后置通知");
}
return result;
}
}
1
2
3
4
5
@Test
public void testMultiAround() {
Calculate calculate = context.getBean(Calculate.class);
calculate.div(2,1);
}
1
2
3
@Around_前置通知 : Func_multi[2, 1]
@Around_返回通知 : Result = 2
@Around_后置通知 : 后置通知
多个切面的优先级

只需要使用注解 @Order 在切面类的注解上即可,数值越小优先级越高

  • 默认值是 Integer.MAX_VALUE

四、声明式事务

1 JDBCTemplate

1.1 简介

Spring 对 JDBC 进行封装,使用 JDBCTemplate 方便实现数据库操作。

1.2 准备工作

(1) 导入 maven 坐标
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
<!--        mysql 驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>

<!-- c3p0 连接池-->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>

<!-- spring 核心包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.1</version>
</dependency>

<!-- spring 的对象关系映射 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.3</version>
</dependency>

<!-- spring 整合junit-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.5.RELEASE</version>
</dependency>

<!-- junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
(2) 编写实体类与创建数据库表

所有实体类我们都习惯性的将它们放在 dao 这个包下。

例:User.java

1
2
3
4
5
6
7
8
9
10
11
12
package fr.gdai.spring.transaction.domain;

public class User {
private Integer id;
private String name;
private String password;
private Integer age;
private String gender;
private String email;

// 省略Getter、Setter、无参构造、有参构造、toString方法
}

创建数据库:

1
mysql -h [数据库的主机名] -u [数据库用户名] -P [端口] -p
1
2
3
4
5
6
7
8
9
10
create database mybatis_test if not exists default charset utf8;
use mybatis_test;
create table t_user (
id int auto_increment primary key comment '用户ID',
username varchar(20) comment '用户名',
password varchar(20) comment '用户密码',
age int comment '用户年龄',
gender varchar(1) comment '用户性别',
email varchar(20) comment '用户邮箱'
) comment 'test';
(3) 配置 Spring

我们依旧使用纯注解的形式设置 Spring:

SpringConfig.java

1
2
3
4
5
6
package fr.gdai.spring.transaction.configs;

@Configuration
@Import({DataSourceUtil.class, SpringJDBC.class})
public class SpringConfig {
}

SpringJDBC.java

1
2
3
4
5
6
7
8
9
10
11
package fr.gdai.spring.transaction.configs;

public class SpringJDBC {

@Bean
public static JdbcTemplate getJdbcTemplate(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
}

DataSourceUtil.java

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
package fr.gdai.spring.transaction.configs;

@PropertySource("classpath:jdbc.properties")
public class DataSourceUtil {
@Value("${jdbc.driver}")
String jdbcDriver;

@Value("${jdbc.url}")
String jdbcURL;

@Value("${jdbc.username}")
String jdbcUser;

@Value("${jdbc.password}")
String jdbcPassword;

@Bean
public DataSource getDataSource() throws PropertyVetoException {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setDriverClass(jdbcDriver);
dataSource.setJdbcUrl(jdbcURL);
dataSource.setUser(jdbcUser);
dataSource.setPassword(jdbcPassword);
return dataSource;
}

}

jdbc.properties :用来配置 JDBC 连接数据库的参数

1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis_test
jdbc.username=root
jdbc.password=root
(3) 测试连接(Spring 整合 JUnit)

在这里,我们使用 Spring 整合 JUnit 测试。意味着我们可以注入的方式直接获取 IOC 容器中的 Bean,而不再通过 ApplicationContext.getBean() 来获取 Bean 对象

需要的配置如下:

  • @RunWith(SpringJUnit4ClassRunner.class):指定当前测试类在 Spring 整合 JUnit 的测试环境中执行。
  • @ContextConfiguration(classes = {SpringConfig.class}):然后导入 Spring 核心配置
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
package fr.gdai.spring.transaction;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringConfig.class})
public class JdbcTemplateTest {

@Autowired
private JdbcTemplate jdbcTemplate;

@Test
public void testInsert() {
String sql = "insert into t_user values(?, ?, ?, ?, ?, ?)";
jdbcTemplate.update(sql, 99,"test", "test", 1, "男", "test@test.com");
}

@Test
public void testSelectById() {
String sql = "select * from t_user where id = ?";
User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), 1);
System.out.println(user.toString());
}

@Test
public void testSelectAll() {
String sql = "select * from t_user";
List<User> userList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
userList.forEach(System.out::println);
}

@Test
public void testSelectCount() {
String sql = "select count(*) from t_user";
Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
System.out.println(count);
}
}

2 声明式事务概念

2.1 编程式事务

在以往对于事务的实现中,事务功能的相关操作全部通过自己编写代码来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Connection con = new ...;
try{
// 开启事务,关闭自动提交
con.setAutoCommit(false);

// TODO:核心操作

//提交事务
con.commit();
} catch(Exception e) {
// 事务回滚
con.rollBack();
} finally {
// 释放连接
con.close();
}

我们可以看出,编程式事务有不少缺点:

  1. 细节没有被屏蔽 :所有细节都要程序员自己来实现,比较繁琐。
  2. 代码复用性不高 :没有有效抽取出来,每次实现功能都需要自己编写代码,代码没有得到复用。

2.2 声明式事务

我们为了解决编程式事务的缺点,提出了声明式事务。

声明式依托框架完成,框架将固定模式的事务代码抽取出来,进行相关的封装。封装完成后,只需要配置或注解让框架完成事务的管理。我们可以把对事务的管理AOP 结合起来。

优点:

  1. 提高开发效率
  2. 消除冗余重复的代码
  3. 框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化。

3 基于注解的声明式事务

3.1 准备工作

(0) 继续开发

我们在第一节 JDBCTemplate 的基础上继续开发。

(1) 添加 domain 实体类与表

User.java

1
2
3
4
5
6
7
8
9
10
11
12
package fr.gdai.spring.transaction.domain;

public class User {
private Integer id;
private String username;
private String password;
private Integer age;
private String gender;
private String email;
private Integer balance;
// 省略Getter、Setter、无参构造、有参构造、toString方法
}

修改 t_user 表,添加 balance 字段,并添加一条测试用的数据。

1
2
alter table t_use add balance int unsigned default null comment '余额(无符号)';
insert into t_user values(99, 'test', 'test', 1, '男', 'test@test.com', 50);

Book.java

1
2
3
4
5
6
7
8
9
10
package fr.gdai.spring.transaction.domain;

public class Book {
private Integer bookId;
private String bookName;
private Integer price;
private Integer stock;
// 省略Getter、Setter、无参构造、有参构造、toString方法
}

新增 t_book 表,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
-- auto-generated definition
create table t_book
(
book_id int auto_increment comment '图书ID'
primary key,
book_name varchar(20) null comment '图书名称',
price int null comment '图书价格',
stock int unsigned null comment '库存(无符号)'
)
comment '图书表';

insert into t_book values (null, 'L_etranger', 100, 100);
insert into t_book values (null, 'Nausea', 100, 200);
(2) 添加 dao 组件

创建 BookDao.java

1
2
3
4
5
6
package fr.gdai.spring.transaction.dao;

public interface BookDao {
Integer getBookPriceByBookId(Integer bookId);
void updateBookStock(Integer bookId);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package fr.gdai.spring.transaction.dao.impl;

@Repository
public class BookDaoImpl implements BookDao {

@Autowired
private JdbcTemplate jdbcTemplate;

// 通过图书的ID查询价格
@Override
public Integer getBookPriceByBookId(Integer bookId) {
String sql = "select price from t_book where book_id = ?";
return jdbcTemplate.queryForObject(sql, Integer.class, bookId);
}

// 通过图书ID更新库存
@Override
public void updateBookStock(Integer bookId) {
String sql = "update t_book set stock = stock - 1 where book_id = ?";
jdbcTemplate.update(sql, bookId);
}

}

创建 UserDao.java

1
2
3
4
5
package fr.gdai.spring.transaction.dao;

public interface UserDao {
void updateUserBalance(Integer userId, Integer bookPrice);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package fr.gdai.spring.transaction.dao.impl;

@Repository
public class UserDaoImpl implements UserDao {

@Autowired
private JdbcTemplate jdbcTemplate;

// 更新用户余额
@Override
public void updateUserBalance(Integer userId, Integer bookPrice) {
String sql = "update t_user set balance = balance - ? where id = ?";
jdbcTemplate.update(sql, bookPrice, userId);
}
}

(3) 添加 service 组件

创建 BookService.java

1
2
3
4
5
package fr.gdai.spring.transaction.service;

public interface BookService {
void buyBook(Integer userId, Integer bookId);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package fr.gdai.spring.transaction.service.impl;

@Service
public class BookServiceImpl implements BookService {

@Autowired
private BookDao bookDao;
@Autowired
private UserDao userDao;

@Override
public void buyBook(Integer userId, Integer bookId) {
// 1 查询图书价格
Integer bookPrice = bookDao.getBookPriceByBookId(bookId);
// 2 更新图书的库存
bookDao.updateBookStock(bookId);
// 3 更新用户余额
userDao.updateUserBalance(userId, bookPrice);
}
}
(4) 添加 controller 组件

创建 BookContraller.java

1
2
3
4
5
6
7
8
9
10
11
12
package fr.gdai.spring.transaction.controller;

@Controller
public class BookController {

@Autowired
private BookService bookService;

public void buyBook(Integer userId, Integer bookId) {
bookService.buyBook(userId, bookId);
}
}

3.2 事务相关的测试

我们使用 Spring 结合 JUnit 的测试方法来测试当一个用户的余额 < 图书单价时,数据库的更新情况。

(1) 无事务功能的测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package fr.gdai.spring.transaction.service.impl;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringConfig.class})
public class BookServiceImplTest {
@Autowired
private BookServiceImpl bookService;

@Test
public void buyBook() {
bookService.buyBook(99,1);
}
/**
* User.id=99, User.balance=50;
* Book.id=1, Book.price=100;
*/
}

此时我们来检索数据库:

1
2
select t_user.id user_id, t_user.balance, t_book.book_id, t_book.price, t_book.stock 
from t_book, t_user where t_user.id=99 and t_book.book

查询结果如下,我们可以看到,图书的库存 stock 数量 -1,但是用户的余额没有变化

1
2
3
4
5
+---------+---------+---------+-------+-------+
| user_id | balance | book_id | price | stock |
+---------+---------+---------+-------+-------+
| 99 | 50 | 1 | 100 | 99 |
+---------+---------+---------+-------+-------+

在当前没有手动设置事务时,默认的每条 SQL 语句都是一个事务,三个 SQL 语句各自独立。所以库存数量减少了,余额却没有变化。

(2) 实现事务的测试
  1. 配置事务管理器

    TransactionManagerUtil

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package fr.gdai.spring.transaction.configs;

    public class TransactionManagerUtil {

    @Bean
    TransactionManager getTxManager(DataSource dataSource) {
    TransactionManager txManager = new DataSourceTransactionManager(dataSource);
    return txManager;
    }
    }

    SpringConfig.java 中添加以下注解,以开启事务的注解驱动。设置成功后可以使用 @Transactional 注解声明该方法或类中所有方法使用事务进行管理

    1
    @EnableTransactionManagement

    或使用 .xml 注解的方式开启事务

    1
    2
    3
    4
    5
    6
    7
    8
    <!-- 配置事务管理器 -->
    <bean id="transactionManager"
    class="org.springframework.transaction.annotation.EnableTransactionManagement">
    <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 开启事务的注解驱动 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
  2. Service 层设置开启事务。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package fr.gdai.spring.transaction.service.impl;

    // @Transactional 说明类中所有方法都开始事务管理
    @Service
    public class BookServiceImpl implements BookService {
    ...

    @Transactional
    @Override
    public void buyBook(Integer userId, Integer bookId) {...}
    }
  3. 再次执行测试 buyBook(),并检索数据库。此时我们发现事务已经生效。

    1
    org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback;
    1
    2
    3
    4
    5
    +---------+---------+---------+-------+-------+
    | user_id | balance | book_id | price | stock |
    +---------+---------+---------+-------+-------+
    | 99 | 50 | 1 | 100 | 100 |
    +---------+---------+---------+-------+-------+

3.3 设置事务的属性

只读 onlyRead

对一个查询操作来说,我们设置成只读 @readOnly(默认 false ),就能明确告诉数据库这个操作不涉及写操作。这样数据库针对查询操作就能进行优化。

1
@Transactional(readOnly=true)

如果在非查询操作的事务中使用只读属性,会抛出以下错误

1
nested exception is java.sql.SQLException: Connection is read-only.
超时 timeout

事务的执行过程中,当程序阻塞住了,从而长时间占用数据库资源(大概率是 Java程序 或者 MySQL 数据库链接等等)。 这个时候设置超时(默认值为 -1),可以在规定时间到达后立马回滚,将资源释放出来,并抛出 TransactionTimedOutException超时回滚,释放资源,抛出异常

1
@Transactional(timeout = second)

超时后,抛出的异常如下

1
org.springframework.transaction.TransactionTimedOutException: Transaction timed out
回滚策略 rollback

声明式事务默认只针对运行时异常回滚编译时异常不会滚

  • roollbackForClassName = "异常对应的全类名":一般不使用,因为默认运行时异常回滚
  • roollbackFor = 异常类.class:一般不使用
  • noRoollbackForClassName:当出现发生异常时不回滚
  • noRoollbackFor:同上
事务隔离级别 isolation 🌟

课程回顾:并发事务的隔离性与隔离级别

总而言之,一个事务与另一个事务的隔离程度称为隔离级别,当隔离级别越高,数据的一致性就越好,但并发性越弱

使用方式:

1
@Transaction(isolation = Isolation.DEFAULT) // 数据库默认的隔离级别,其他可选项如下
  • READ UNCOMMITTED读未提交。允许 Transaction1Transaction_1 读取 Transaction2Transaction_2 未提交的修改。
  • READ COMMITTED读已提交Transaction1Transaction_1 只能读取 Transaction2Transaction_2 提交的修改。
  • REPEATABLE READ可重复读Transaction1Transaction_1 可以重复多次一个字段中读取到相同的值。即在 Transaction1Transaction_1 执行期内禁止其他事务对该【字段】更新
  • SERIALIZABLE串行化。执行结果跟串行执行一样,即在 Transaction1Transaction_1 执行期内禁止其他事务对该【表】更新
隔离级别 关键字 脏读 不可重复读 幻读
读未提交 READ UNCOMMITTED
读已提交(oracal 默认隔离级别) READ COMMITTED
可重复读(mySQL 默认隔离级别) REPEATABLE READ
串行化 SERIALIZABLE
传播行为 propegation

A 方法 调用 B 方法,且两者都声明了事务,我们从B 的角度来思考:

  • B 使用自己的事务 : 如果 B 回滚了,不会影响 A 的回滚propagation = Propagation.REQUIRED_NEW
  • B 使用 A 的事务(默认):如果 B 发生回滚了,则 A 也会回滚propagation = Propagation.REQUIRED

想象一下这样一个场景,如今我们的账户余额 balance = 150,我们将如下两本书加入了购物车:A.price = 100B.price = 69 。在最后结账的时候,假设从购物车付款的逻辑为

1
2
3
4
5
6
@Transactional(propagation = Propagation.REQUIRED)
public void checkOut(Integer userId, Integer[] bookIds) {
for(Integer bookId : bookIds) {
buyBook(userId, bookId); // 这个方法同样有 @Transactional
}
}

因为余额 balance = 50 不足以支付 B.price = 69 的价格,所以该次 buyBook() 操作回滚。从而导致 checkOut() 方法也回滚。

3.4 基于 .xml 的声明式事务

哔哩哔哩:【尚硅谷】Spring:基于xml的声明式事务