9 Spring的数据库编程
约 11362 字大约 38 分钟
2025-12-24
学习目标
- 了解 JdbcTemplate 类的作用
- 熟悉 Spring JDBC 的配置
- 熟悉 JdbcTemplate 的增删改查操作
- 熟悉 Spring 事务管理
- 掌握基于 XML 方式的声明式事务
- 熟悉基于注解方式的声明式事务
数据库用于处理持久化业务产生的数据,应用程序在运行过程中经常要操作数据库。一般情况下,数据库的操作由持久层来实现。Spring 作为扩展性较强的一站式开发框架,它提供了 JDBC 模块,Spring JDBC 可以管理数据库连接资源,简化传统 JDBC 的操作,进而提升程序数据库操作的效率。
9.1 Spring JDBC
传统的 JDBC 在操作数据库时,需要先打开数据库连接,执行 SQL 语句,然后封装结果,最后关闭数据库连接等资源。频繁的数据库操作会产生大量重复代码,造成代码冗余。Spring 的 JDBC 模块负责数据库资源管理和错误处理,大大简化了开发人员对数据库的操作,使开发人员可以从繁琐的数据库操作中解脱出来,从而将更多的精力投入到编写业务逻辑中。
9.1.1 JdbcTemplate 概述
针对数据库操作,Spring 框架提供了 JdbcTemplate 类。JdbcTemplate 是一个模板类,Spring JDBC 中的更高层次的抽象类均在此模板类基础上创建。
JdbcTemplate 类的继承关系十分简单,它继承自抽象类 JdbcAccessor,同时实现了 JdbcOperations 接口。
抽象类 JdbcAccessor 为其子类提供了一些访问数据库时使用的公共属性,具体如下:
DataSource:DataSource主要功能是获取数据库连接。在具体的数据操作中,DataSource还可以提供对数据库连接的缓冲池和分布式事务的支持。SQLExceptionTranslator:SQLExceptionTranslator是一个接口,全称为org.springframework.jdbc.support.SQLExceptionTranslator。该接口负责对SQLException异常进行转译工作。通过必要的设置或者调用其方法,JdbcTemplate可以将SQLException的转译工作委托给其实现类完成。
9.1.2 Spring JDBC 的配置
Spring JDBC 模块主要由四个包组成,分别是 core(核心包)、dataSource(数据源包)、object(对象包)和 support(支持包),这四个包的具体说明如表 9-1 所示。
表9-1 SpringJDBC中的主要包及其说明
| 包名 | 说明 |
|---|---|
core | 核心包,包含了JDBC的核心功能,包括JdbcTemplate类、SimpleJdbcInsert类、SimpleJdbcCall类和NamedParameterJdbcTemplate类 |
dataSource | 数据源包,包含访问数据源的实用工具类,它有多种数据源的实现,可以在JavaEE容器外部测试JDBC代码 |
object | 对象包,以面向对象的方式访问数据库,它可以执行查询、修改和更新操作并将返回结果作为业务对象,并且可在数据表的列和业务对象的属性之间映射查询结果 |
support | 支持包,包含了core和object包的支持类,如提供异常转换功能的SQLException类 |
从表 9-1 可知,Spring 对数据库的操作都封装在了 core、dataSource、object 和 support 这四个包中。想要使用 Spring JDBC,就需要对这些包进行配置。在 Spring 中,JDBC 的配置是在配置文件 applicationContext.xml 中完成的,其具体配置如下:
<?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">
<!-- 1. 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!-- 数据库驱动 -->
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<!-- 连接数据库的 url -->
<property name="url" value="jdbc:mysql://localhost/spring?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai"/>
<!-- 连接数据库的用户名 -->
<property name="username" value="root"/>
<!-- 连接数据库的密码 -->
<property name="password" value="root"/>
</bean>
<!-- 2. 配置 JDBC 模板 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 默认必须使用数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 3. 配置注入类 -->
<bean id="xxx" class="Xxx">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
</beans>在上述代码中,定义了三个 Bean,分别是 dataSource、jdbcTemplate 和注入类的 Bean。其中,dataSource 对应的 org.springframework.jdbc.datasource.DriverManagerDataSource 类用于配置数据源;jdbcTemplate 对应的 org.springframework.jdbc.core.JdbcTemplate 类中定义了 JdbcTemplate 的相关配置。
上述代码中 dataSource 配置中有四个属性,这四个属性是 JDBC 连接数据库所必需的,它们的含义如表 9-2 所示。
表9-2 dataSource中4个属性的含义
| 属性名 | 含义 |
|---|---|
driverClassName | 所使用的驱动名称,对应驱动JAR包中的Driver类 |
url | 连接数据库的URL |
username | 访问数据库的用户名 |
password | 访问数据库的密码 |
表 9-2 中的四个属性需要根据数据库类型或系统配置设置相应的属性值。例如,如果数据库类型不同,需要更改驱动名称;如果数据库不在本地,则需将地址中的 localhost 替换为主机 IP;默认情况下,数据库端口号可省略(MySQL 默认端口号为 3306),但如果修改过,则需加上修改后的端口号。此外,连接数据库的用户名和密码需与数据库创建时一致。
配置 JdbcTemplate 时,需要将 dataSource 注入到 JdbcTemplate 中,而其他需要使用 JdbcTemplate 的 Bean 也需要将其注入(通常注入 Dao 类中,在 Dao 类中进行与数据库相关的操作)。
9.2 JdbcTemplate 的常用方法
JdbcTemplate 类提供了大量的更新和查询数据库的方法,可用于操作数据库。
9.2.1 execute() 方法
execute() 方法用于执行 SQL 语句,其语法格式如下:
jdbcTemplate.execute("SQL语句");下面以创建数据表的 SQL 语句为例演示该方法的使用,具体步骤如下。
(1)数据库
数据库使用前文一直使用的:use heima_ssm_book;
(2)创建项目并引入依赖
创建一个名为 ch9 的 Maven 项目,并在 pom.xml 文件中加载所需依赖项,包括 Spring 基础包、Spring JDBC 的包、MySQL 数据库驱动包和 Spring 事务处理的 spring-tx 包:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
......
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>5.3.31</spring.version>
<common.logging.version>1.2</common.logging.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>${common.logging.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
</project>(3)编写配置文件
在项目的 src/main/resources 目录下创建配置文件 applicationContext.xml,配置 id 为 dataSource 的数据源 Bean 和 id 为 jdbcTemplate 的 JDBC 模板 Bean,并将数据源注入 JDBC 模板中:
<?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">
<!-- 1. 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!-- 数据库驱动-->
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<!-- 连接数据库的url-->
<property name="url"
value="jdbc:mysql://localhost/heima_ssm_book?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai"/>
<!-- 数据库用户名-->
<property name="username" value="root"/>
<!-- 数据库密码-->
<property name="password" value="123456"/>
</bean>
<!-- 2. 配置 JDBC 模板 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 默认使用的数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>(4)编写测试类
创建内创建测试类 TestJdbcTemplate。在该类的 main() 方法中通过 Spring 容器获取 JdbcTemplate 实例,然后调用 execute() 方法执行创建数据表的 SQL 语句:
package io.weew12.github;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
public class TestJdbcTemplate {
/**
* 调用 execute() 方法建表
*/
public static void main(String[] args) {
// 初始化 Spring 容器,加载 applicationContext.xml 配置
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
// 通过容器获取 JdbcTemplate 的实例
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
// 使用 execute() 方法执行 SQL 语句,创建用户账户管理表 account
jdbcTemplate.execute("create table ch9_account(" +
"id int primary key auto_increment," +
" username varchar(50)," +
" balance double)");
}
}在 IDEA 中启动 TestJdbcTemplate 类结果如图所示。

从图可看出,数据库中新增了数据表 ch9_account。
9.2.2 update() 方法
update() 方法可用于插入、更新和删除数据的操作。JdbcTemplate 提供了一系列 update() 方法的重载,常用的如表 9-3 所示。
表9-3 JdbcTemplate类中常用的update()方法
| 方法 | 说明 |
|---|---|
int update(String sql) | 该方法是最简单的update()方法重载形式,它直接执行传入的SQL语句,并返回受影响的行数 |
int update(PreparedStatementCreator psc) | 该方法执行参数psc返回的语句,然后返回受影响的行数 |
int update(String sql, PreparedStatementSetter pss) | 该方法通过参数pss设置SQL语句中的参数,并返回受影响的行数 |
int update(String sql, Object... args) | 该方法可以为SQL语句设置多个参数,这些参数保存在参数args中,使用Object...设置SQL语句中的参数,要求参数不能为 null,并返回受影响的行数 |
下面通过一个案例演示如何使用 update() 方法对数据表 ch9_account 进行添加、更新、删除操作:
(1)编写实体类
在项目包中创建 Account 类,在该类中定义 id、username 和 balance 属性,分别表示账户 ID、用户名和账户余额,并提供 getter/setter 方法:
package io.weew12.github;
public class Account {
/**
* 账户id
*/
private Integer id;
/**
* 用户名
*/
private String username;
/**
* 账户余额
*/
private Double balance;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", username='" + username + '\'' +
", balance=" + balance +
'}';
}
}(2)编写 Dao 层接口
创建接口 AccountDao,并在接口中定义添加、更新和删除账户的方法:
package io.weew12.github;
public interface AccountDao {
// 添加
int addAccount(Account account);
// 更新
int updateAccount(Account account);
// 删除
int deleteAccount(int id);
}(3)实现 Dao 层接口
创建 AccountDao 接口的实现类 AccountDaoImpl,并在类中实现添加、更新和删除账户的方法:
package io.weew12.github;
import org.springframework.jdbc.core.JdbcTemplate;
public class AccountDaoImpl implements AccountDao {
/**
* 定义 JdbcTemplate 属性及其 setter 方法
*/
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
/**
* 添加账户
*/
@Override
public int addAccount(Account account) {
// 定义sql
String sql = "insert into heima_ssm_book.ch9_account(username, balance) values (?, ?)";
// 定义数组来存放sql语句中的参数
Object[] objects = {
account.getUsername(), account.getBalance()
};
// 执行添加操作 返回受影响的记录条数
return this.jdbcTemplate.update(sql, objects);
}
/**
* 更新账户
*/
@Override
public int updateAccount(Account account) {
// 定义sql
String sql = "update heima_ssm_book.ch9_account set username=?, balance=? where id=?";
// 定义数组存放sql语句中的参数
Object[] objects = {
account.getUsername(), account.getBalance(), account.getId()
};
// 执行更新操作 返回受影响的记录条数
return this.jdbcTemplate.update(sql, objects);
}
/**
* 删除账户
*/
@Override
public int deleteAccount(int id) {
// 定义sql
String sql = "delete from heima_ssm_book.ch9_account where id = ?";
// 执行删除操作 返回受影响的记录条数
return this.jdbcTemplate.update(sql, id);
}
}在上述代码中,定义 addAccount() 操作;定义 updateAccount() 操作;定义 deleteAccount() 操作。从以上三种操作的代码可以看出,添加、更新和删除操作的实现步骤类似,只是定义的 SQL 语句有所不同。
(4)编写配置文件
在 applicationContext.xml 中定义一个 id 为 accountDao 的 Bean,将 jdbcTemplate 注入 accountDao 实例中:
<?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">
......
<!-- 定义 id 为 accountDao 的 Bean -->
<bean id="accountDao" class="io.weew12.github.AccountDaoImpl">
<!-- 将 jdbcTemplate 注入 accountDao 实例中 -->
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
</beans>(5)测试添加功能
创建测试类 TestAddAccount,用于添加用户账户信息:
package com.itheima;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestAddAccount {
public static void main(String[] args) {
// 加载配置文件
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
// 获取 AccountDao 实例
AccountDao accountDao = (AccountDao) applicationContext.getBean("accountDao");
// 创建 Account 对象,并向 Account 对象中添加数据
Account account = new Account();
account.setUsername("tom");
account.setBalance(1000.00);
// 执行 addAccount() 方法,并获取返回结果
int num = accountDao.addAccount(account);
if (num > 0) {
System.out.println("成功插入了 " + num + " 条数据!");
} else {
System.out.println("插入操作执行失败!");
}
}
}在上述代码中,调用了 AccountDao 对象的 addAccount() 方法向数据表 account 中添加一条数据;通过返回的受影响行数判断数据是否插入成功。在 IDEA 中启动 TestAddAccount 类,控制台输出结果如图所示。

