Java 企业级应用开发 part 5 - J2EE part 5

Spring 数据库编程

  • 数据源配置
  • JDBC Template
  • 事务管理

Spring JDBC

要先引入相关依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!-- spring-tx包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!-- MySQL数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
<scope>runtime</scope>
</dependency>

配置数据源

applicationContext.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
56
57
<?xml version="1.0" encoding="UTF-8"?>
<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="http://www.springframework.org/schema/beans"
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:3306/spring?useUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=Asia/Shanghai&amp;useSSL=false"/>
<!--连接数据库的用户名 -->
<property name="username" value="<username>"/>
<!--连接数据库的密码 -->
<property name="password" value="<password>"/>
</bean>
<!-- 2配置JDBC模板 -->
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 默认必须使用数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!--定义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>
<!-- 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 expression="execution(* com.itheima.*.*(..))"
id="txPointCut"/>
<!-- 切面:将切入点与通知整合 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>
</beans>

JDBC Template 的使用

execute()

直接执行 SQL 语句

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
package com.itheima;

import org.springframework.context.ApplicationContext;
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配置
ApplicationContext applicationContext = new
ClassPathXmlApplicationContext("applicationContext.xml");
// 通过容器,获取JdbcTemplate的实例
JdbcTemplate jdTemplate =
(JdbcTemplate) applicationContext.getBean("jdbcTemplate");
// 使用execute()方法执行SQL语句,创建用户账户管理表account
jdTemplate.execute("create table account(" +
"id int primary key auto_increment," +
"username varchar(50)," +
"balance double)");
System.out.println("账户表account创建成功!");
}
}

编写实体类

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
package com.itheima;

public class Account {
private Integer id; // 账户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;
}

public String toString() {
return "Account [id=" + id + ", "
+ "username=" + username +
", balance=" + balance + "]";
}
}

编写 DAO 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.itheima;

import java.util.List;

public interface AccountDao {
// 添加
public int addAccount(Account account);

// 更新
public int updateAccount(Account account);

// 删除
public int deleteAccount(int id);

// 通过id查询
public Account findAccountById(int id);

// 查询所有账户
public List<Account> findAllAccount();

// 转账
public void transfer(String outUser, String inUser, Double money);
}

编写 DAO 实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.itheima;

import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
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 {
// 定义JdbcTemplate属性及其setter方法
private JdbcTemplate jdbcTemplate;

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

/**
* 具体实现方法
* ... ...
*/
}
update()

Insert 操作

AccountDao.addAccount(Account account)
1
2
3
4
5
6
7
8
9
10
11
12
13
// 添加账户
public int addAccount(Account account) {
// 定义SQL
String sql = "insert into account(username,balance) value(?,?)";
// 定义数组来存放SQL语句中的参数
Object[] obj = new Object[]{
account.getUsername(),
account.getBalance()
};
// 执行添加操作,返回的是受SQL语句影响的记录条数
int num = this.jdbcTemplate.update(sql, obj);
return num;
}
TestAddAccount.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
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("插入操作执行失败!");
}
}
}

addAccount 方法中 int num = this.jdbcTemplate.update(sql, obj); 通过传入 SQL 语句和参数数组来执行插入操作,并返回受影响的记录条数,传入参数封装成了一个通用的 Obejct[] 数组,适用于各种 SQL 语句的参数传递。
这里的 update 语义上是所有数据库更新操作,包括插入、更新和删除等,并不仅限于数据库层面的更新操作。

Update 操作

AccountDao.updateAccount(Account account)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 更新账户
public int updateAccount(Account account) {
// 定义SQL
String sql = "update account set username=?,balance=? where id = ?";
// 定义数组来存放SQL语句中的参数
Object[] params = new Object[]{
account.getUsername(),
account.getBalance(),
account.getId()
};
// 执行更新操作,返回的是受SQL语句影响的记录条数
int num = this.jdbcTemplate.update(sql, params);
return num;
}
TestUpdateAccount.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.itheima;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestUpdateAccount {
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.setId(1);
account.setUsername("tom");
account.setBalance(2000.00);
// 执行updateAccount()方法,并获取返回结果
int num = accountDao.updateAccount(account);
if (num > 0) {
System.out.println("成功修改了" + num + "条数据!");
} else {
System.out.println("修改操作执行失败!");
}
}
}

