Spring基础学习笔记

学习来源:

学习时间:2021年2月4日,2023年6月12日

1 Spring简介

  1. 2002年,首次推出了Spring框架的雏形,interface21框架

  2. 2004年3月24日,正式发布了1.0版本

  3. 创始人Rod Johnson

  4. Spring maven依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
  1. 优点:
  • Spring是一个开源的免费的容器
  • Spring是一个轻量级的、非入侵的框架
  • 控制反转IOC,面向切面编程AOP
  • 支持事务的处理,对框架整合的支持

Spring 的核心是一个 容器,通常称为 Spring 应用程序上下文(Application Context),用于创建和管理应用程序组件。这些组件(或 bean)在 Spring 应用程序上下文中连接在一起以构成一个完整的应用程序。

将 bean 连接在一起的行为是基于一种称为 依赖注入(DI)的模式。依赖项注入的应用程序不是由组件自身创建和维护它们依赖的其他 bean 的生命周期,而是依赖于单独的实体(容器)来创建和维护所有组件,并将这些组件注入需要它们的 bean。通常通过构造函数参数或属性访问器方法完成此操作。

例如,假设在应用程序的许多组件中,要处理两个组件:inventory service(用于获取库存级别)和 product service(用于提供基本产品信息)。product service 取决于 inventory service,以便能够提供有关产品的完整信息。下图说明了这些 bean 与 Spring 应用程序上下文之间的关系。

image-20230612162929669

2 Spring的组成及拓展

image-20211223165922731

3 IOC控制反转

3.1 程序的耦合

3.1.1 概念

耦合性(Coupling),也叫耦合度,是对模块间关联程度的度量。耦合的强弱取决于模块间接口的复杂性、调用模块的方式以及通过界面传送数据的多少。模块间的耦合度是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系模块间联系越多,其耦合性越强,同时表明其独立性越差( 降低耦合性,可以提高其独立性)。耦合性存在于各个领域,而非软件设计中独有的,但是我们只讨论软件工程中的耦合在软件工程中,耦合指的就是就是对象之间的依赖性。对象之间的耦合越高,维护成本越高。因此对象的设计应使类和构件之间的耦合最小。软件设计中通常用耦合度和内聚度作为衡量模块独立程度的标准。划分模块的一个准则就是高内聚低耦合。

1) 耦合的分类

(1)内容耦合。当一个模块直接修改或操作另一个模块的数据时,或一个模块不通过正常入口而转入另一个模块时,这样的耦合被称为内容耦合。内容耦合是最高程度的耦合,应该避免使用之。

(2)公共耦合。两个或两个以上的模块共同引用一个全局数据项,这种耦合被称为公共耦合。在具有大量公共耦合的结构中,确定究竟是哪个模块给全局变量赋了一个特定的值是十分困难的。

(3)外部耦合 。一组模块都访问同一全局简单变量而不是同一全局数据结构,而且不是通过参数表传递该全局变量的信息,则称之为外部耦合。

(4)控制耦合 。一个模块通过接口向另一个模块传递一个控制信号,接受信号的模块根据信号值进行适当的动作,这种耦合被称为控制耦合。

(5)标记耦合 。若一个模块 A 通过接口向两个模块 B 和 C 传递一个公共参数,那么称模块 B 和 C 之间存在一个标记耦合。

(6)数据耦合。模块之间通过参数来传递数据,那么被称为数据耦合。数据耦合是最低的一种耦合形式,系统中一般都存在这种类型的耦合,因为为了完成一些有意义的功能,往往需要将某些模块的输出数据作为另一些模块的输入数据。

(7)非直接耦合 。两个模块之间没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的。

总结

耦合是影响软件复杂程度和设计质量的一个重要因素,在设计上我们应采用以下原则

如果模块间必须存在耦合,就尽量使用数据耦合,少用控制耦合,限制公共耦合的范围,尽量避免使用内容耦合

2) 内聚与耦合

内聚标志一个模块内各个元素彼此结合的紧密程度,它是信息隐蔽和局部化概念的自然扩展。内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述的是模块内的功能联系。耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。 程序讲究的是低耦合,高内聚。就是同一个模块内的各个元素之间要高度紧密,但是各个模块之间的相互依存度却要不那么紧密。

内聚和耦合是密切相关的,同其他模块存在高耦合的模块意味着低内聚,而高内聚的模块意味着该模块同其他模块之间是低耦合。在进行软件设计时,应力争做到高内聚,低耦合

3) 解耦思路

耦合:程序之间的依赖关系。包括类之间的依赖和方法之间的依赖。

解耦:降低程序之间的依赖关系。

实际开发中应该做到:编译期不依赖,运行时才依赖

解耦思路:

  • 第一步:使用反射来创建对象,而避免使用new关键字
  • 第二步:通过读取配置文件,来获取要创建的对象全限定类名
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
public class JdbcDemo {
public static void main(String[] args) throws SQLException {
// 1.注册驱动
// 方法一:此处编译期依赖了外部的jar包
// DriverManager.registerDriver(new com.mysql.jdbc.Driver());

// 方法二:利用反射,减少耦合
Class.forName("com.mysql.jdbc.Driver");

// 2.获取连接
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","123456");
// 3.获取操作数据库的预处理对象
PreparedStatement preparedStatement= con.prepareStatement("select * from account");
// 4.执行sql,得到结果集
ResultSet resultSet=preparedStatement.executeQuery();
// 5.遍历结果集
while (resultSet.next()){
System.out.println(resultSet.getString("name"));
}
// 6.释放资源
resultSet.close();
preparedStatement.close();
con.close();
}
}

3.1.2 程序实例

账户持久层接口

1
2
3
4
5
6
public interface IAccountDao {
/**
* 模拟保存
*/
void saveAccount();
}

账户持久层实现类

1
2
3
4
5
public class AccountDaoImpl implements IAccountDao {
public void saveAccount() {
System.out.println("保存了账户");
}
}

业务层接口

1
2
3
4
5
6
public interface IAccountService {
/**
* 模拟保存账户
*/
void saveAccount();
}

业务层实现类

1
2
3
4
5
6
7
8
9
public class AccountServiceImpl implements IAccountService {
// 业务层调用持久层 强依赖
// 多态,父类引用指向子类对象
private IAccountDao accountDao = new AccountDaoImpl();

public void saveAccount() {
accountDao.saveAccount();
}
}

表现层

1
2
3
4
5
6
7
public class Client {
public static void main(String[] args) {
//表现层调用业务层 强依赖
IAccountService iAccountService = new AccountServiceImpl();
iAccountService.saveAccount();
}
}

3.1.3 工厂模式解耦

1) bean

Bean有可重用组件的含义。Java bean >> 实体类。Java bean是用Java语言编写的可重用组件,它就是创建service和dao对象的。它:

  • 需要一个配置文件来配置service和dao,该配置文件的内容至少应包含:唯一标志-全限定类名(key-value形式)
  • 通过读取配置文件中配置的内容,反射创建对象
  • 配置文件可以是xmlproperties
2) 工厂模式实例

配置文件bean.properties

1
2
accountService = com.hongyi.service.impl.AccountServiceImpl
accountDao = com.hongyi.dao.impl.AccountDaoImpl

BeanFactory.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
/**
* 一个创建bean对象的工厂
*/
public class BeanFactory {
// 定义一个properties对象
private static Properties props;
// 使用静态代码块为Properties对象赋初值
static {
try {
// 实例化对象
props = new Properties();
// 获取properties文件的流对象
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
props.load(in);
} catch (IOException e) {
throw new ExceptionInInitializerError("初始化Properties失败");
}
}

/**
* 根据bean的名称,获取bean对象
* @param beanName
* @return
*/
public static Object getBean(String beanName){
Object bean = null;
try{
String beanPath = props.getProperty(beanName);
// 利用反射的方式来创建对象
// 每次都会调用默认构造函数创建对象
bean = Class.forName(beanPath).newInstance();
} catch (Exception e){
e.printStackTrace();
}
return bean;
}
}

表现层代码改造

1
2
3
4
5
6
public class Client {
public static void main(String[] args) {
IAccountService iAccountService = (IAccountService) BeanFactory.getBean("accountService");
iAccountService.saveAccount();
}
}

业务层实现类代码改造

1
2
3
4
5
6
7
public class AccountServiceImpl implements IAccountService {
//业务层调用持久层
private IAccountDao accountDao = (IAccountDao) BeanFactory.getBean("accountDao");
public void saveAccount() {
accountDao.saveAccount();
}
}
3) 工厂模式实例问题分析及改造

单例和多例对象的区别

单例:对象只被创建一次,从而类中的成员只会初始化一次。会有线程安全问题。

多例:对象被创建多次,执行效率没有单例高,但没有线程安全问题。

前述的工厂模式是多例模式,但可采用单例模式进行创建对象(原因?)。

改造为单例模式的BeanFactory.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
/**
* 一个创建bean对象的工厂
*/
public class BeanFactory {
// 定义一个properties对象
private static Properties props;

// 定义一个map,用于存放我们要创建的对象,我们把它称之为容器。
private static Map<String, Object> beans;

// 使用静态代码块为Properties对象赋初值
static {
try {
// 实例化对象
props = new Properties();
// 获取properties文件的流对象
InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
props.load(in);
// 实例化容器
beans = new HashMap<String, Object>();
// 取出配置文件中所有的key
Enumeration keys = props.keys();
// 遍历枚举
while (keys.hasMoreElements()) {
// 取出每个key
String key = keys.nextElement().toString();
// 根据key获取value
String beanPath = props.getProperty(key);
// 反射创建对象
Object value = Class.forName(beanPath).newInstance();
// 把key和value存储容器中
beans.put(key,value);
}
} catch (Exception e) {
throw new ExceptionInInitializerError("初始化Properties失败");
}
}

/**
* 根据bean的名称,获取bean对象 单例
* @param beanName
* @return
*/
public static Object getBean(String beanName){
return beans.get(beanName);
}
}