此时再次查询数据库中的 ch9_account 表,结果如图所示。

从图可看出,account 表中新增了一条数据,说明使用 JdbcTemplate 的 update() 方法已成功向数据表 account 中插入了一条数据。
(6)测试更新操作
执行完插入操作后,下面调用 JdbcTemplate 类的 update() 方法执行更新操作。创建测试类 TestUpdateAccount,用于更新用户账户信息:
package io.weew12.github;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestUpdateAccount {
public static void main(String[] args) {
// 加载配置文件
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
// 获取 AccountDao 实例
AccountDao accountDao = applicationContext.getBean("accountDao", AccountDao.class);
// 创建 Account 对象,并向 Account 对象中添加数据
Account account = new Account();
account.setId(1);
account.setUsername("tony");
account.setBalance(2000.00);
// 执行 updateAccount() 方法,并获取返回结果
int rows = accountDao.updateAccount(account);
if (rows > 0) {
System.out.println("成功更新了 " + rows + "条数据");
} else {
System.out.println("更新操作执行失败!");
}
}
}在上述代码中,增加了 id 属性值的设置;将余额修改为 2000.00;调用了 AccountDao 对象中的 updateAccount() 方法执行对数据表的更新操作。
在 IDEA 中启动 TestUpdateAccount 类,控制台输出结果如图所示。