Delete 操作

AccountDao.deleteAccount(int id)
1
2
3
4
5
6
7
8
// 删除账户
public int deleteAccount(int id) {
// 定义SQL
String sql = "delete from account where id = ? ";
// 执行删除操作,返回的是受SQL语句影响的记录条数
int num = this.jdbcTemplate.update(sql, id);
return num;
}
TestDeleteAccount.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.itheima;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestDeleteAccount {
public static void main(String[] args) {
// 加载配置文件
ApplicationContext applicationContext = new
ClassPathXmlApplicationContext("applicationContext.xml");
// 获取AccountDao实例
AccountDao accountDao =
(AccountDao) applicationContext.getBean("accountDao");
// 执行deleteAccount()方法,并获取返回结果
int num = accountDao.deleteAccount(1);
if (num > 0) {
System.out.println("成功删除了" + num + "条数据!");
} else {
System.out.println("删除操作执行失败!");
}
}
}

这里单个 id 参数传入 update 方法时实际上没有封装成 Object[]

query()

查单个

AccountDao.findAccountById(int id)
1
2
3
4
5
6
7
8
9
10
// 通过id查询单个账户信息
public Account findAccountById(int id) {
//定义SQL语句
String sql = "select * from account where id = ?";
// 创建一个新的BeanPropertyRowMapper对象
RowMapper<Account> rowMapper =
new BeanPropertyRowMapper<Account>(Account.class);
// 将id绑定到SQL语句中,并通过RowMapper返回一个Object类型的单行记录
return this.jdbcTemplate.queryForObject(sql, rowMapper, id);
}

这里的 RowMapper 用于将查询结果的每一行封装成一个 Account 对象

TestFindAccountById.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.itheima;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class FindAccountByIdTest {
public static void main(String[] args) {
// 加载配置文件
ApplicationContext applicationContext = new
ClassPathXmlApplicationContext("applicationContext.xml");
// 获取AccountDao实例
AccountDao accountDao =
(AccountDao) applicationContext.getBean("accountDao");
// 执行findAccountById()方法
Account account = accountDao.findAccountById(1);
System.out.println(account);
}
}

查多个

AccountDao.findAllAccount()
1
2
3
4
5
6
7
8
9
10
//查询所有账户信息
public List<Account> findAllAccount() {
// 定义SQL语句
String sql = "select * from account";
// 创建一个新的BeanPropertyRowMapper对象
RowMapper<Account> rowMapper =
new BeanPropertyRowMapper<Account>(Account.class);
// 执行静态的SQL查询,并通过RowMapper返回结果
return this.jdbcTemplate.query(sql, rowMapper);
}

类似的通过 RowMapper 实现查询结果的转换

FindAllAccount.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.itheima;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.util.List;

public class FindAllAccountTest {
public static void main(String[] args) {
// 加载配置文件
ApplicationContext applicationContext = new
ClassPathXmlApplicationContext("applicationContext.xml");
// 获取AccountDao实例
AccountDao accountDao =
(AccountDao) applicationContext.getBean("accountDao");
// 执行findAllAccount()方法,获取Account对象的集合
List<Account> account = accountDao.findAllAccount();
// 循环输出集合中的对象
for (Account act : account) {
System.out.println(act);
}
}
}

Spring 事务管理

声明式事务管理

基于 XML 的声明式事务管理
applicationContext.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
<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 expression="execution(* com.itheima.*.*(..))"
id="txPointCut"/>
<!-- 切面:将切入点与通知整合 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>
...