3.2 IOC简介

3.2.1 概念

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。

3.2.2 作用

削减计算机程序的耦合,降低代码中的依赖关系。

3.3 Spring中的IOC

采用配置的方式替代之前的编码工作。

3.3.1 IOC基本依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>

依赖关系图

image-20211215195631106

3.3.2 核心容器的创建

  • bean.xml配置文件
1
2
3
4
5
6
7
8
9
10
11
<?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">
<!--把对象的创建交给Spring管理-->
<!--id任意 class是全限定类名-->
<bean id="accountService" class="com.hongyi.service.impl.AccountServiceImpl"></bean>

<bean id="accountDao" class="com.hongyi.dao.impl.AccountDaoImpl"></bean>
</beans>
  • Client.java代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {
/**
* 获取spring的ioc核心容器,并根据id获取对象
* @param args
*/
public static void main(String[] args) {
// 1.获取核心容器对象
//ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");//方法1
ApplicationContext ac = new FileSystemXmlApplicationContext("E:\\develop\\ioc_demo1\\src\\main\\resources\\bean.xml");//方法2

// 2.根据id获取bean对象
IAccountService accountService = (IAccountService) ac.getBean("accountService");//方法1
IAccountDao accountDao = ac.getBean("accountDao",IAccountDao.class);//方法2
System.out.println(accountService);
}
}

ApplicationContext三个常用实现类

  • ClassPathXmlApplicationContext:它可以加载类路径下的配置文件,要求配置文件必须在类路径下,否则加载不了。更常用
  • FileSystemXMLApplicationContext:它可以加载磁盘任意路径下的配置文件(前提是必须有访问权限)。采用绝对路径。不常用
  • AnnotationConfigApplicationContext:它是用于读取注解创建容器的。最常用

ApplicationContext继承关系

image-20211215195640171

核心容器的两个接口引发出的问题

  • ApplicationContext:它在构造核心容器时,创建对象采取的策略是采用立即加载的方式,只要一读取配置文件马上就创建配置文件中配置的对象。适用于单例对象的场景。

    1
    ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");

    这行代码一执行完毕就创建好了对象。

  • BeanFactory:它在构造核心容器时,创建对象采取的策略是采用延迟加载的方式。也就是说,什么时候根据id获取对象了,什么时候才真正地创建对象。适用于多例对象的场景。

    1
    2
    3
    Resource resource = new ClassPathResource("bean.xml");
    BeanFactory factory = new XmlBeanFactory(resource);
    IAccountService as = (IAccountService)factory.getBean("accountService");

    直到第三行代码被执行时,才创建对象。

实际开发中,采用ApplicationContext更多,Spring会自动的选择是否是即时加载还是延时加载。

3.3.3 Bean对象管理的细节

1) 创建bean的三种方式
  1. 使用默认构造函数创建:在spring的配置文件中,使用bean标签,配以id和class属性之后,且没有其他属性和标签时,采用的就是默认构造函数创建bean对象。此时如果类中没有默认构造函数,则对象无法创建。
1
<bean id="accountService" class="com.hongyi.service.impl.AccountServiceImpl"></bean>
  1. 使用普通工厂中的方法创建对象:使用某个类中的方法创建对象,并存入spring容器。
1
2
<bean id="instanceFactory" class="com.hongyi.factory.InstanceFactory"></bean>
<bean id="accountService" factory-bean="instanceFactory" factory-mothod="getAccountService"></bean>

InstanceFactory.java代码

1
2
3
4
5
public class instanceFactory {
public IAccountService getAccountService(){
return new AccountServiceImpl();
}
}
  1. 使用静态工厂中的静态方法创建对象:使用某个类中的静态方法创建对象,并存入spring容器中。
1
<bean id="accountService" class="com.hongyi.factory.StaticFactory" factory-mothod="getAccountService"></bean>

StaticFactory.java代码

1
2
3
4
5
public class StaticFactory {
public static IAccountService getAccountService(){
return new AccountServiceImpl();
}
}
2) bean的作用范围

bean标签的scope属性:用于指定bean的作用范围。取值有:常用的就是前两个。spring 5.x已不支持后三种了。

1
<bean id="accountService" class="com.hongyi.service.impl.AccountServiceImpl" scope="prototype"></bean>
  • singleton:默认,单例
  • prototype:多例
  • request:作用于web应用的请求范围
  • session:作用于web应用的会话范围
  • global-session:作用于集群环境的会话范围(全局会话范围),当不是集群环境时,作用同session一样。

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {
/**
* 获取spring的ioc核心容器,并根据id获取对象
* @param args
*/
public static void main(String[] args) {
// 1.获取核心容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");

// 2.根据id获取bean对象
IAccountService as1 = (IAccountService) ac.getBean("accountService");
IAccountService as2 = (IAccountService) ac.getBean("accountService");
// 比较两次创建的bean对象是否相同
System.out.println(as1 == as2); // true
}
}
3) bean的生命周期

单例对象

  • 出生:当容器创建时出生
  • 活着:只要容器还在,就活着
  • 死亡:容器一销毁,对象就死亡
  • 总结:单例对象的生命周期同容器的一致

编码测试

1
<bean id="accountService" class="com.hongyi.service.impl.AccountServiceImpl" init-method="init" destroy-method="destroy"></bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AccountServiceImpl implements IAccountService {

public void saveAccount() {
System.out.println("service中的saveAccount方法执行了");
}

public void init(){
System.out.println("对象初始化了");
}

public void destroy(){
System.out.println("对象销毁了");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
// 1.获取核心容器对象
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");

// 2.根据id获取bean对象
IAccountService as = (IAccountService) ac.getBean("accountService");
as.saveAccount();

// 手动关闭容器
ac.close();
}
}

执行结果:

image-20211215195648599

多例对象

  • 出生:当我们要使用对象时,spring为我们创建
  • 活着:在使用过程中,就一直活着
  • 死亡:当对象长时间不被使用,且没有别的对象引用时,则由Java的垃圾回收机制回收

3.3.4 依赖注入 DI

1) 概念

依赖注入:Dependency Injection。依赖关系的管理都交由spring来管理。在当前类需要用到其他类的对象,由spring为我们提供,我们只需要在配置文件中说明。依赖关系的维护就叫做依赖注入。

依赖注入的数据

  • 基本类型和String
  • 其他bean类型(在配置文件中或注解配置过的bean)
  • 复杂类型/集合类型

注入方式

  • 使用构造函数提供
  • 使用set方法提供
  • 使用注解提供
2) 构造函数注入

注入示例代码

1
2
3
4
5
6
7
<bean id="accountService" class="com.hongyi.service.impl.AccountServiceImpl">
<constructor-arg name="name" value="hongyi"></constructor-arg>
<constructor-arg name="age" value="18"></constructor-arg>
<constructor-arg name="birthday" ref="now"></constructor-arg>
</bean>
<!-- 配置一个日期对象 -->
<bean id="now" class="java.util.Date"></bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AccountServiceImpl implements IAccountService {
// 如果是经常变化的数据,并不适用于注入的方式
private String name;
private Integer age;
private Date birthday;

public AccountServiceImpl(String name, Integer age, Date birthday) {
this.name = name;
this.age = age;
this.birthday = birthday;
}

public void saveAccount() {
System.out.println("service中的saveAccount方法执行了"+name+","+age+","+birthday);
}
}

方法:使用的标签是constructor-arg;位于bean标签的内部;

标签中的属性:

  • type:用于指定要注入的数据的数据类型,该数据类型也是构造函数中某个或某些参数的类型
  • index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值。索引的位置从零开始
  • name:用于指定给构造函数中指定名称的参数赋值。以上三个用于指定给构造函数中哪个参数赋值。常用的是name。
  • value:用于提供基本类型和String类型的数据
  • ref:用于指定其他bean类数据。他指的就是在spring的ioc容器中出现的bean对象。

优势

在获取bean对象时,注入数据是必须的操作,否则对象无法创建成功。

弊端

改变了bean对象的实例化方式,使我们在创建对象时,如果用不到这些数据,也必须提供。

这种注入方式不常用

3) set方法注入

编码测试

1
2
3
4
5
6
7
8
<!-- 配置一个日期对象 -->
<bean id="now" class="java.util.Date"></bean>

<bean id="accountService2" class="com.hongyi.service.impl.AccountServiceImpl2">
<property name="age" value="21"></property>
<property name="name" value="hongyi"></property>
<property name="birthday" ref="now"></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 AccountServiceImpl2 implements IAccountService {
// 如果是经常变化的数据,并不适用于注入的方式
private String name;
private Integer age;
private Date birthday;

public void setName(String name) {
this.name = name;
}

public void setAge(Integer age) {
this.age = age;
}

public void setBirthday(Date birthday) {
this.birthday = birthday;
}

public void saveAccount() {
System.out.println("service中的saveAccount方法执行了"+name+","+age+","+birthday);
}
}

涉及的标签:property;出现的位置:bean标签的内部;属性有:

  • name:用于指定注入时所调用的set方法名称
  • value:用于提供基本类型和String类型的数据
  • ref:用于指定其他bean类数据。他指的就是在spring的ioc容器中出现的bean对象。

优势

创建对象时,没有明确的限制,可以直接使用默认构造函数。

弊端