此时再次查询数据库中的 ch9_account 表,结果如图所示。

从图可看出,account 表中 id 为 1 的 balance 字段数值被修改为 2000,由此可知调用 JdbcTemplate 的 update() 方法已成功更新了表中 id 为 1 的账户余额信息。
(7)测试删除操作
执行完更新操作后,最后调用 JdbcTemplate 类的 update() 方法执行删除操作。创建测试类 TestDeleteAccount,该类主要用于删除用户账户信息:
package io.weew12.github;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestDeleteAccount {
public static void main(String[] args) {
// 加载配置文件
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
// 获取 AccountDao 实例
AccountDao accountDao = applicationContext.getBean("accountDao", AccountDao.class);
// 执行 updateAccount() 方法,并获取返回结果
int rows = accountDao.deleteAccount(1);
if (rows > 0) {
System.out.println("成功删除了 " + rows + "条数据");
} else {
System.out.println("删除操作执行失败!");
}
}
}在上述代码中,调用 AccountDao 对象中的 deleteAccount() 方法删除 ch9_account 表中 id 为 1 的数据。
在 IDEA 中启动 TestDeleteAccount 类,控制台输出结果如图所示。

此时再次查询数据库中的 ch9_account 表,结果如图所示。

从图可看出,ch9_account 表为空,表明程序调用 JdbcTemplate 的 update() 方法成功删除了 id 为 1 的数据。
9.2.3 query() 方法
JdbcTemplate 类中还提供了一系列 query() 方法用于处理数据库表的各种查询操作,如表 9-4 所示。
表9-4 JdbcTemplate类常用的query()方法
| 方法 | 说明 |
|---|---|
List query(String sql, RowMapper rowMapper) | 执行String类型参数提供的SQL语句,并通过参数rowMapper返回一个List类型的结果 |
List query(String sql, PreparedStatementSetter pss, RowMapper rowMapper) | 根据String类型参数提供的SQL语句创建PreparedStatement对象,通过参数rowMapper将结果返回到List中 |
List query(String sql, Object[] args, RowMapper rowMapper) | 使用Object[]的值来设置SQL语句中的参数值,RowMapper是个回调方法,直接返回List类型的数据 |
<T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args) | 将args参数绑定到SQL语句中,并通过参数rowMapper返回单行记录 |
<T> List<T> queryForList(String sql, Object[] args, Class<T> elementType) | 该方法可以返回多行数据的结果,但必须返回列表,args参数是SQL语句中的参数,elementType参数返回的是List数据类型 |
了解了 JdbcTemplate 类常用的 query() 方法后,下面通过一个具体案例演示 query() 方法的使用,具体步骤如下。
(1)插入数据
向数据表 ch9_account 中插入几条数据:
use heima_ssm_book;
insert into ch9_account
values (1, 'zhangsan', 100),
(2, 'lisi', 500),
(3, 'wangwu', 300);插入数据后,ch9_account 表中的数据如图所示。

