Spring--四、Spring中的JdbcTemplate和事务控制

Spring–四、Spring中的JdbcTemplate和事务控制

Spring中的JdbcTemplate

JdbcTemplate概述

它是spring框架中提供的一个对象,是对原始Jdbc API对象的简单封装。spring框架为我们提供了很多的操作模板类。

  • 操作关系型数据的:
    • JdbcTemplate
    • HibernateTemplate
  • 操作nosql数据库的:
    • RedisTemplate
  • 操作消息队列的:
    • JmsTemplate

持久层总图

环境搭建

需要导入的jar包(版本可自行选择):

  • spring-jdbc-5.1.9.RELEASE.jar(JDBC)
  • spring-tx-5.1.9.RELEASE.jar(关于事务的)
  • spring-context-5.1.9.RELEASE.jar(IOC)

编写配置文件

1
2
3
4
5
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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>

配置数据源(Spring内置数据源)

1
2
3
4
5
6
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/test?serverTimezone=UTC"></property>
<property name="username" value="root"></property>
<property name="password" value="password"></property>
</bean>

配置JdbcTemplate

1
2
3
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>

操作示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//实体类
public class UserManager {
String username, password;
double money;

@Override
public String toString() {
return "UserManager{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", money=" + money +
'}';
}

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;
}

public double getMoney() {
return money;
}