如果有某个成员必须有值,则获取对象是有可能set方法没有执行。

这种方式更常用

采用set方法注入复杂类型

说明:用于给List结构集合注入的标签有list,array,set。用于给Map结构集合注入的标签有map,props。即结构相同,标签可以互换。

bean.xml

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
<bean id="accountService" class="com.hongyi.service.impl.AccountServiceImpl">
<property name="myStrs">
<!-- array可以更换为list或set标签 -->
<array>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</array>
</property>
<property name="myList">
<list>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</list>
</property>
<property name="mySet">
<set>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</set>
</property>
<property name="myMap">
<map>
<entry key="k1" value="v1"></entry>
<entry key="k2" value="v2"></entry>
<entry key="k3">
<value>v3</value>
</entry>
</map>
</property>
<property name="myProps">
<props>
<prop key="k1">prop-v1</prop>
<prop key="k2">prop-v2</prop>
</props>
</property>
</bean>

AccountServiceImpl.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
public class AccountServiceImpl implements IAccountService {

private String[] myStrs;
private List<String> myList;
private Set<String> mySet;
private Map<String,String> myMap;
private Properties myProps;

public void setMyStrs(String[] myStrs) {
this.myStrs = myStrs;
}

public void setMyList(List<String> myList) {
this.myList = myList;
}

public void setMySet(Set<String> mySet) {
this.mySet = mySet;
}

public void setMyMap(Map<String, String> myMap) {
this.myMap = myMap;
}

public void setMyProps(Properties myProps) {
this.myProps = myProps;
}

public void saveAccount() {
System.out.println(Arrays.toString(myStrs));
System.out.println(myList);
System.out.println(mySet);
System.out.println(myMap);
System.out.println(myProps);
}
}

Client.java

1
2
3
4
5
6
7
8
public class Client {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountService as = (IAccountService) ac.getBean("accountService");

as.saveAccount();
}
}

执行结果

image-20211215195657913

4) 基于注解的IOC配置

bean标签的主要形式

1
2
3
<bean id="" class="" scope="" init-method="" destroy-method="">
<property name="" value=""|ref=""></property>
</bean>
用于创建对象的注解

作用就和在xml配置文件中编写一个bean标签实现的作用是一样的。

@Component

作用:用于把当前类对象存入spring容器中

属性:value:用于指定bean的id,当不写时,默认值是当前类名首字母改小写。当只有一个value时,可以省略

bean.xml代码:用于扫描注解存在的包

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:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
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">

<!-- 告知spring在创建容器时要扫描的包,配置所需要的标签不是在beans的约束中,而是一个名称为context的名称空间和约束中 -->
<context:component-scan base-package="com.hongyi"></context:component-scan>
</beans>

AccountServiceImpl.java代码

1
2
3
4
5
6
7
8
9
@Component(value = "accountService")
// value可以省略@Component("accountService")
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao;

public void saveAccount() {
accountDao.saveAccount();
}
}

Client.java代码

1
2
3
4
5
6
7
public class Client {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountService as = (IAccountService) ac.getBean("accountService");
System.out.println(as);
}
}

@Controller,@Service,@Repository

以上三个注解,作用和属性与Component是相同的。它们三个是spring框架为我们提供明确的三层使用的注解,使我们的三层对象更加清晰。各用于表现层,业务层,持久层。

用于注入数据的注解

作用就和在bean标签中编写一个property标签实现的作用是一样的。

@Autowired

示例代码:

1
2
3
4
5
6
7
8
9
@Service(value = "accountService")
public class AccountServiceImpl implements IAccountService {
// 自动为accountDao注入数据,注入类型是IAccountDao
@Autowired
private IAccountDao accountDao;
public void saveAccount() {
accountDao.saveAccount();
}
}

IOC容器和注入图示:

image-20211215195707173


  • 作用:自动按照类型注入。只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功。

  • 出现位置:可以是变量上,也可以是方法上。

  • 细节:在使用注解注入时,set方法就不是必须的了。

  1. 如果IOC容器中没有任何bean的类型和要注入的变量类型匹配,则报错。
1
2
3
4
5
6
7
@Repository("accountDao")
// 若不是IAccountDao的接口实现类,则会注入失败
public class AccountDaoImpl { // implements IAccountDao {
public void saveAccount() {
System.out.println("保存账户");
}
}

image-20211215195714106

  1. 如果IOC容器中有多个类型匹配时:

首先按照类型(红色部分)进行匹配,再按照id(蓝色部分)进行匹配。若都不对应,则报错。

image-20211215195721840

@Qualifier

  • 作用:在按照类注入的基础上再按照名称注入。它在给类成员注入时不能单独使用。但是在给方法参数注入时可以。要和@Autowired搭配使用

  • 属性:value:用于指定注入bean的id。

1
2
3
4
5
6
7
8
9
@Service(value = "accountService")
public class AccountServiceImpl implements IAccountService {
@Autowired
@Qualifier("accountDao")
private IAccountDao accountDao;
public void saveAccount() {
accountDao.saveAccount();
}
}

@Resource

  • 作用:直接按照bean的id注入。可以独立使用。

  • 属性:name:用于指定bean的id。

1
2
3
4
5
6
7
8
@Service(value = "accountService")
public class AccountServiceImpl implements IAccountService {
@Resource(name = "accountDao")
private IAccountDao accountDao;
public void saveAccount() {
accountDao.saveAccount();
}
}

以上三个注解都只能注入其他bean类型的数据,而基本类型和String类型无法使用上述注解实现。此外,集合类型的注入只能通过XML来实现

@Value

  • 作用:用于注入基本类型和String类型的数据

  • 属性:value:用于指定数据的值。可以使用spring中的SpEL(spring中的el表达式),写法为:${表达式}

  • 代码示例:

1
2
@Value("${jdbc.driver}")
private String driver;
用于改变作用范围的注解

作用就和在bean标签中使用scope属性是一样的。

@Scope

作用:用于指定bean的作用范围

属性:value:指定范围的取值,取值有:singleton,prototype,对应单例和多例,默认单例

1
2
3
4
5
6
7
8
9
10
@Service(value = "accountService")
@Scope(value = "prototype")
public class AccountServiceImpl implements IAccountService {
@Autowired
private IAccountDao accountDao;

public void saveAccount() {
accountDao.saveAccount();
}
}
和生命周期相关的注解

作用就和在bean标签中使用init-method和destroy-method是一样的。了解。

3.3.5 采用xml配置的案例

  • bean.xml
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
<?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">
<!--配置Service对象-->
<bean id="accountService" class="com.hongyi.service.impl.AccountServiceImpl">
<!--注入dao对象-->
<property name="accountDao" ref="accountDao"></property>
</bean>

<!--配置Dao对象-->
<bean id="accountDao" class="com.hongyi.dao.impl.AccountDaoImpl">
<!--注入QueryRunner对象-->
<property name="runner" ref="runner"></property>
</bean>

<!--配置QueryRunner对象-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!--注入数据源-->
<constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>

<!--配置数据源-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--注入连接数据库的必备信息-->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property>
<property name="user" value="root"></property>
<property name="password" value="123456"></property>
</bean>
</beans>
  • AccountDaoImpl.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