(2)编写查询方法
在 AccountDao 接口中,声明 findAccountById() 方法,通过 id 查询单个账户信息;声明 findAllAccount() 方法,用于查询所有账户信息,代码如下:
package io.weew12.github;
import java.util.List;
public interface AccountDao {
......
// 根据id查询单个账户信息
Account findAccountById(int id);
// 查询所有的账户信息
List<Account> findAllAccount();
}(3)实现查询方法
在 AccountDaoImpl 类中,实现 AccountDao 接口中的 findAccountById() 方法和 findAllAccount() 方法,并调用 query() 方法分别进行查询:
package io.weew12.github;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;
public class AccountDaoImpl implements AccountDao {
/**
* 定义 JdbcTemplate 属性及其 setter 方法
*/
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
......
/**
* 根据id查询单个账户信息
*/
@Override
public Account findAccountById(int id) {
// 定义 SQL 语句
String sql = "select * from heima_ssm_book.ch9_account where id = ?";
// 创建一个新的 BeanPropertyRowMapper 对象
BeanPropertyRowMapper<Account> rowMapper = new BeanPropertyRowMapper<>(Account.class);
// 将 id 绑定到 SQL 语句中,并通过 RowMapper 返回单行记录
return this.jdbcTemplate.queryForObject(sql, rowMapper, id);
}
/**
* 查询所有的账户信息
*/
@Override
public List<Account> findAllAccount() {
// 定义 SQL 语句
String sql = "select * from heima_ssm_book.ch9_account";
// 创建一个新的 BeanPropertyRowMapper 对象
BeanPropertyRowMapper<Account> rowMapper = new BeanPropertyRowMapper<>(Account.class);
// 执行静态的 SQL 查询,并通过 RowMapper 返回结果
return this.jdbcTemplate.query(sql, rowMapper);
}
}在上述两个方法中,BeanPropertyRowMapper 是 RowMapper 接口的实现类,它可以自动将数据表中的数据映射到用户自定义的类中(前提是用户自定义类中的字段要与数据表中的字段相对应)。创建完 BeanPropertyRowMapper 对象后,在 findAccountById() 方法中通过调用 queryForObject() 方法返回单行记录,而在 findAllAccount() 方法中通过调用 query() 方法返回一个结果集合。
(4)测试条件查询
创建测试类 FindAccountByIdTest,用于测试条件查询:
package io.weew12.github;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class FindAccountByIdTest {
public static void main(String[] args) {
// 加载配置文件
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
// 获取 AccountDao 实例
AccountDao accountDao = applicationContext.getBean("accountDao", AccountDao.class);
// 执行 findAccountById() 方法
Account accountById = accountDao.findAccountById(1);
System.out.println(accountById);
}
}在 IDEA 中启动 FindAccountByIdTest 类,控制台输出结果如图所示。

(5)测试查询所有用户信息
创建测试类 FindAllAccountTest,用于查询所有用户账户信息:
package io.weew12.github;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.List;
public class FindAllAccountTest {
public static void main(String[] args) {
// 加载配置文件
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
// 获取AccountDao实例
AccountDao accountDao = applicationContext.getBean("accountDao", AccountDao.class);
// 执行 findAllAccount()方法 获取Account对象集合
List<Account> allAccount = accountDao.findAllAccount();
// 输出
allAccount.forEach(System.out::println);
}
}执行 FindAllAccountTest 类,控制台输出结果如图所示。