这里的 <aop:advisor> 不同于 part 4 的 <aop:aspect>,前者是将切入点和通知整合成一个切面,而后者是将切入点和通知分别定义成两个独立的组件,然后通过 <aop:aspect> 将它们整合成一个切面,advisor 通常用于点对点事务管理,aspect 通常用于面向切面编程

AccountDao.transfer(String outUser, String inUser, Double money)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 转账
* inUser:收款人
* outUser:汇款人
* money:收款金额
*/
public void transfer(String outUser, String inUser, Double money) {
// 收款时,收款用户的余额=现有余额+所汇金额
this.jdbcTemplate.update("update account set balance = balance +? "
+ "where username = ?", money, inUser);
// 模拟系统运行时的突发性问题
int i = 1 / 0;
// 汇款时,汇款用户的余额=现有余额-所汇金额
this.jdbcTemplate.update("update account set balance = balance-? "
+ "where username = ?", money, outUser);
}
TransacationTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.itheima;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TransactionTest {
public static void main(String[] args) {
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("applicationContext.xml");
// 获取AccountDao实例
AccountDao accountDao =
(AccountDao) applicationContext.getBean("accountDao");
// 调用实例中的转账方法
accountDao.transfer("lisi", "zhangsan", 100.0);
// 输出提示信息
System.out.println("转账成功!");
}
}

由于在 transfer 方法中模拟了一个运行时异常,导致事务回滚,最终账户余额没有发生变化,若没有开启事务,则第一个SQL语句会生效,第二个SQL语句会失败。

基于注解的声明式事务管理
applicationContext-annotation.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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns="http://www.springframework.org/schema/beans"
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">
<!-- 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:3306/spring?useUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=Asia/Shanghai&amp;useSSL=false"/>
<!--连接数据库的用户名 -->
<property name="username" value="<username>"/>
<!--连接数据库的密码 -->
<property name="password" value="<password>" />
</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>
<!-- 5.注册事务管理器驱动 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
AccountDaoImpl.transfer(String outUser, String inUser, Double money)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 转账
* inUser:收款人
* outUser:汇款人
* money:收款金额
*/
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT, readOnly = false)
public void transfer(String outUser, String inUser, Double money) {
// 收款时,收款用户的余额=现有余额+所汇金额
this.jdbcTemplate.update("update account set balance = balance +? "
+ "where username = ?", money, inUser);
// 模拟系统运行时的突发性问题
int i = 1 / 0;
// 汇款时,汇款用户的余额=现有余额-所汇金额
this.jdbcTemplate.update("update account set balance = balance-? "
+ "where username = ?", money, outUser);
}
AnnotationTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.itheima;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AnnotationTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new
ClassPathXmlApplicationContext("applicationContext-annotation.xml");
// 获取AccountDao实例
AccountDao accountDao =
(AccountDao) applicationContext.getBean("accountDao");
// 调用实例中的转账方法
accountDao.transfer("lisi", "zhangsan", 100.0);
// 输出提示信息
System.out.println("转账成功!");
}
}

事务参数

Isolation(隔离级别)
  • DEFAULT: 使用数据库默认的隔离级别
  • READ_UNCOMMITTED: 读未提交,允许脏读、不可重复
  • READ_COMMITTED: 读已提交,禁止脏读,允许不可重复读
  • REPEATABLE_READ: 可重复读,禁止脏读、不可重复读
  • SERIALIZABLE: 串行化,禁止脏读、不可重复
Propagation(传播行为)
  • REQUIRED: 支持当前事务,如果当前没有事务,就新建一个事务
  • SUPPORTS: 支持当前事务,如果当前没有事务,就以非事务方式
  • MANDATORY: 支持当前事务,如果当前没有事务,就抛出异常
  • REQUIRES_NEW: 新建事务,如果当前存在事务,就把当前事务挂起
  • NOT_SUPPORTED: 以非事务方式执行,如果当前存在事务,就把当前事务挂起
  • NEVER: 以非事务方式执行,如果当前存在事务,就抛出异常
  • NESTED: 如果当前存在事务,则创建并在嵌套事务内执行,否则就新建一个事务