39
40
41
42
43
44
45
46
47
48
public class AccountDaoImpl implements IAccountDao {

private QueryRunner runner;

public void setRunner(QueryRunner runner) {
this.runner = runner;
}

public Account findAccountById(Integer id) {
try {
return runner.query("select * from account where id = ?",new BeanHandler<Account>(Account.class),id);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public List<Account> findAllAccount() {
try {
return runner.query("select * from account",new BeanListHandler<Account>(Account.class));
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public void saveAccount(Account account) {
try {
runner.update("insert into account(name,money) values(?,?)",account.getName(),account.getMoney());
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public void updateAccount(Account account) {
try {
runner.update("update account set name = ?,money = ? where id = ?",account.getName(),account.getMoney(),account.getId());
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public void deleteAccount(Integer id) {
try {
runner.update("delete from account where id = ?",id);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
  • AccountTest.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
39
40
/**
* 使用JUnit单元测试 测试配置
*/
public class AccountServiceTest {

@Test
public void testFindAll() {
// 1.获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 2.得到业务层对象
IAccountService as = (IAccountService) ac.getBean("accountService");
// 3.执行方法
List<Account> allAccount = as.findAllAccount();
for(Account account : allAccount){
System.out.println(account);
}
}

@Test
public void testFindOne() {
// 1.获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 2.得到业务层对象
IAccountService as = (IAccountService) ac.getBean("accountService");
Account account = as.findAccountById(1);
System.out.println(account);
}

@Test
public void testSave() {
}

@Test
public void testUpdate() {
}

@Test
public void testDelete() {
}
}

3.3.6 采用注解配置的案例

  • bean.xml
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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
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="com.hongyi"></context:component-scan>
<!--配置QueryRunner对象-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!--注入数据源-->
<constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>

<!--配置数据源-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--注入连接数据库的必备信息-->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property>
<property name="user" value="root"></property>
<property name="password" value="123456"></property>
</bean>
</beans>
  • AccountDaoImpl.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
39
40
41
42
43
44
45
46
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {

@Autowired
private QueryRunner runner;

public Account findAccountById(Integer id) {
try {
return runner.query("select * from account where id = ?",new BeanHandler<Account>(Account.class),id);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public List<Account> findAllAccount() {
try {
return runner.query("select * from account",new BeanListHandler<Account>(Account.class));
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public void saveAccount(Account account) {
try {
runner.update("insert into account(name,money) values(?,?)",account.getName(),account.getMoney());
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public void updateAccount(Account account) {
try {
runner.update("update account set name = ?,money = ? where id = ?",account.getName(),account.getMoney(),account.getId());
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public void deleteAccount(Integer id) {
try {
runner.update("delete from account where id = ?",id);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
  • AccountServiceImpl.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
@Service("accountService")
public class AccountServiceImpl implements IAccountService {

@Autowired
private IAccountDao accountDao;

public Account findAccountById(Integer id) {
return accountDao.findAccountById(id);
}

public List<Account> findAllAccount() {
return accountDao.findAllAccount();
}

public void saveAccount(Account account) {
accountDao.saveAccount(account);
}

public void updateAccount(Account account) {
accountDao.updateAccount(account);
}

public void deleteAccount(Integer id) {
accountDao.deleteAccount(id);
}
}

3.3.7 Spring的其它注解

@Configuration

  • 作用:指定当前类是一个配置类。此时该类的作用相当于bean.xml

  • 细节:当配置类作为AnnotationConfigApplicationContext对象创建的参数时,该注解可以不写。

1
ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);

@ComponentScan

  • 作用:用于通过注解指定spring在创建容器时要扫描的包。

  • 属性:value:它和basePackages作用相同,都是用于指定创建容器时要扫描的包。使用此注解就等同于在xml中配置了:

1
<context:component-scan base-package="com.hongyi"></context:component-scan>

示例代码:

1
2
3
4
5
6
7
8
9
10
11
/**
* 该类是一个配置类,作用同bean.xml
*/
@Configuration
@ComponentScan(basePackages = {"com.hongyi"})
// 或当扫描的包只有一个时:
// @ComponentScan(basePackages = "com.hongyi")
// @ComponentScan("com.hongyi")
public class SpringConfiguration {

}

@Bean

  • 作用:用于把当前方法的返回值作为bean对象,存入spring的IOC容器中。

  • 属性:name:用于指定bean的id,当不注明时默认是当前方法的名称。

  • 细节:当我们使用注解配置方法时,如果方法有参数,spring框架会去容器中查找有没有可用的bean对象,查找方式同autowired注解的作用是一样的。当有以上三个注解组合使用时,xml配置文件中的所有内容都可以拿掉了。

  • 示例代码:

SpringConfiguration.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
/**
* 该类是一个配置类,作用同bean.xml
*/
@Configuration
@ComponentScan(basePackages = {"com.hongyi"})
public class SpringConfiguration {
/**
* 用于创建一个QueryRunner对象
* @param dataSource
* @return
*/
@Bean(name = "runner")
public QueryRunner createQueryRunner(DataSource dataSource){
return new QueryRunner(dataSource);
}

/**
* 用于创建数据源对象
* @return
*/
@Bean(name = "dataSource")
public DataSource createDataSource(){
ComboPooledDataSource ds = null;
try {
ds = new ComboPooledDataSource();
ds.setDriverClass("com.mysql.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://localhost:3306/test");
ds.setUser("root");
ds.setPassword("12345678");
return ds;
} catch (Exception e) {
e.printStackTrace();
}
return ds;
}
}

注意:当不加bean注解时,14行代码new出的对象不会加入到ioc容器当中,当加上bean注解后,spring会自动地将其加入到容器,效果同以下xml配置相同:

1
2
3
4
5
<!--配置QueryRunner对象-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!--注入数据源-->
<constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>

通过注解获取容器AnnotationConfigApplicationContext

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testFindAll() {
// 1.通过注解获取容器
// ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
// 2.得到业务层对象
IAccountService ac = (IAccountService) ac.getBean("accountService");
// 3.执行方法
List<Account> allAccount = as.findAllAccount();
for(Account account : allAccount){
System.out.println(account);
}
}

@Import

  • 作用:用于导入其他的配置类

@PropertySource

  • 作用:用于指定properties文件的位置

  • 属性:value:指定文件的名称和路径;关键字classpath表示类路径下。

代码示例:

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
@Configuration
@ComponentScan(basePackages = {"com.hongyi"})
@PropertySource("classpath:jdbcConfig.properties")
public class SpringConfiguration {

@Value("${jdbc.driver}")
private String driver;

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

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

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

/**
* 用于创建一个QueryRunner对象
* @param dataSource
* @return
*/
@Bean(name = "runner")
public QueryRunner createQueryRunner(DataSource dataSource){
return new QueryRunner(dataSource);
}

/**
* 用于创建数据源对象
* @return
*/
@Bean(name = "dataSource")
public DataSource createDataSource(){
ComboPooledDataSource ds = null;
try {
ds = new ComboPooledDataSource();
ds.setDriverClass(driver);
ds.setJdbcUrl(url);
ds.setUser(username);
ds.setPassword(password);
return ds;
} catch (Exception e) {
e.printStackTrace();
}
return ds;
}
}

3.3.8 Spring整合JUnit

1) 问题分析
  1. 应用程序的入口:main方法

  2. junit单元测试中,没有main方法也能执行:junit中集成了一个main方法,该方法就会判断当前测试类中哪些方法有@Test注解,junit就会让有Test注解的方法执行。

  3. junit不会管是否采用了spring框架。在执行测试方法时,junit不知道是否采用了spring框架。所以不会读取配置文件/配置类来创建ioc容器

  4. 由以上三点可知,当测试方法执行时,没有核心容器,即使写了autowired也无法实现注入。

2) 整合配置
  1. 导入spring整合junit的坐标
1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
  1. 使用junit提供的一个注解把原有的main方法替换,替换成spring提供的

@RunWith

1
2
3
4
@RunWith(SpringJUnit4ClassRunner.class)
public class AccountServiceTest {
// code...
}
  1. 告知spring的运行器,spring的ioc创建时基于xml还是注解的,并说明位置

@ContextConfiguration

locations:指定xml文件的位置,加上classpath关键字,表示在类路径下

classes:指定注解类所在的位置

1
2
3
4
5
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class AccountServiceTest {
// code...
}

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class AccountServiceTest {

@Autowired
private IAccountService as;

@Test
public void testFindAll() {
// 3.执行方法
List<Account> allAccount = as.findAllAccount();
for(Account account : allAccount){
System.out.println(account);
}
}

@Test
public void testFindOne() {
Account account = as.findAccountById(1);
System.out.println(account);
}
}

2 AOP面向切面

2.1 添加转账方法并演示事务问题

  1. 持久层IAccountDao.java新增findAccountByName()方法,并在实现类中实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Account findAccountByName(String accountName) {
try {
List<Account> accounts = runner.query("select * from account where name = ?", new BeanListHandler<Account>(Account.class), accountName);
if (accounts == null || accounts.size() == 0){
return null;
}
if (accounts.size() > 1){
throw new RuntimeException("结果集不唯一,数据异常");
}
return accounts.get(0);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
  1. 业务层IAccountService.java新增transfer()方法,并在实现类中实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void transfer(String sourceName, String targetName, Float money) {
// 1.根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
// 2.根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
// 3.转出账户减钱
source.setMoney(source.getMoney()-money);
// 4.转入账户加钱
target.setMoney(target.getMoney()+money);
// 5.更新转出账户
accountDao.updateAccount(source);
// 6.更新转入账户
accountDao.updateAccount(target);
}
  1. 测试代码:
1
2
3
4
5
6
7
8
@Test
public void testTransfer(){
// 1.获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 2.得到业务层对象
IAccountService as = (IAccountService) ac.getBean("accountService");
as.transfer("aaa","bbb",100f);
}

以上操作不满足事务的一致性。

2.1.1 问题分析

image-20211215195739458

事务的控制都应当在业务层中完成。

2.1.2 连接的工具类

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
/**
* 连接的工具类,用于从数据源中获取一个连接,并且实现和线程的绑定
*/
public class ConnectionUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

private DataSource dataSource;

/**
* 供spring注入
* @param dataSource
*/
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}

/**
* 获取当前线程上的sql连接
* @return
*/
public Connection getConnection(){
try{
// 1.先从ThreadLocal上获取
Connection conn = tl.get();
// 2.判断当前线程上是否有连接
if (conn == null){
// 3.从数据源中获取一个连接,并且存入ThreadLocal中
conn = dataSource.getConnection();
tl.set(conn);
}
// 4.返回当前线程上的连接
return conn;
}catch (Exception e){
throw new RuntimeException(e);
}
}

/**
* 把连接和线程解绑
*/
public void removeConnection(){
tl.remove();
}
}

2.1.3 管理事务的工具类

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
56
57
58
/**
* 和事务管理相关的工具类
* 包含开启事务,提交事务,回滚事务,释放连接
*/
public class TransactionManager {

private ConnectionUtils connectionUtils;

public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}

/**
* 开启事务
*/
public void beginTransaction(){
try {
connectionUtils.getConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}

/**
* 提交事务
*/
public void commit(){
try {
connectionUtils.getConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}

/**
* 回滚事务
*/
public void rollback(){
try {
connectionUtils.getConnection().rollback();
connectionUtils.removeConnection();
} catch (SQLException e) {
e.printStackTrace();
}
}

/**
* 释放连接
*/
public void release(){
try {
connectionUtils.getConnection().close();//还回连接池中
} catch (SQLException e) {
e.printStackTrace();
}
}

}

2.1.4 改造业务层和持久层实现类代码

业务层AccountServiceImpl部分代码:

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
private TransactionManager txManager;
// 在xml通过构造方法进行注入
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}

public List<Account> findAllAccount() {
try{
// 1.开启事务
txManager.beginTransaction();
// 2.执行操作
List<Account> accounts = accountDao.findAllAccount();
// 3.提交事务
txManager.commit();
// 4.返回结果
return accounts;
}catch (Exception e){
// 5.回滚操作
txManager.rollback();
throw new RuntimeException();
}finally {
// 6.释放连接
txManager.release();
}
}

public void transfer(String sourceName, String targetName, Float money) {
try{
// 1.开启事务
txManager.beginTransaction();
// 2.执行操作
// 2.1根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
// 2.2根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
// 2.3转出账户减钱
source.setMoney(source.getMoney()-money);
// 2.4转入账户加钱
target.setMoney(target.getMoney()+money);
// 2.5更新转出账户
accountDao.updateAccount(source);
// 2.6更新转入账户
accountDao.updateAccount(target);
// 3.提交事务
txManager.commit();
}catch (Exception e){
// 4.回滚操作
txManager.rollback();
}finally {
// 5.释放连接
txManager.release();
}
}

bean.xml代码

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
<?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">
<!--配置Service对象-->
<bean id="accountService" class="com.hongyi.service.impl.AccountServiceImpl">
<!--注入dao对象-->
<property name="accountDao" ref="accountDao"></property>
<!--注入事务管理器类对象-->
<property name="txManager" ref="txManger"></property>
</bean>

<!--配置Dao对象-->
<bean id="accountDao" class="com.hongyi.dao.impl.AccountDaoImpl">
<!--注入QueryRunner对象-->
<property name="runner" ref="runner"></property>
<!--注入ConnectionUtils-->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>

<!--配置QueryRunner对象-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>

<!--配置数据源-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--注入连接数据库的必备信息-->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property>
<property name="user" value="root"></property>
<property name="password" value="123456"></property>
</bean>

<!--配置connection的工具类 ConnectionUtils-->
<bean id="connectionUtils" class="com.hongyi.utils.ConnectionUtils">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>

<!--配置事务管理器-->
<bean id="txManger" class="com.hongyi.utils.TransactionManager">
<!--注入ConnectionUtils-->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>

</beans>

问题:耦合度太高

2.2 动态代理

2.2.1 特点

  1. 特点:字节码随用随创建,随用随加载。

  2. 作用:不修改源码的基础上,对方法增强

  3. 分类:

    1. 基于接口的动态代理,JDK代理
    2. 基于子类的动态代理,cglib代理

2.2.2 基于接口的动态代理

  • 涉及的类:Proxy

  • 提供者:JDK官方

  • 如何创建代理对象:使用Proxy类中的newProxyInstance()方法

  • 创建要求:被代理类至少实现一个接口,如果没有则不能使用

  • newProxyInstance()方法参数:

(1)ClassLoader:类加载器;用于加载代理对象的字节码,和被代理对象使用相同的类加载器。固定写法:代理谁,就写谁的类加载器,如:instance.getClass().getClassLoader()

(2)Class[]:字节码数组;它是用于让代理对象和被代理对象有相同的方法。固定写法:代理谁,就写谁的getClass().getInterfaces()

(3)InvocationHandler:用于提供增强的代码。它是让我们写如何代理。一般写该接口的实现类,通常情况下为匿名内部类,但不是必须的。此接口的实现类,都是谁用,谁写。

代码示例

IProducer.java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 对生产厂家要求的接口
*/
public interface IProducer {

/**
* 销售
* @param money
*/
public void saleProduct(float money);

/**
* 售后
* @param money
*/
public void afterService(float money);
}

Producer.java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 一个生产者
*/
public class Producer implements IProducer{
/**
* 销售
* @param money
*/
public void saleProduct(float money){
System.out.println("销售产品,并拿到钱"+ money);
}

/**
* 售后
* @param money
*/
public void afterService(float money){
System.out.println("提供售后服务,并拿到钱"+ money);
}
}

Client.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
/**
* 一个消费者
*/
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();

/**
* 基于接口的动态代理
*/
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() {
/**
* 作用:执行被代理对象(如这里的producer对象)的任何接口方法都会先经过该方法
* @param proxy 代理对象Proxy的引用 一般不用
* @param method 当前执行的方法 如saleProduct
* @param args 当前执行方法所需的参数
* @return 和被代理对象有相同的返回值
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 提供增强的代码
Object returnValue = null;
// 1.获取方法执行的参数
Float money = (Float)args[0];
// 2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())){
returnValue = method.invoke(producer,money*0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
}
}

2.2.3 基于子类的动态代理

导入坐标

1
2
3
4
5
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
  • 涉及的类:Enhancer

  • 提供者:第三方cglib库

  • 如何创建代理对象:使用Enhancer类中的create()方法

  • 创建要求:被代理类不能是最终类

  • create()方法参数:

(1)Class:字节码;用于指定被代理对象的字节码。固定写法。

(2)Callback:用于提供增强的代码。它是让我们写如何代理。一般写该接口的子接口的实现类:MethodInterceptor,通常情况下为匿名内部类,但不是必须的。此接口的实现类,都是谁用,谁写。

代码示例

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
/**
* 一个消费者
*/
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* 基于子类的动态代理
*/
Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 作用:执行被代理对象(如这里的producer对象)的任何接口方法都会先经过该方法
* @param o 同proxy
* @param method 同methods
* @param objects 同args
* 以上三个参数同invoke方法参数相同
* @param methodProxy 当前执行方法的代理对象 一般不用
* @return
* @throws Throwable
*/
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 提供增强的代码
Object returnValue = null;
// 1.获取方法执行的参数
Float money = (Float)objects[0];
// 2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())){
returnValue = method.invoke(producer,money*0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(10000);
}
}

2.2.4 利用动态代理实现事务控制

BeanFactory.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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 用于创建Service的代理对象的工厂
*/
public class BeanFactory {
private IAccountService accountService;

private TransactionManager txManager;

public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}

public final void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}

/**
* 获取Service的代理对象
*
* @return
*/
public IAccountService getAccountService() {
Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() {
/**
* 添加事务的支持
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtValue = null;
try {
// 1.开启事务
txManager.beginTransaction();
// 2.执行操作
rtValue = method.invoke(accountService, args);
// 3.提交事务
txManager.commit();
// 4.返回结果
return rtValue;
} catch (Exception e) {
// 5.回滚操作
txManager.rollback();
throw new RuntimeException();
} finally {
// 6.释放连接
txManager.release();
}
}
});
return null;
}
}

bean.xml代码

1
2
3
4
5
6
7
8
9
10
<!--配置代理的service对象-->
<bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>

<!--配置beanfactory-->
<bean id="beanFactory" class="com.hongyi.factory.BeanFactory">
<!--注入service-->
<property name="accountService" ref="accountService"></property>
<!--注入事务管理器-->
<property name="txManager" ref="txManger"></property>
</bean>

2.3 面向切面编程

2.3.1 AOP概念

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

简单来说,就是把程序中重复的代码抽取出来,在需要执行的时候,使用动态代理技术,在不修改源码的基础上,对已有的方法进行增强。目的:解耦。

示例

先来看一个例子, 如何给如下UserServiceImpl中所有方法添加进入方法的日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserServiceImpl implements IUserService {

@Override
public List<User> findUserList() {
System.out.println("execute method: findUserList");
return Collections.singletonList(new User("pdai", 18));
}

@Override
public void addUser() {
System.out.println("execute method: addUser");
// do something
}

}

我们将记录日志功能解耦为日志切面,它的目标是解耦。进而引出AOP的理念:就是将分散在各个业务逻辑代码中相同的代码通过横向切割的方式抽取到一个独立的模块中。

image-20231101152846940

OOP面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。

image-20231101152930561

2.3.2 AOP相关术语

  • 连接点Joinpoint:告诉AOP在哪里干,指的是那些被拦截的点,在spring中指的是方法,因为spring只支持方法类型的连接点。业务层接口中的方法都是连接点。
  • 切入点Pointcut:连接点的集合。
  • 通知Advice:告诉AOP干什么。指的是拦截到连接点之后要做的事情。通知类型有前置通知,后置通知,异常通知,最终通知,环绕通知。

image-20211215195752205

  • 引入Introduction:是一种特殊的通知,在不修改代码的前提下,引入可以在运行期间为类动态地添加一些方法或字段。
  • 目标对象Target:代理的目标对象,即被代理对象
  • 织入Weaving:告诉AOP如何实现。把增强应用到目标对象来创建新的代理对象的过程。
  • 代理Proxy:一个类被AOP织入增强后,就产生了一个结果代理类。即代理对象。
  • 切面Aspect:是切入点和通知(引介)的结合。

image-20231101154103759

2.3.3 基于XML配置的AOP

1) 必要的代码

业务层接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface IAccountService {
/**
* 模拟保存账户
*/
public void saveAccount();

/**
* 模拟更新账户
* @param i
*/
public void updateAccount(int i);

/**
* 删除账户
* @return
*/
int deleteAccount();
}

业务层实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AccountService implements IAccountService {
public void saveAccount() {
System.out.println("执行了保存账户");
}

public void updateAccount(int i) {
System.out.println("执行了更新账户"+i);
}

public int deleteAccount() {
System.out.println("执行了删除账户");
return 0;
}
}

记录日志的工具类

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 用于记录日志的工具类
* 提供了公共的代码
*/
public class Logger {
/**
* 用于打印日志:计划让其在切入点方法执行之前执行(切入点方法为业务层方法)
*/
public void beforePrintLog(){
System.out.println("Logger类中的printLog方法开始记录日志");
}
}
2) 配置步骤
  1. 把通知bean也交给spring来管理

  2. 使用aop:config标签表明开始aop配置

  3. 使用aop:aspect标签表明开始配置切面,属性有:

  • id:给切面配置一个唯一标识
  • ref:指定通知类bean的id
  1. aop:aspect标签的内部,使用对应的标签来配置通知的类型,示例中是让pringLog方法在切入点执行之前执行,所以是前置通知。属性有:
  • method:用于指定Logger类中哪个方法是前置的。
  • pointcut:用于指定切入点表达式,该表达式的含义是指对业务层中哪些方法增强。写法:

关键字:execution(表达式)

表达式:访问修饰符+返回值+包名.包名…..类名.方法名(参数)

标准的表达式写法:(只对某个方法增强)

1
public void com.hongyi.service.impl.AccountService.saveAccount()

其中访问修饰符可以省略;

返回值可以使用通配符,表示任意返回值;

包名可以使用通配符,表示任意包,但有几级包就要有几级通配符;也可以使用..代表当前包及其子包;

类名和方法名都可以使用*号进行通配;

参数列表用..表示任意参数和任意类型;

于是有了全通配写法:

全通配写法:(对所有方法增强)

1
* *..*.*(..)

实际开发中,切入点表达式的通常写法:切到业务层实现类下的所有方法,即:

1
* com.hongyi.service.impl.*.*(..)

bean.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">

<!--配置spring的ioc,把service对象配置进来-->
<bean id="accountService" class="com.hongyi.service.impl.AccountService"></bean>

<!--spring中基于xml的aop配置步骤-->
<!--配置Logger类-->
<bean id="logger" class="com.hongyi.utils.Logger"></bean>

<!--配置aop-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知类型,且建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution(public void com.hongyi.service.impl.AccountService.saveAccount())"></aop:before>
<aop:before method="printLog" pointcut="execution(* com.hongy i.service.impl.*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
</beans>

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 测试AOP的配置
*/
public class AOPTest {
public static void main(String[] args) {
// 1.获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 2.获取对象
IAccountService as = (IAccountService) ac.getBean("accountService");
// 3.执行方法
as.saveAccount();
}
}

执行结果:

image-20211215195801894

3) 四种常用的通知类型

bean.xml代码

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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">

<!--配置spring的ioc,把service对象配置进来-->
<bean id="accountService" class="com.hongyi.service.impl.AccountService"></bean>

<!--spring中基于xml的aop配置步骤-->
<!--配置Logger类-->
<bean id="logger" class="com.hongyi.utils.Logger"></bean>

<!--配置aop-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置前置通知-->
<aop:before method="beforePrintLog" pointcut="execution(* com.hongyi.service.impl.*.*(..))"></aop:before>

<!--配置后置通知,它和异常通知永远只能二者中执行一个-->
<aop:after-returning method="afterReturningPrintLog" pointcut="execution(* com.hongyi.service.impl.*.*(..))"></aop:after-returning>

<!--配置异常通知,切入点方法执行产生异常之后才执行-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut="execution(* com.hongyi.service.impl.*.*(..))"></aop:after-throwing>

<!--配置最终通知-->
<aop:after method="afterPrintLog" pointcut="execution(* com.hongyi.service.impl.*.*(..))"></aop:after>
</aop:aspect>
</aop:config>
</beans>

执行结果:

image-20211215195809057

4) 通用化切入点表达式

采用标签<aop:pointcut>,此标签写在aop:aspect内部,只能在当前切面中使用。它还可以写在aop:aspect的外面,此时就变成了所有切面可用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--配置aop-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置前置通知-->
<aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>

<!--配置后置通知-->
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>

<!--配置异常通知-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>

<!--配置最终通知-->
<aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>

<!--配置切入点表达式,id指定表达式的唯一标识,expression指定表达式内容-->
<aop:pointcut id="pt1" expression="execution(* com.hongyi.service.impl.*.*(..))"/>
</aop:aspect>
</aop:config>
5) 环绕通知

测试代码

1
2
3
4
5
6
/**
* 环绕通知
*/
public void aroundPrintLog(){
System.out.println("Logger类中的aroundPrintLog方法开始记录日志");
}
1
2
3
4
<!--配置切入点表达式,id指定表达式的唯一标识,expression指定表达式内容-->
<aop:pointcut id="pt1" expression="execution(* com.hongyi.service.impl.*.*(..))"/>
<!--配置环绕通知-->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
1
2
3
4
5
6
7
8
9
10
public class AOPTest {
public static void main(String[] args) {
// 1.获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 2.获取对象
IAccountService as = (IAccountService) ac.getBean("accountService");
// 3.执行方法
as.saveAccount();
}
}

测试结果

image-20211215195816705

  • 问题:当配置了环绕通知后,切入点方法没有执行,而通知方法执行了。

  • 分析:通过对比动态代理中的环绕通知代码,发现动态代理中的环绕通知中有明确的切入点方法调用,而上面的代码中没有。

  • 解决:Spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法,该接口可以作为环绕通知的方法参数,在程序执行时,Spring框架会为我们提供该接口的实现类供我们使用。

spring中的环绕通知:一种可以在代码中手动控制增强方法何时执行的方式。相当于自定义何时来执行通知方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 环绕通知
*/
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("Logger类中的aroundPrintLog方法开始记录日志,前置");
rtValue = pjp.proceed(args);//明确调用业务层方法,也就是切入点方法
System.out.println("Logger类中的aroundPrintLog方法开始记录日志,后置");
return rtValue;
} catch (Throwable throwable) {
System.out.println("Logger类中的aroundPrintLog方法开始记录日志,异常");
throw new RuntimeException(throwable);
} finally {
System.out.println("Logger类中的aroundPrintLog方法开始记录日志,最终");
}
}

执行结果

image-20211215195824963

2.3.4 基于注解配置的AOP

代码演示

bean.xml代码

1
2
3
4
5
<!--配置Spring创建容器时要扫描的包-->
<context:component-scan base-package="com.hongyi"></context:component-scan>

<!--配置Spring开启注解aop的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

Logger.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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {

//切入点
@Pointcut("execution(* com.hongyi.service.impl.*.*(..))")
private void pt1(){}

/**
* 前置通知
*/
@Before("pt1()")
public void beforePrintLog(){
System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志");
}

/**
* 后置通知
*/
@AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志");
}

/**
* 异常通知
*/
@AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志");
}

/**
* 最终通知
*/
@After("pt1()")
public void afterPrintLog(){
System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志");
}

/**
* 环绕通知
*/
@Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("Logger类中的aroundPrintLog方法开始记录日志,前置");
rtValue = pjp.proceed(args);//明确调用业务层方法,也就是切入点方法
System.out.println("Logger类中的aroundPrintLog方法开始记录日志,后置");
return rtValue;
} catch (Throwable throwable) {
System.out.println("Logger类中的aroundPrintLog方法开始记录日志,异常");
throw new RuntimeException(throwable);
} finally {
System.out.println("Logger类中的aroundPrintLog方法开始记录日志,最终");
}
}
}

执行结果

image-20211215195832454

注意最终通知和后置通知的顺序有变化

2.4 AOP日志

3 JdbcTemplate的使用

3.1 基本使用

依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* JdbcTemplate的基本用法
*/
public class JdbcTemplateDemo1 {
public static void main(String[] args) {
// 准备数据源:spring的内置数据源 DriverManagerDataSource
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/test");
ds.setUsername("root");
ds.setPassword("123456");
// 1.创建JdbcTemplate对象
JdbcTemplate jt = new JdbcTemplate();
// 设置数据源
jt.setDataSource(ds);
// 2.执行操作
jt.execute("insert into account(id,name,money) values (4,'ddd',1000)");
}
}

3.2 JdbcTemplate在Spring的ioc容器中的使用

代码示例

bean.xml代码

1
2
3
4
5
6
7
8
9
10
11
12
<!--配置JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>

<!--配置数据源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/test?useSSL=false"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</bean>

JdbcTemplateDemo2.java代码

1
2
3
4
5
6
7
8
9
10
public class JdbcTemplateDemo2 {
public static void main(String[] args) {
// 1.获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 2.获取对象
JdbcTemplate jt = (JdbcTemplate) ac.getBean("jdbcTemplate",JdbcTemplate.class);
// 3.执行操作
jt.execute("insert into account(id,name,money) values (10,'ddd',1000)");
}
}

3.3 JdbcTemplate的CRUD操作

代码示例

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
/**
* JdbcTemplate的基本用法
*/
public class JdbcTemplateDemo2 {
public static void main(String[] args) {
// 1.获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 2.获取对象
JdbcTemplate jt = (JdbcTemplate) ac.getBean("jdbcTemplate",JdbcTemplate.class);
// 3.执行操作
// 保存
jt.update("insert into account (id,name,money) values (8,'hongyi',999)");
// 更新
jt.update("update account set name=?,money=? where id=?","test",100,1);
// 删除
jt.update("delete from account where id=?",1);
// 查询所有
// 法一:自己提供封装类AccountRowMapper
List<Account> accounts = jt.query("select * from account where money > ?",new AccountRowMapper(),999f);
// 法二:利用Spring提供的自动封装BeanPropertyRowMapper,就不用写AccountRowMapper了
List<Account> accounts = jt.query("select * from account where money > ?",new BeanPropertyRowMapper<Account>(Account.class),999f);
for(Account account : accounts){
System.out.println(account.getId()+" "+account.getName()+" "+account.getMoney());
}
// 查询一个
List<Account> accounts = jt.query("select * from account where id = ?",new BeanPropertyRowMapper<Account>(Account.class),2);
System.out.println(accounts.isEmpty()?"没有该用户":accounts.get(0));
// 查询返回一行一列(使用聚合函数,但不加group by子句)
Long count = jt.queryForObject("select count(*) from account where money > ?",Long.class,100);
System.out.println(count);
}
}

/**
* 定义Account的封装策略
*/
class AccountRowMapper implements RowMapper<Account>{
/**
* 把结果集中的数据封装到Account中,由Spring把每个Account加到集合中
* @param resultSet sql查询返回的结果集
* @param i
* @return
* @throws SQLException
*/
public Account mapRow(ResultSet resultSet, int i) throws SQLException {
Account account = new Account();;
account.setId(resultSet.getInt("id"));
account.setName(resultSet.getString("name"));
account.setMoney(resultSet.getFloat("money"));
return account;
}
}

3.4 JdbcTemplate的在DAO中的使用

代码示例

AccountDaoImpl.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
/**
* 持久层接口实现类
*/
public class AccountDaoImpl implements IAccountDao {

private JdbcTemplate jdbcTemplate;

public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public Account findAccountById(Integer id) {
List<Account> accountList = jdbcTemplate.query("select * from account where id = ?",new BeanPropertyRowMapper<Account>(Account.class),id);
return accountList.isEmpty()?null:accountList.get(0);
}

public Account findAccountByName(String name) {
List<Account> accountList = jdbcTemplate.query("select * from account where name = ?",new BeanPropertyRowMapper<Account>(Account.class),name);
if(accountList.isEmpty()){
return null;
}else if(accountList.size()>1){
throw new RuntimeException("结果集不唯一");
}
return accountList.get(0);
}

public void updateAccount(Account account) {
jdbcTemplate.update("update account set name = ?,money = ? where id = ?",account.getName(),account.getMoney(),account.getId());
}
}

bean.xml

1
2
3
4
<!--配置账户的持久层-->
<bean id="accountDao" class="com.hongyi.dao.impl.AccountDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>

测试代码

1
2
3
4
5
6
7
8
public class JdbcTemplateDemo3 {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountDao accountDao = (IAccountDao) ac.getBean("accountDao");
System.out.println(accountDao.findAccountById(2));
System.out.println(accountDao.findAccountByName("bbb"));
}
}

注意当有多个Dao实现类时,可继承JdbcDaoSupport类,去除重复代码。

代码示例

1
2
3
public class AccountDaoImpl2 extends JdbcDaoSupport implements IAccountDao{
// code...
}

4 Spring事务

4.1 要点

  • JavaEE体系进行分层开发,事务处理位于业务层,Spring为业务层的事务处理解决方案。
  • Spring的事务控制都是基于AOP的,它既可以通过编程的方式实现(编程式),也可以使用配置的方式实现(声明式)。重点是采用配置的方式实现AOP

4.2 事务的四个特性 ACID

  • 原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
  • 一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。
  • 隔离性(Isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
  • 持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。

Spring事务管理涉及的接口的联系如下:

image-20211215195859659

4.3 基于XML的声明式事务控制

4.3.1 项目搭建

需求

用户aaa向用户bbb转账100元,事务开始前,两者都有初始资金1000:

image-20211225111422140

项目结构

image-20211225111544247

导入相关依赖

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
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.9</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.13.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.13.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.9</version>
<scope>test</scope>
</dependency>
</dependencies>

重要类代码

AccountServiceImpl.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
39
/**
* 账户的业务层实现类
* 事务控制应该都是在业务层
*/
public class AccountServiceImpl implements IAccountService{

private IAccountDao accountDao;

public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}

@Override
public Account findAccountById(Integer accountId) {
return accountDao.findAccountById(accountId);

}

@Override
public void transfer(String sourceName, String targetName, Float money) {
System.out.println("transfer....");
//2.1根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.2根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//2.3转出账户减钱
source.setMoney(source.getMoney()-money);
//2.4转入账户加钱
target.setMoney(target.getMoney()+money);
//2.5更新转出账户
accountDao.updateAccount(source);

// 模拟事务异常
int i = 1 / 0;

//2.6更新转入账户
accountDao.updateAccount(target);
}
}

AccountServiceTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 使用Junit单元测试:测试我们的配置
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {

@Autowired
private IAccountService as;

@Test
public void testTransfer(){
// aaa向bbb转账100元
as.transfer("aaa","bbb",100f);
}
}

4.3.2 XML配置的声明式事务

1) 配置步骤
  1. 配置事务管理器

  2. 配置事务通知

    • 此时需要导入事务的约束,tx名称空间和约束,同时也需要aop的

    • 使用tx:advice标签配置事务通知

    • 属性:

      • id:给事务通知起一个唯一标识
      • transaction-manager:给事务通知提供一个事务管理器的引用
  3. 配置AOP中的通用切入点表达式

    • 建立事务通知和切入点表达式的对应关系
  4. 配置事务的属性:是在事务的通知tx:advice标签的内部

2) 事务的属性
  • isolation:用于指定事务的隔离级别。默认是DEFAULT,表示使用数据库的默认隔离级别
  • propagation:用于指定事务的传播行为,默认为REQUIRED,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS
  • read-only:用于指定事务是否只读。只有查询方法才能设置为true,默认为false,表示读写
  • timeout:用于指定事务的超时时间,默认为-1,表示永不超时。如果指定了数值,则以秒为单位
  • rollback-for:用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,不回滚。没有默认值,表示任何异常都回滚
  • no-rollback-for:与上述相反
3) bean.xml整体配置代码
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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- 配置业务层-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>

<!-- 配置账户的持久层-->
<bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>

<!-- 配置数据源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/eesy"></property>
<property name="username" value="root"></property>
<property name="password" value="12345678"></property>
</bean>

<!--spring中基于xml的声明式事务控制配置步骤-->
<!--1.配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>

<!--2.配置事务的通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!--5.配置事务的属性-->
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" read-only="false"/>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>

<!--配置AOP-->
<aop:config>
<!--3.配置切入点表达式-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"/>
<!--4.建立事务通知和切入点表达式的对应关系-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
</aop:config>

</beans>

4.4 基于注解的声明式事务控制

项目搭建同4.3节

4.4.1 xml引入注解依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

4.4.2 配置步骤

  1. 配置事务管理器
  2. 开启spring对注解事务的支持
  3. 在需要事务支持的地方使用@Transactional注解

bean.xml代码

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
<!--配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.itheima"></context:component-scan>


<!--配置JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>

<!-- 配置业务层-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>

<!-- 配置账户的持久层-->
<bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>

<!-- 配置数据源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/eesy"></property>
<property name="username" value="root"></property>
<property name="password" value="12345678"></property>
</bean>

<!--spring中基于注解的声明式事务控制配置步骤
1.配置事务管理器
2.开启spring对注解事务的支持
3.在需要事务支持的地方使用@Transactional注解
-->
<!--1.配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>

<!--2.开启spring对注解事务的支持-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>

AccountDaoImpl.java

1
2
3
4
5
6
7
8
9
10
/**
* 账户的持久层实现类
*/
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
@Autowired
private JdbcTemplate jdbcTemplate;

//code...
}

AccountServiceImpl.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
/**
* 账户的业务层实现类
* 事务控制应该都是在业务层
*/
@Service("accountService")
// 只读性事务配置
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public class AccountServiceImpl implements IAccountService{

@Autowired
private IAccountDao accountDao;

public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}

@Override
public Account findAccountById(Integer accountId) {
return accountDao.findAccountById(accountId);

}

// 读写性事务配置,可能需要单独配置
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
@Override
public void transfer(String sourceName, String targetName, Float money) {
// code...
}
}

4.4.3 基于纯注解的声明式事务控制

项目架构

image-20211225152033506

SpringConfiguration.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Spring的配置类,相当于bean.xml
*/
// 表明该类是一个配置类
@Configuration
// 要扫描的包
@ComponentScan("com.itheima")
// 导入其他配置类
@Import({JdbcConfig.class, TransactionConfig.class})
// 数据源参数
@PropertySource("jdbcConfig.properties")
// 开启事务控制管理
@EnableTransactionManagement
public class SpringConfiguration {

}

TransactionConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 和事务相关的配置类
*/
public class TransactionConfig {
/**
* 用于创建事务管理器对象
* @param dataSource
* @return
*/
@Bean("transactionManager")
public PlatformTransactionManager createTransactionManager(DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}
}

JdbcConfig.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
39
40
41
42
/**
* 和连接数据库相关的配置类
*/
public class JdbcConfig {

@Value("${jdbc.driver}")
private String driver;

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

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

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


/**
* 创建JdbcTemplate对象
* @param dataSource
* @return
*/
@Bean(name = "jdbcTemplate")
public JdbcTemplate createJdbcTemplate(DataSource dataSource){
return new JdbcTemplate(dataSource);
}

/**
* 创建数据源对象
* @return
*/
@Bean(name = "dataSource")
public DataSource createDataSource(){
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
return ds;
}
}

jdbcConfig.properties

1
2
3
4
jdbc.driver = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3306/eesy
jdbc.username = root
jdbc.password = 12345678

其余dao层和service层的代码不变,这样就删除了bean.xml配置,采用了纯注解的形式来控制事务。

4.5 @Transactional注解

4.5.1 作用域

  • 当标注在类上的时候:表示给该类所有的 public 方法添加上 @Transactional注解
  • 当标注在方法上的时候:事务的作用域就只在该方法上生效,并且如果类及方法上都配置 @Transactional注解时,方法的注解会覆盖类上的注解

4.5.2 属性

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
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
// 事务管理器
@AliasFor("transactionManager")
String value() default "";

@AliasFor("value")
String transactionManager() default "";

// 事务的传播行为
Propagation propagation() default Propagation.REQUIRED;

// 事务的隔离级别
Isolation isolation() default Isolation.DEFAULT;

int timeout() default -1;

// 读写或只读事务,默认读写
boolean readOnly() default false;

// 通过类指定回滚的异常类型
Class<? extends Throwable>[] rollbackFor() default {};

// 通过类名指定回滚的异常类型
String[] rollbackForClassName() default {};

// 通过类指定不回滚的异常类型
Class<? extends Throwable>[] noRollbackFor() default {};

// 通过类名指定不回滚的异常类型
String[] noRollbackForClassName() default {};
}
① 传播行为

事务传播:当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。

例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。

1
2
3
4
5
6
7
8
9
10
@Transactional(Propagation=XXX)
public void methodA(){
methodB();
//doSomething
}

@Transactional(Propagation=XXX)
public void methodB(){
//doSomething
}

事务传播行为有7种,默认是 REQUIRED 传播机制。

如果A方法调用B方法:

含义
REQUIRED 如果A存在事务,则B加入该事务;如果A不存在事务,则B创建一个新的事务;
SUPPORTS 如果A存在事务,则加入该事务;如果A不存在事务,则B以非事务的方式继续运行;
MANDATORY 如果A存在事务,则加入该事务;如果A不存在事务,则B抛出异常;
REQUIRES_NEW 如果A不存在事务,重新创建一个新的事务;如果A存在事务,则B暂停当前的事务;
NOT_SUPPORTED 以非事务的方式运行;如果A存在事务,则B暂停当前的事务
NEVER 以非事务的方式运行;如果A存在事务,则B抛出异常
NESTED 如果A存在事务,则B在该事务内嵌套事务运行;如果A不存在事务,则B创建一个新的事务;

REQUIRED和NESTED区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class ServiceA{
@Autowired
private ServiceB serviceB;

@Transactional
public void A(){
try{
serviceB.B();
}catch(Exception e){
e.printStackTrace();
}
// 伪代码,执行数据库修改操作
}
}
1
2
3
4
5
6
7
@Service
public class ServiceB{
@Transactional(propagation = Propagation.REQUIRED)
public void B(){
//伪代码,执行数据库修改操作
}
}
  • 情况一:A方法中出现了异常,结果A、B方法修改操作都会被回滚
  • 情况二:B方法中出现了异常,结果A、B方法修改操作都会被回滚(一荣俱荣,一损俱损)

如果将B方法修改为NESTED

1
2
3
4
5
6
7
@Service
public class ServiceB{
@Transaction(propagation = Propagation.NESTED)
public void B(){
//伪代码,执行数据库修改操作
}
}
  • 情况一:A方法中出现了异常,结果A、B方法修改操作都会被回滚
  • 情况二:B方法中出现了异常,结果B方法修改操作被回滚,A方法修改操作提交
② 隔离级别

Spring事务隔离级别对应的事务隔离级别与 MySQL 一致。

③ 读写事务
  • 设置为true:表示只读,如果该方法内存在增、删、改操作则会抛出异常;
  • 设置为false(默认):表示读写,增、删、改、查操作都允许;
④ 回滚异常类型
  • rollbackFor:通过类进行指定,如@Transactional(rollbackFor = {Exception.class})
  • rollbackForClassName:通过类名进行指定,如@Transactional(rollbackForClassName = {"java.lang.Exception"})

Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor 或者 rollbackForClassName 属性。

4.6 事务失效的场景

在某些业务场景下,如果一个请求中,需要同时写入多张表的数据。为了保证操作的原子性(要么同时成功,要么同时失败),避免数据不一致的情况,我们一般都会用到spring事务。

4.6.1 没有被 Spring 管理

1
2
3
4
5
6
7
@Service
public class OrderServiceImpl implements OrderService{
@Transactional
public void updateOrder(Order order){
//update order
}
}

如果此时把@Service注解注释掉,那么这个类就不会被加载成一个Bean,这个类就不会Spring管理了,事务自然就失效了。

4.6.2 没有在Spring配置文件中启用事务管理器

如果是Spring Boot项目,它默认会自动配置事务管理器并开启事务支持。

4.6.3 数据库引擎不支持事务

这里以 MySQL为例,MyISAM引擎是不支持事务操作的,一般要支持事务都会使用InnoDB引擎,根据MySQL 的官方文档说明,从MySQL 5.5.5 开始的默认存储引擎是 InnoDB,之前默认的都是 MyISAM,所以这一点要值得注意,如果底层引擎不支持事务,那么再怎么设置也没有用。

4.6.4 方法不是 public

@Transactional注解只能用于public 的方法上,否则事务不会生效,如果要用在非public的方法上,则可以开启基于 AspectJ 框架的静态代理模式。

4.6.5 在类中方法内部调用(类方法自调用)*

1
2
3
4
5
6
7
8
9
10
11
@Service
public class OrderServiceImpl implements OrderService {
public void update(Order order) {
updateOrder(order); // updateOrder支持事务,但update不支持
}

@Transactional
public void updateOrder(Order order) {
// update order
}
}

事务不生效的原因: 事务是通过Spring AOP代理来实现的,而在同一个类中,一个方法调用另一个方法时,调用方法直接调用目标方法的代码,而不是通过代理类进行调用。即以上代码,调用目标updateOrder方法不是通过代理类进行的,因此事务不生效。

此外:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void update(Order order) {
updateOrder(order);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateOrder(Order order) {
//...
}
}

这次在 update 方法上加了 @Transactional,如果在 updateOrder 上加了 REOUIRES_NEW新开启一个事务,也不会生效。因为它们发生了自身调用,调用了该类自己的方法,而没有经过Spring的代理类,默认只有调用外部代理类的方法,事务才会生效。


Spring之所以可以对开启@Transactional的方法进行事务管理,是因为Spring为当前类生成了一个代理类,然后在执行相关方法时,会判断这个方法有没有@Transactional注解,如果有的话,则会开启一个事务。

image-20231101180744661

代码模拟——类方法自调用

  • OrderService
1
2
3
4
5
6
7
8
public interface OrderService {

// 校验订单参数
void verifyOrderParameters();

// 保存订单
void saveOrder();
}
  • OrderServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OrderServiceImpl implements OrderService{

@Override
public void verifyOrderParameters() {
System.out.println("校验订单参数");
// 调用保存订单方法
saveOrder();
}

@Override
@Transactional
public void saveOrder() {
System.out.println("保存订单信息到DB");
}
}
  • 一个静态代理类,模拟Spring在事务中动态生成的代理类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class OrderServiceImplProxy implements OrderService{

// 目标对象的引用
private OrderServiceImpl orderServiceImpl;

public OrderServiceImplProxy(OrderServiceImpl orderServiceImpl) {
this.orderServiceImpl = orderServiceImpl;
}

@Override
public void verifyOrderParameters() {
orderServiceImpl.verifyOrderParameters();
}

@Override
public void saveOrder() {
// 方法增强
System.out.println("开启事务...");
orderServiceImpl.saveOrder();
System.out.println("提交事务...");
}
}
  • 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
// 创建一个订单业务的真实处理对象
OrderServiceImpl orderServiceImpl = new OrderServiceImpl();
// 创建一个代理对象
OrderServiceImplProxy orderServiceImplProxy = new OrderServiceImplProxy(orderServiceImpl);
// 执行代理对象的校验订单方法
orderServiceImplProxy.verifyOrderParameters();
System.out.println("--------------------------------------------");
// 执行代理对象的保存订单方法
orderServiceImplProxy.saveOrder();
}
}
1
2
3
4
5
6
校验订单参数
保存订单信息到DB
--------------------------------------------
开启事务...
保存订单信息到DB
提交事务...