9.3 Spring 事务管理概述
在实际开发中,操作数据库时还会涉及事务管理问题,为此 Spring 提供了专门用于事务处理的 API。
Spring 的事务管理简化了传统的事务管理流程,并在一定程度上减少了开发人员的工作量。
9.3.1 事务管理的核心接口
Spring 包含一个名称为 spring-tx 的包,该包是 Spring 提供的用于事务管理的依赖包。spring-tx 依赖包提供了三个接口实现事务管理,这三个接口具体如下:
PlatformTransactionManager接口:用于根据属性管理事务。TransactionDefinition接口:用于定义事务的属性。TransactionStatus接口:用于界定事务的状态。
1. PlatformTransactionManager
PlatformTransactionManager 接口主要用于管理事务,该接口中提供了三个管理事务的方法,具体如表 9-5 所示。
表9-5 PlatformTransactionManager接口管理事务的方法
| 方法 | 说明 |
|---|---|
TransactionStatus getTransaction(TransactionDefinition definition) | 用于获取事务状态信息 |
void commit(TransactionStatus status) | 用于提交事务 |
void rollback(TransactionStatus status) | 用于回滚事务 |
表 9-5 列举了 PlatformTransactionManager 接口提供的方法。在实际应用中,Spring 事务管理实际是由具体的持久化技术完成的,而 PlatformTransactionManager 接口只提供统一的抽象方法。为了应对不同持久化技术的差异性,Spring 为它们提供了具体的实现类。例如,Spring 为 Spring JDBC 和 MyBatis 等依赖于 DataSource 的持久化技术提供了实现类 DataSourceTransactionManager,这样一来,Spring JDBC 或 MyBatis 等持久化技术的事务管理可以由 DataSourceTransactionManager 类实现,而且 Spring 可以通过 PlatformTransactionManager 接口对这些实现类进行统一管理。
2. TransactionDefinition
TransactionDefinition 接口中定义了事务描述相关的常量,其中包括事务的隔离级别、事务的传播行为、事务的超时时间和是否为只读事务。
(1)事务的隔离级别
事务的隔离级别是指事务之间的隔离程度,TransactionDefinition 接口中定义了五种隔离级别,具体如表 9-6 所示。
表9-6 TransactionDefinition接口中定义的隔离级别
| 隔离级别 | 说明 |
|---|---|
ISOLATION_DEFAULT | 采用当前数据库默认的事务隔离级别 |
ISOLATION_READ_UNCOMMITTED | 读未提交。允许另外一个事务读取到当前未提交的数据,隔离级别最低,可能会导致脏读、幻读或不可重复读 |
ISOLATION_READ_COMMITTED | 读已提交。被一个事务修改的数据提交后才能被另一个事务读取,可以避免脏读,无法避免幻读,而且不可重复读 |
ISOLATION_REPEATABLE_READ | 允许重复读,可以避免脏读,资源消耗上升。这是MySQL数据库的默认隔离级别 |
ISOLATION_SERIALIZABLE | 事务串行执行,也就是按照时间顺序执行多个事务,不存在并发问题,最可靠,但性能与效率最低 |
表 9-6 列举了 TransactionDefinition 接口定义的五种隔离级别,除了 ISOLATION_DEFAULT 是 TransactionDefinition 接口特有的隔离级别外,其余四种分别与 java.sql.Connection 接口定义的隔离级别相对应。
(2)事务的传播行为
事务的传播行为是指处于不同事务中的方法在相互调用时,方法执行期间的事务维护情况。例如,当一个事务的方法 B 调用另一个事务的方法 A 时,可以规定 A 方法继续在 B 方法所属的现有事务中运行,也可以规定 A 方法开启一个新事务,在新事务中运行时 B 方法所属的现有事务先挂起,等 A 方法的新事务执行完毕后再恢复。TransactionDefinition 接口中定义的七种事务传播行为,具体如表 9-7 所示。
表9-7 TransactionDefinition接口中定义的7种事务传播行为
| 事务传播行为 | 说明 |
|---|---|
PROPAGATION_REQUIRED | 默认的事务传播行为。如果当前存在一个事务,则加入该事务;如果当前没有事务,则创建一个新的事务 |
PROPAGATION_SUPPORTS | 如果当前存在一个事务,则加入该事务;如果当前没有事务,则以非事务方式执行 |
PROPAGATION_MANDATORY | 当前必须存在一个事务,如果没有,就抛出异常 |
PROPAGATION_REQUIRES_NEW | 创建一个新的事务,如果当前已存在一个事务,将已存在的事务挂起 |
PROPAGATION_NOT_SUPPORTED | 不支持事务,在没有事务的情况下执行,如果当前已存在一个事务,则将已存在的事务挂起 |
PROPAGATION_NEVER | 永远不支持当前事务,如果当前已存在一个事务,则抛出异常 |
PROPAGATION_NESTED | 如果当前存在事务,则在当前事务的一个子事务中执行 |
表 9-7 中列举了 TransactionDefinition 接口中定义的事务传播行为,开发人员可根据实际需要进行选择。
(3)事务的超时时间
事务的超时时间是指事务执行的时间界限,Spring 中事务对传播行为依赖较大,超过这个时间界限,事务将会回滚。TransactionDefinition 接口提供了 TIMEOUT_DEFAULT 常量定义事务的超时时间。
(4)是否为只读事务
当事务为只读时,该事务不修改任何数据,只读事务有助于提升性能,如果在只读事务中修改数据,会引发异常。
TransactionDefinition 接口中除了提供事务的隔离级别、事务的传播行为、事务的超时时间和是否为只读事务的常量外,还提供了一系列方法来获取事务的属性。TransactionDefinition 接口常用的方法如表 9-8 所示。
表9-8 TransactionDefinition接口常用的方法
| 方法 | 说明 |
|---|---|
int getPropagationBehavior() | 返回事务的传播行为 |
int getIsolationLevel() | 返回事务的隔离层次 |
int getTimeout() | 返回事务的超时属性 |
boolean isReadOnly() | 判断事务是否为只读 |
String getName() | 返回定义的事务名称 |
表 9-8 中列举了 TransactionDefinition 接口提供的方法,在程序中可通过调用 TransactionDefinition 接口的这些方法获取当前事务的属性。
3. TransactionStatus
TransactionStatus 接口主要用于界定事务的状态,通常情况下,编程式事务中使用该接口较多。
TransactionStatus 接口提供了一系列返回事务状态信息的方法,具体如表 9-9 所示。
表9-9 TransactionStatus接口的方法
| 方法 | 说明 |
|---|---|
boolean isNewTransaction() | 判断当前事务是否为新事务 |
boolean hasSavepoint() | 判断当前事务是否创建了一个保存点 |
boolean isRollbackOnly() | 判断当前事务是否被标记为rollback-only |
void setRollbackOnly() | 将当前事务标记为rollback-only |
boolean isCompleted() | 判断当前事务是否已经完成(提交或回滚) |
void flush() | 刷新底层的修改到数据库 |
表 9-9 中列举了 TransactionStatus 接口提供的方法,事务管理器可以通过该接口提供的方法获取事务运行的状态信息,此外,事务管理器可以通过 setRollbackOnly() 方法间接回滚事务。
9.3.2 事务管理的方式
Spring 中的事务管理分为两种方式,一种是传统的编程式事务管理,另一种是声明式事务管理。
- 编程式事务管理:通过编写代码实现的事务管理,包括定义事务的开始、正常执行后的事务提交和异常时的事务回滚。
- 声明式事务管理:通过 AOP 技术实现的事务管理,其主要思想是将事务管理作为一个“切面”代码单独编写,然后通过 AOP 技术将事务管理的“切面”代码植入到业务目标类中。
声明式事务管理最大的优点在于开发人员无需通过编程的方式来管理事务,只需在配置文件中进行相关的事务规则声明,就可以将事务规则应用到业务逻辑中。这使得开发人员可以更加专注于核心业务逻辑代码的编写,在一定程度上减少了工作量,提高了开发效率,所以在实际开发中推荐使用声明式事务管理。
9.4 声明式事务管理
在日常生活中人们会经常使用网银转账,当执行转账操作后,转出金额的账户要减去相应的金额,转入金额的账户要增加相应的金额。通常情况下,后台程序中减去和增加这两次操作会构成一个事务,事务可以保证数据安全性、一致性,因此在很多项目系统中事务管理都非常重要。通常会使用 Spring 的声明式事务管理,Spring 的声明式事务管理可以通过两种方式来实现:一种是基于 XML 的方式,另一种是基于注解的方式。
9.4.1 基于 XML 方式的声明式事务
基于 XML 方式的声明式事务管理是通过在配置文件中配置事务规则的相关声明来实现的。在使用 XML 文件配置声明式事务管理时,首先要引入 tx 命名空间,在引入 tx 命名空间之后,可以使用 <tx:advice> 元素来配置事务管理的通知,进而通过 Spring AOP 实现事务管理。
配置 <tx:advice> 元素时,通常需要指定 id 和 transaction-manager 属性,其中,id 属性是配置文件中的唯一标识,transaction-manager 属性用于指定事务管理器。除此之外,<tx:advice> 元素还包含子元素 <tx:attributes>,<tx:attributes> 元素可配置多个 <tx:method> 子元素,<tx:method> 子元素主要用于配置事务的属性。<tx:method> 元素的常用属性如表 9-10 所示。
表9-10<tx:method>元素的常用属性
| 属性 | 说明 |
|---|---|
name | 用于指定方法名的匹配模式。该属性为必选属性,它指定了与事务属性相关的方法名 |
propagation | 用于指定事务的传播行为。其属性值就是表9-7中的值 |
isolation | 用于指定事务的隔离级别 |
read-only | 用于指定事务是否只读 |
timeout | 用于指定事务的超时时间 |
rollback-for | 用于指定触发事务回滚的异常类 |
no-rollback-for | 用于指定不触发事务回滚的异常类 |
表 9-10 列举了 <tx:method> 元素的常用属性,下面通过一个案例演示如何通过 XML 方式实现 Spring 的声明式事务管理。本案例以 9.2 节的项目代码和数据表为基础,编写一个模拟银行转账的程序,要求在转账时通过 Spring 对事务进行控制。案例具体实现步骤如下。
(1)导入依赖
在项目的 pom.xml 文件中加入 aspectjweaver 依赖包和 aspectjrt 依赖包作为实现切面所需的依赖包:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.7</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>(2)定义 Dao 层方法
在 AccountDao 接口中声明转账方法 transfer():
package io.weew12.github;
import java.util.List;
public interface AccountDao {
......
// 转账方法
void transfer(String outUser, String inUser, Double money);
}(3)实现 Dao 层方法
在 AccountDaoImpl 实现类中实现 AccountDao 接口中的 transfer() 方法:
package io.weew12.github;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;
public class AccountDaoImpl implements AccountDao {
......
/**
* 转账方法
*
* @param outUser 汇款人
* @param inUser 收款人
* @param money 金额
*/
@Override
public void transfer(String outUser, String inUser, Double money) {
// 收款时,收款用户的余额=现有余额+所汇金额
this.jdbcTemplate.update("update heima_ssm_book.ch9_account set balance = balance + ?" +
"where username = ?", money, inUser);
// 模拟系统运行时的突发问题
int i = 1 / 0;
// 汇款时,汇款用户的余额=现有余额-所汇金额
this.jdbcTemplate.update("update heima_ssm_book.ch9_account set balance = balance - ?" +
"where username = ?", money, outUser);
}
}在上述代码中,两次调用 update() 方法对 account 表中的数据执行收款和汇款的更新操作。在两个操作之间,添加了一行代码 int i = 1/0 用于模拟系统运行时的突发问题,让转账操作失败。
(4)修改配置文件
<?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">
<!-- 1. 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!-- 数据库驱动 -->
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<!-- 连接数据库的 url -->
<property name="url" value="jdbc:mysql://localhost/spring?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai"/>
<!-- 连接数据库的用户名 -->
<property name="username" value="root"/>
<!-- 连接数据库的密码 -->
<property name="password" value="root"/>
</bean>
<!-- 2. 配置 JDBC 模板 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 默认必须使用数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 3. 定义 id 为 accountDao 的 Bean -->
<bean id="accountDao" class="com.itheima.AccountDaoImpl">
<!-- 将 jdbcTemplate 注入到 accountDao 实例中 -->
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<!-- 4. 事务管理器,依赖于数据源 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>(5)测试系统
创建测试类 TransactionTest:
package io.weew12.github;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TransactionTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
// 获取AccountDao实例
AccountDao accountDao = applicationContext.getBean("accountDao", AccountDao.class);
// 转账
accountDao.transfer("lisi", "zhangsan", 100.00);
System.out.println("转账完成...");
}
}在上述代码中,调用 AccountDao 实例中的 transfer() 方法,由 lisi 向 zhangsan 的账户中转人 100 元。在执行转账操作前,先查看 ch9_account 表中的数据,如图所示。