public void setMoney(double money) {
this.money = money;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//获取Spring容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//根据id获取bean对象
JdbcTemplate jt = (JdbcTemplate)ac.getBean("jdbcTemplate");
//查询
//BeanPropertyRowMapper是将查询的每一行封装成一个对象,放到集合中
//这里就是将查询到的username、password、money三个属性封装成一个UserManager类
List<UserManager> userManagers = jt.query("select * from user",
new BeanPropertyRowMapper<UserManager(UserManager.class));
//删除
jt.update("delete from user where username = ?","aaa");
//插入
jt.update("insert into user(username,password,money)values(?,?)","aaa","fff",5000);
//更新
jt.update("update user set money = money-? where username = ?",300,"aaa");

在DAO中使用JdbcTemplate

实体类如上所示

第一种方式

在DAO类中定义JdbcTemplate

UserManager的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
25
26
27
public class UserManagerDao implements IUserManagerDao {

JdbcTemplate jdbcTemplate;

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

@Override
public UserManager FindUserByName(String username) {
List<UserManager> userManagers = jdbcTemplate.query("select * from user where username=?",
new BeanPropertyRowMapper<UserManager>(UserManager.class), username);
return userManagers.isEmpty()?null:userManagers.get(0);
}

@Override
public List<UserManager> FindAll() {
return jdbcTemplate.query("select * from user",
new BeanPropertyRowMapper<UserManager>(UserManager.class));
}

@Override
public void UpdateUserByName(UserManager userManager) {
jdbcTemplate.update("update user set password=?, money=? where username=?",
userManager.getPassword(), userManager.getMoney(), userManager.getUsername());
}
}

通过xml文件配置该DAO类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/test?serverTimezone=UTC"></property>
<property name="username" value="root"></property>
<property name="password" value="password"></property>
</bean>

<bean id="userManagerDao" class="com.dao.Impl.UserManagerDao">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>

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

有一个小问题,当DAO类过多时以下代码会有很多重复代码:

1
2
3
4
JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

第二种方式

让DAO类继承JdbcDaoSupport

JdbcDaoSupport是spring框架为我们提供的一个类,该类中定义了一个JdbcTemplate对象,我们可以直接获取使用,只需要继承该类就可以了。而且此时我们不需要再用xml文件注入jdbcTemplate了,而可以直接给UserManagerDao类注入一个dataSource对象(因为JdbcDaoSupport类有dataSource的set方法,他会自动用dataSource创建JdbcTemplate),可见xml文件配置方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class UserManagerDao extends JdbcDaoSupport implements IUserManagerDao {
@Override
public UserManager FindUserByName(String username) {
List<UserManager> userManagers =
super.getJdbcTemplate().query("select * from user where username=?",
new BeanPropertyRowMapper<UserManager>(UserManager.class), username);
return userManagers.isEmpty()?null:userManagers.get(0);
}
@Override
public List<UserManager> FindAll() {
return super.getJdbcTemplate().query("select * from user",
new BeanPropertyRowMapper<UserManager>(UserManager.class));
}
@Override
public void UpdateUserByName(UserManager userManager) {
super.getJdbcTemplate().update("update user set password=?, money=? where username=?",
userManager.getPassword(), userManager.getMoney(), userManager.getUsername());
}
}
1
2
3
4
5
6
7
8
9
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/test?serverTimezone=UTC"></property>
<property name="username" value="root"></property>
<property name="password" value="password"></property>
</bean>
<bean id="userManagerDao" class="com.dao.Impl.UserManagerDao">
<property name="dataSource" ref="dataSource"></property>
</bean>

对比

当使用第二种方法时确实减少了代码的冗余,但是当使用注解配置时,由于JdbcDaoSupport类是Spring的类,我们无法修改,则第二种方法只有当使用xml文件配置时才可以使用,第一种方法可以在使用注解时使用。

Spring中的事务控制

Spring中的事务控制概述

事务控制即让由一系列动作组成的事务保持原子性、一致性、隔离性、持久性。

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

举一个经典的例子就是银行转账,银行转账分为几步:转账开始,账户一扣钱,账户二加钱,转账结束。但是当账户一扣过钱之后系统出现了某种异常转账行为不能继续进行。如果没有事务控制这次转账将会被强行结束,而事务控制则会保持其一致性,开启回滚事务,让所有信息恢复到转账开始前的状态。

Spring的事务控制都是基于AOP的,分为两种,一种是声明式事务控制(即用配置的方式来实现),一种是编程式事务控制(即用编程的方式来实现)。

Spring事务控制API介绍

20160324011156424

PlatformTransactionManager

事务管理器

1
2
3
TransactionStatus getTransaction();//获取事务的状态信息
void commit();//提交事务
void rollback();//回滚事务

TransactionDefinition

事务的定义信息对象

1
2
3
4
5
String getName();//获取事务对象名
int getIsolationLevel();//获取事务隔离级
int getPropagationBehavior();//获取事务传播行为
int getTimeout();//获取事务超时时间
boolean isReadOnly();//获取事务是否只读
  • 读写型事务:增、删、改
  • 只读型事务:查询
事务的隔离级别

1573310968216

事务的传播行为
  • REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。一般的选择(默认值)
  • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行(没有事务)
  • MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常
  • REQUERS_NEW:新建事务,如果当前在事务中,把当前事务挂起。
  • NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
  • NEVER:以非事务方式运行,如果当前存在事务,抛出异常
  • NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行REQUIRED类似的操作。
超时时间

默认值是-1,没有超时限制。如果有,以秒为单位进行设置。

是否是只读事务

建议查询时设置为只读。

TransactionStatus

描述了某个时间点上十五对象的状态信息

1
2
3
4
5
6
void flush();//刷新事务
boolean hasSavepoint();//获取是否存在存储点
boolean isCompleted();//获取事务是否完成
boolean isNewTransaction();//获取事务是否为新事务
boolean isRollbackOnly();//获取事务是否回滚
void setRollbackOnly();//设置事务回滚

基于XML的声明式事务控制

环境搭建

设置打包方式和导包

先设置打包方式为jar包,导入jar包:

  • spring-jdbc
  • spring-tx
  • spring-context
  • aspectjweaver
  • 数据库驱动
创建Spring配置文件并导入约束(此处需要导入tx和aop的命名空间)
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: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">
</beans>
准备数据库和实体类
1
2
3
4
5
create table account( 
id int primary key auto_increment,
name varchar(40),
money float
)character set utf8 collate utf8_general_ci;
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
public class Account implements Serializable {
private Integer id;
private String name;
private Float money;
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Float getMoney() {
return money;
}
public void setMoney(Float money) {
this.money = money;
}
}
准备Dao层接口和实现类

接口:

1
2
3
4
5
public interface IAccountDao {
Account findAccountById(Integer id);
Account findAccountByName(String name);
void updateAccount(Account 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
public class AccountDaoImpl extends JdbcDaoSupport implements IAccountDao {
public Account findAccountById(Integer id) {
List<Account> list =
getJdbcTemplate().query("select * from account where id = ?",
new BeanPropertyRowMapper<Account>(Account.class), id);
return list.isEmpty()?null:list.get(0);
}
public Account findAccountByName(String name) {
List<Account> list =
getJdbcTemplate().query("select * from account where name = ?",
new BeanPropertyRowMapper<Account>(Account.class), name);
if (list.isEmpty()){
return null;
}
if (list.size()>1){
throw new RuntimeException("结果不唯一");
}
return list.get(0);
}
public void updateAccount(Account account) {
getJdbcTemplate().update("update account set money = ? where id = ?",
account.getMoney(), account.getId());
}
}
准备业务层接口及实现类

接口:

1
2
3
4
public interface IAccountService {
Account findAccountById(Integer id);
void transfer(String sourceName, String targetName, Float money);
}

实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class AccountServiceImpl implements IAccountService {

private IAccountDao accountDao;

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

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

public void transfer(String sourceName, String targetName, Float money) {
Account source = accountDao.findAccountByName(sourceName);
Account target = accountDao.findAccountByName(targetName);

source.setMoney(source.getMoney()-money);
target.setMoney(target.getMoney()+money);

accountDao.updateAccount(source);
accountDao.updateAccount(target);
}
}
在配置文件中配置数据源,业务层及持久层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--配置数据源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/test?serverTimezone=UTC"></property>
<property name="username" value="root"></property>
<property name="password" value="password"></property>
</bean>

<!--配置Dao-->
<bean id="accountDao" class="com.dao.Impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>

<!--配置service-->
<bean id="accountService" class="com.service.Impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>

基于XML的配置步骤

配置事务管理器
1
2
3
4
<!--配置事务管理器-->
<bean id="transactionManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
配置事务的通知及属性
1
2
3
4
5
6
7
8
9
10
11
<!--配置事务的通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!--配置事务的属性
name为业务的核心方法名称,可以用通配符*代替
find*即表示以find开头的方法,他比单用通配符优先级更高
-->
<tx:attributes>
<tx:method name="*" read-only="false" propagation="REQUIRED"/>
<tx:method name="find*" read-only="true" propagation="SUPPORTS"></tx:method>
</tx:attributes>
</tx:advice>
  • read-only:是否是只读事务。默认false,不只读。
  • isolation:指定事务的隔离级别。默认值是使用数据库的默认隔离级别。
  • propagation:指定事务的传播行为。
  • timeout:指定超时时间。默认值为:-1。永不超时。
  • rollback-for:用于指定一个异常,当执行产生该异常时,事务回滚。产生其他异常,事务不回滚。没有默认值,任何异常都回滚。
  • no-rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,产生其他异常时,事务回滚。没有默认值,任何异常都回滚。
配置AOP切入点表达式及切入点表达式和事务通知的对应关系
1
2
3
4
5
<aop:config>
<aop:pointcut id="pt1" expression="execution(* com.service.Impl.*.*(..))"/>
<!--建立切入点表达式和事务通知的对应关系-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
</aop:config>

测试代码

1
2
3
4
5
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountService accountService = (IAccountService) ac.getBean("accountService");
accountService.transfer("a", "b", 100f);
}

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

环境搭建

设置打包方式和导包

和基于XML配置相同

创建spring的配置文件导入约束并配置扫描的包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<!--配置要扫描的包-->
<context:component-scan base-package="com"></context:component-scan>
创建数据库表和实体类

和基于XML配置相同

创建Dao接口和实现类并使用注解让spring管理

注意:当使用注解时不能再继承JdbcDaoSupport了,必须手动创建JdbcTemplate类并用@Autowried注入,还要在bean.xml文件中配置JdbcTemplate。

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
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {

@Autowired
private JdbcTemplate jdbcTemplate;

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

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

public void updateAccount(Account account) {
jdbcTemplate.update("update account set money = ? where id = ?",
account.getMoney(), account.getId());
}
}
创建业务层接口和实现类并使用注解让spring管理

去掉set方法,使用Autowried自动注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service("accountService")
public class AccountServiceImpl implements IAccountService {

@Autowired
private IAccountDao accountDao;

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

public void transfer(String sourceName, String targetName, Float money) {
Account source = accountDao.findAccountByName(sourceName);
Account target = accountDao.findAccountByName(targetName);

source.setMoney(source.getMoney()-money);
target.setMoney(target.getMoney()+money);

accountDao.updateAccount(source);
accountDao.updateAccount(target);
}
}
其他xml配置
1
2
3
4
5
6
7
8
9
10
11
<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.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/test?serverTimezone=UTC"></property>
<property name="username" value="root"></property>
<property name="password" value="password"></property>
</bean>

基于注解的配置步骤

配置事务管理器
1
2
3
4
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
在业务层使用@Transactional注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service("accountService")
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public class AccountServiceImpl implements IAccountService {

@Autowired
private IAccountDao accountDao;

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

@Transactional(readOnly = false, propagation = Propagation.REQUIRED)
public void transfer(String sourceName, String targetName, Float money) {
Account source = accountDao.findAccountByName(sourceName);
Account target = accountDao.findAccountByName(targetName);

source.setMoney(source.getMoney()-money);
target.setMoney(target.getMoney()+money);

accountDao.updateAccount(source);
accountDao.updateAccount(target);
}
}

该注解的属性和xml中的属性含义一致。该注解可以出现在接口上,类上和方法上。

  • 出现接口上,表示该接口的所有实现类都有事务支持。
  • 出现在类上,表示类中所有方法有事务支持
  • 出现在方法上,表示方法有事务支持。

以上三个位置的优先级:方法>类>接口

在配置文件中开启spring对注解事务的支持
1
2
<!--开启spring对注解事务的支持-->
<tx:annotation-driven transaction-manager="transactionManager"/>

测试代码

同上

Spring5的部分新特性

JDK升级

spring5.0在2017年9月发布了它的GA(通用)版本。该版本是基于jdk8编写的,所以jdk8以下版本将无法使用。同时,可以兼容jdk9版本。 tomcat版本要求8.5及以上。

jdk1.8版本(就是JDK8)运行时间如下:

1573372057304

当切换到jdk1.7版本之后,运行时间如下:

1573372064223

在框架中创建对象通常都是用反射来创建对象的。

核心容器的更新

Spring Framework 5.0 现在支持候选组件索引作为类路径扫描的替代方案。该功能已经在类路径扫描器中添加,以简化添加候选组件标识的步骤。

应用程序构建任务可以定义当前项目自己的 META-INF/spring.components 文件。在编译时,源模型是自包含的,JPA 实体和 Spring 组件是已被标记的。

从索引读取实体而不是扫描类路径对于小于 200 个类的小型项目是没有明显差异。但对大型项目影响较大。加载组件索引开销更低。因此,随着类数的增加,索引读取的启动时间将保持不变。

加载组件索引的耗费是廉价的。因此当类的数量不断增长,加上构建索引的启动时间仍然可以维持一个常数, 不过对于组件扫描而言,启动时间则会有明显的增长。

这个对于我们处于大型 Spring 项目的开发者所意味着的,是应用程序的启动时间将被大大缩减。虽然 20 或者 30 秒钟看似没什么,但如果每天要这样登上好几百次,加起来就够你受的了。使用了组件索引的话,就能帮助你每天过的更加高效。

你可以在 Spring 的 Jira上了解更多关于组件索引的相关信息。

结束

关于Spring框架的东西大概就这么多了。。。

Donate comment here