可以看到,类方法自调用并没有走代理对象(第1,2行输出);

解决方案:获取本对象的代理对象,再进行调用。

在Spring 中可以在当前线程中暴露并获取当前代理类,通过在启动类上添加以下注解来启用暴露代理类,如下面的示例所示。

1
2
// 设置暴露代理类
@EnableAspectJAutoProxy(exposeProxy = true)

然后通过以下代码获取当前代理类,并调用代理类的事务方法:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class OrderServiceImpl implements OrderService {
public void update(Order order) {
// 通过代理类调用
((OrderService) AopContext.currentProxy()).updateOrder();
}

@Transactional
public void updateOrder(Order order) {
// ...
}
}

4.6.6 异常类型不匹配

@Transactional注解默认的回滚异常类型是运行时异常(RuntimeException)。

1
2
3
4
5
6
7
8
9
10
11
@Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void update(Order order) {
try{
// update order
}catch{
throw new Exception("更新失败");
}
}
}

因为 Spring 默认回滚的是 RuntimeException 异常,和程序抛出的 Exception 异常不匹配,所以事务也是不生效的。如果要触发默认RuntimeException之外异常的回滚,则需要在 @Transactional 事务注解上指定异常类,示例如下:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class OrderServiceImpl implements OrderService {
@Transactional(rollbackFor = Exception.class)
public void update(Order order) {
try{
// update order
}catch{
throw new Exception("更新失败");
}
}
}

4.6.7 异常被捕获但未抛出

当抛出的异常被try-catch捕获时,事务也会失效,具体看代码:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class OrderServiceImpl implements OrderService {
@Transactional(rollbackFor = Exception.class)
public void update(Order order) {
try{
// update order
}catch (Exception e){
e.printStackTrace();
// 没有抛出异常
}
}
}

这个方法把异常给捕获了,但没有抛出来,所以事务不会回滚。

需要修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class OrderServiceImpl implements OrderService {
@Transactional(rollbackFor = Exception.class)
public void update(Order order) {
try{
// update order
}catch (Exception e){
e.printStackTrace();
throw new Exception("更新失败");
}
}
}