从图中可以看出,此时 lisi 的账户余额是 500,而 zhangsan 的账户余额是 100。执行上述测试方法,控制台显示结果如图所示。

从图可看到,控制台报出了 /by zero 的算术异常信息。此时再次查询数据表 ch9_account,如图所示。

由图可知,zhangsan 的账户余额增加了 100,而 lisi 的账户却没有任何变化,这种情况显然是不合理的。由于没有添加事务管理,使得系统无法保证数据的安全性与一致性,下面使用事务管理解决该问题。
(6)使用事务管理测试系统
在上述配置文件中添加事务管理的配置:
<?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:tx="http://www.springframework.org/schema/tx"
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/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 1. 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!-- 数据库驱动-->
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<!-- 连接数据库的url-->
<property name="url"
value="jdbc:mysql://localhost/heima_ssm_book?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai"/>
<!-- 数据库用户名-->
<property name="username" value="root"/>
<!-- 数据库密码-->
<property name="password" value="123456"/>
</bean>
<!-- 2. 配置 JDBC 模板 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 默认使用的数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 3. 定义 id 为 accountDao 的 Bean -->
<bean id="accountDao" class="io.weew12.github.AccountDaoImpl">
<!-- 将 jdbcTemplate 注入 accountDao 实例中 -->
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<!-- 4. 事务管理器,依赖于数据源 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 5. 编写通知:对事务进行增强(通知),需要编写切入点和具体执行事务的细节 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- name:* 表示任意方法名称 -->
<tx:method name="*" propagation="REQUIRED" isolation="DEFAULT" read-only="false"/>
</tx:attributes>
</tx:advice>
<!-- 6. 编写 AOP,让 Spring 自动为目标生成代理,需要使用 AspectJ 的表达式 -->
<aop:config>
<!-- 切入点 -->
<aop:pointcut id="txPointCut" expression="execution(* io.weew12.github.*.*(..))"/>
<!-- 切面:将切入点与通知整合 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>
</beans>第 5 步代码通过编写的通知来声明事务;第 6 步代码通过声明 AOP 的方式让 Spring 自动生成通知。

从图中可以看出,此时 lisi 的账户余额是 500,而 zhangsan 的账户余额是 200。执行上述测试方法,控制台显示结果如图所示。

从图可看到,控制台报出了 /by zero 的算术异常信息。此时再次查询数据表,效果如图所示。

从图可看到,表中 lisi 的账户余额与 zhangsan 的账户余额都没有任何变化,说明事务管理已经生效。
9.4.2 基于注解方式的声明式事务
9.4.1 节中讲解了基于 XML 方式的声明式事务,但基于 XML 的 AOP 实现存在缺点,即需要在 Spring 配置文件中配置大量的信息,造成代码冗余。为了解决此问题,可以使用基于注解的方式实现 AOP,这样做可以简化 Spring 配置文件中的代码。Spring 提供了 @Transactional 注解实现事务管理,@Transactional 注解和 XML 文件中 <tx:advice> 元素具有相同的功能。@Transactional 注解提供了一系列属性用于配置事务,具体如表 9-11 所示。
表9-11 @Transactional注解的属性
| 属性 | 说明 |
|---|---|
value | 用于指定使用的事务管理器 |
propagation | 用于指定事务的传播行为 |
isolation | 用于指定事务的隔离级别 |
timeout | 用于指定事务的超时时间 |
readOnly | 用于指定事务是否为只读 |
rollbackFor | 用于指定导致事务回滚的异常类数组 |
rollbackForClassName | 用于指定导致事务回滚的异常类数组名称 |
noRollbackFor | 用于指定不会导致事务回滚的异常类数组 |
noRollbackForClassName | 用于指定不会导致事务回滚的异常类数组名称 |
表 9-11 列举了 @Transactional 注解的属性,@Transactional 注解可以标注在接口、接口方法、类或类方法上,当标注在类上时,该类的所有 public 方法都将具有同样类型的事务属性;当标注在类中的方法上时,如果该类也标注了 @Transactional,那么类中方法的注解将会屏蔽类的注解。在实际应用中,@Transactional 注解通常应用在业务实现类上,其中,value、propagation 和 isolation 这三个属性的应用范围较广,开发人员可根据实际需要选择使用。
当使用 @Transactional 注解时,还需在 Spring 的 XML 文件中通过 <tx:annotation-driven> 元素配置事务注解驱动,<tx:annotation-driven> 元素中有一个常用属性 transaction-manager,该属性用于指定事务管理器。下面对 9.4.1 节的案例进行修改,以注解方式来实现项目中的事务管理,具体实现步骤如下。
(1)创建配置文件
在项目的 src/main/resources 目录下,创建配置文件 applicationContextAnnotation.xml,在该文件中声明事务管理器等配置信息:
<?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: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">
<!-- 配置数据源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://localhost/heima_ssm_book?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<!-- 配置jdbc模板-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- accountDao bean定义-->
<bean id="accountDao" class="io.weew12.github.AccountDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<!-- 事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 注册事务管理器注解驱动-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>与基于 XML 方式的配置文件相比,上述文件通过注册事务管理器驱动,替换了之前的编写通知和编写 AOP 的操作,这样大大减少了配置文件中的代码量。
需要注意的是,如果案例中使用了注解开发,则需要在配置文件中开启注解处理器,指定扫描哪些包下的注解。上述文件没有开启注解处理器是因为在配置文件中已经配置了 AccountDaoImpl 类的 Bean,而 @Transactional 注解就配置在该 Bean 中,可以直接生效。
(2)修改 Dao 层实现类
在 AccountDaoImpl 类的 transfer() 方法前添加事务注解 @Transactional:
package io.weew12.github;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
public class AccountDaoImpl implements AccountDao {
......
/**
* 转账方法
*
* @param outUser 汇款人
* @param inUser 收款人
* @param money 金额
*/
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT,
readOnly = false
)
@Override
public void transfer(String outUser, String inUser, Double money) {
// 收款时,收款用户的余额=现有余额+所汇金额
this.jdbcTemplate.update("update heima_ssm_book.ch9_account set balance = balance + ?" +
"where username = ?", money, inUser);
// 模拟系统运行时的突发问题
int i = 1 / 0;
// 汇款时,汇款用户的余额=现有余额-所汇金额
this.jdbcTemplate.update("update heima_ssm_book.ch9_account set balance = balance - ?" +
"where username = ?", money, outUser);
}
}上述方法已经添加了 @Transactional 注解,并且使用该注解的参数配置了事务详情,各个参数之间用英文逗号分隔。
(3)编写测试类
创建测试类 AnnotationTest:
package io.weew12.github;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AnnotationTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContextAnnotation.xml");
// 获取AccountDao 实例
AccountDao accountDao = applicationContext.getBean("accountDao", AccountDao.class);
// 调用转账方法
accountDao.transfer("lisi", "zhangsan", 100.0);
System.out.println("转账成功!");
}
}从上述代码可以看出,与 XML 方式的测试方法相比,该方法只是对配置文件的名称进行了修改。程序执行后,会出现与使用 XML 方式同样的执行结果。
9.5 案例:实现用户登录
通过所学的 Spring 数据库编程知识,实现学生管理系统的登录功能。本案例要求学生在控制台输入用户名和密码,如果用户名和密码正确,则显示用户所属班级;如果登录失败则显示登录失败。
创建数据表初始化一条用户数据:
use heima_ssm_book;
create table ch9_user
(
id int primary key auto_increment,
username varchar(30),
password varchar(30)
);
insert into ch9_user
values (1, 'weew12', '123456');
在 ch9 项目下创建 demo 包:

package io.weew12.github.demo.pojo;
public class User {
private Integer id;
private String username;
private String password;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}package io.weew12.github.demo.dao;
import io.weew12.github.demo.pojo.User;
public interface UserDao {
boolean login(User user);
}package io.weew12.github.demo.dao.Impl;
import io.weew12.github.demo.dao.UserDao;
import io.weew12.github.demo.pojo.User;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;
public class UserDaoImpl implements UserDao {
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public boolean login(User user) {
String sql = "select * from heima_ssm_book.ch9_user where username=? and password=?";
BeanPropertyRowMapper<User> rowMapper = new BeanPropertyRowMapper<>(User.class);
List<User> query = this.jdbcTemplate.query(sql, rowMapper, user.getUsername(), user.getPassword());
return !query.isEmpty();
}
}package io.weew12.github.demo.service;
import io.weew12.github.demo.pojo.User;
public interface UserService {
boolean loginService(User user);
}package io.weew12.github.demo.service.Impl;
import io.weew12.github.demo.dao.UserDao;
import io.weew12.github.demo.pojo.User;
import io.weew12.github.demo.service.UserService;
public class UserServiceImpl implements UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public boolean loginService(User user) {
return userDao.login(user);
}
}配置文件applicationContextDemo.xml
<?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: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">
<!-- 配置数据源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://localhost/heima_ssm_book?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<!-- 配置jdbc模板-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- userDao bean定义-->
<bean id="accountDao" class="io.weew12.github.demo.dao.Impl.UserDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<!-- userService bean定义-->
<bean id="userService" class="io.weew12.github.demo.service.Impl.UserServiceImpl">
<property name="userDao" ref="accountDao"/>
</bean>
<!-- 事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 注册事务管理器注解驱动-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>测试代码:
package io.weew12.github.demo;
import io.weew12.github.demo.pojo.User;
import io.weew12.github.demo.service.Impl.UserServiceImpl;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.Scanner;
public class LoginTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContextDemo.xml");
UserServiceImpl userService = applicationContext.getBean("userService", UserServiceImpl.class);
System.out.println("学生管理系统v1.0");
Scanner scanner = new Scanner(System.in);
User user = new User();
System.out.println("请输入用户名:");
user.setUsername(scanner.nextLine());
System.out.println("请输入 " + user.getUsername() + " 的密码:");
user.setPassword(scanner.nextLine());
boolean success = userService.loginService(user);
if (success) {
System.out.println("用户登录成功");
} else {
System.out.println("用户登录失败");
}
}
}运行效果:


9.6 本章小结
本章主要讲解了 Spring 的数据库编程。首先介绍了 Spring JDBC,包括 JdbcTemplate 概述和 Spring JDBC 的配置;然后讲解了 JdbcTemplate 的增删改查操作,包括 execute() 方法、update() 方法和 query() 方法;接着是 Spring 事务管理概述,包括事务管理的核心接口和事务管理的方式;最后讲解了两种实现声明式事务管理的方式,即基于 XML 方式的声明式事务和基于注解方式的声明式事务。通过学习本章的内容,可以对 Spring 的数据库编程有一定的了解。
【思考题】
- 请简述抽象类
JdbcAccessor提供的一些访问数据库时使用的公共属性。JdbcAccessor是 Spring 框架中一个非常重要的基类,它为所有需要访问数据库的组件提供了基本的支持。这个抽象类封装了一些常用的、与数据库操作相关的属性和方法,使得继承它的子类可以更方便地进行数据库操作。以下是JdbcAccessor类提供的几个关键公共属性:
这些属性共同作用,让基于 JdbcAccessor 的类能够更加灵活高效地管理其与数据库之间的交互过程。
- DataSource:这是最核心的一个属性,代表了应用程序到数据库的连接池。通过设置 DataSource 对象,可以让
JdbcAccessor知道如何获取到数据库连接。 - SQLExceptionTranslator:此属性用于转换底层发生的 SQL 异常到 Spring 的数据访问异常层次结构中,这有助于开发者更好地理解和处理这些错误。
- ignoreWarnings:这是一个布尔类型的标志位,用来指示是否忽略从数据库接收到的警告信息。默认情况下,Spring JDBC 不会自动处理这些警告,但可以通过设置此属性来改变行为。
- logger:提供了一个日志记录器实例,用于在执行数据库操作时输出调试或错误信息。
- 请简述 Spring JDBC 是如何进行配置的。
在使用 Spring JDBC 之前,首先需要对项目进行适当的配置。主要步骤包括但不限于以下几点:- 添加依赖:确保项目的构建文件(如 Maven 的 pom.xml 或 Gradle 的 build.gradle)中包含了 Spring JDBC 及其相关库的依赖项。例如,对于 Maven 来说,可能需要加入 spring-jdbc 和 spring-tx(事务管理)等模块。
- 配置数据源:在 Spring 配置文件(通常是 XML 文件或 Java 配置类)中定义一个 DataSource Bean。这一步骤非常重要,因为它是应用与数据库之间建立联系的基础。常见的实现方式有使用 C3P0、HikariCP 或者直接使用
DriverManagerDataSource等。 - 设置 JdbcTemplate:创建并配置 JdbcTemplate 实例,这是 Spring 提供的一个简化版的 JDBC 访问工具。通常做法是在配置文件中声明一个 JdbcTemplate bean,并将其注入到需要的地方。
- 事务管理:为了保证数据的一致性和完整性,还需要配置事务管理器。Spring 支持多种事务管理策略,其中最常用的是基于注解的声明式事务管理。通过在服务层的方法上添加 @Transactional 注解,可以轻松地控制事务边界。
- 启用命名参数支持:如果希望在查询语句中使用命名参数而不是传统的问号占位符,则需要额外配置
NamedParameterJdbcTemplate。
综上所述,通过上述步骤完成 Spring JDBC 的基础配置后,就可以开始编写具体的数据访问逻辑了。在整个过程中,Spring 提供了丰富的工具和便利的功能,极大地简化了开发者的编码工作量。
