目录
2.1.1PlatformTransactionManager
一、Spring JdbcTemplate
1.1JdbcTemplate概述
JdbcTemplate它是 Spring 框架中提供的一个对象,是对原始繁琐的Jdbc API对象的简单封装。
原始Jdbc开发需要先注册驱动,接着获得Connection对象,然后获取Statement对象,执行对应的sql语句,最后获得ResultSet对象。这个过程比较繁琐。
JdbcTemplate对这些操作进行了简单的封装,为我们的数据库开发提供了简便。
Spring框架为我们提供了很多的操作模板类,这些模板将原来复杂的操作进行了简单封装,简化了开发步骤。例如:
- 操作关系型数据的 JdbcTemplate 和 HibernateTemplate
- 操作nosql数据库的 RedisTemplate
- 操作消息队列的 JmsTemplate 等等
1.2JdbcTemplate开发步骤
JdbcTemplate的开发步骤如下:
- 导入 spring-jdbc 和 spring-tx(控制事务的包) 坐标
- 创建数据库表和实体
- 创建 JdbcTemplate 对象
- 执行数据库操作
首先我们新建一个spring-jdbc项目,在 pom.xml 中导入jar包坐标,除了这两个还要导入mysql、c3p0、druid、junit、spring-context、spring-test这些包,
<dependencies>
<!--...-->
<!--省略了mysql、c3p0、druid、junit、spring-context、spring-test的导入-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
</dependencies>
然后我们在数据库中创建数据表,并给表对象创建对应的实体类。表这里我们还是用mydb1数据库的user表,
给该表传概念对应的实体类User,
public class User {
private String username;
private String password;
//此处省略了get和set方法以及ToString方法
}
然后我们创建JdbcTemplate对象执行数据库操作,
@Test//测试JdbcTemplate开发步骤
public void test1() throws PropertyVetoException {
ComboPooledDataSource dataSource=new ComboPooledDataSource();//创建数据源(连接池)对象
dataSource.setDriverClass("com.mysql.jdbc.Driver");//设置数据库驱动类
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mydb1");//设置数据库的路径
dataSource.setUser("root");//设置用户名
dataSource.setPassword("3837");//设置密码
JdbcTemplate jdbcTemplate=new JdbcTemplate();//创建JdbcTemplate对象
jdbcTemplate.setDataSource(dataSource);//设置数据源(连接池)对象
List<User> list = jdbcTemplate.query("select * from user", new BeanPropertyRowMapper<User>(User.class));
for (User user : list) {
System.out.println(user.toString());
}
}
1.3Spring产生JdbcTemplate对象
我们可以将JdbcTemplate的创建权交给Spring,将数据源DataSource的创建权也交给Spring,
在Spring容器内部将数据源DataSource注入到JdbcTemplate模版对象中,配置如下:
<!--创建数据源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/mydb1"></property>
<property name="user" value="root"></property>
<property name="password" value="3837"></property>
</bean>
<!--创建jdbcTemplate的Bean对象,设置数据库-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--为JdbcTemplate设置数据源对象引用-->
<property name="dataSource" ref="dataSource"></property>
</bean>
注意我们在test文件夹下进行测试(而不是main),所以要在test文件夹下也创建一个Test resources文件夹根目录(没有的话新建一个目录然后右键修改属性为测试资源的根目录),
然后我们进行测试,
@Test//测试Spring产生JdbcTemplate
public void test2() {
ApplicationContext app=new ClassPathXmlApplicationContext("applicationContext.xml");//获取应用上下文对象
JdbcTemplate jdbcTemplate = (JdbcTemplate) app.getBean("jdbcTemplate");//通过标识id获取JdbcTemplate对象
List<User> list = jdbcTemplate.query("select * from user", new BeanPropertyRowMapper<User>(User.class));
for (User user : list) {
System.out.println(user.toString());
}
}
1.4抽取数据库配置文件
我们将上面数据源的配置属性抽取为单独的数据库配置文件jdbc.properties,我们在test resources根目录下创建jdbc配置文件,以键值对的形式存储数据库配置属性,
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb1
jdbc.username=root
jdbc.password=3837
然后我们在Spring配置文件中加载外部的jdbc配置,
<?xml version="1.0" encoding="UTF-8"?>
<!--添加context的命名空间-->
<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">
<!--加载jdbc.properties-->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<!--创建数据源Bean对象,利用jdbc配置设置数据库连接属性-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!--创建jdbcTemplate的Bean对象,设置数据库-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--为JdbcTemplate设置数据源对象引用-->
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
然后就可以连接数据库了。
1.5JdbcTemplate的常用操作
数据库无非就是增删改查,接下来我们看看如何操作。
我们使用Spring集成Junit来测试,将JdbcTemplate对象直接注入到测试类中,不用通过应用上下文对象的getBean方法进行获取。
@Autowired//自动注入
private JdbcTemplate jdbcTemplate;
1.5.1添加数据
@Test//测试添加数据
public void test1() {
int i = jdbcTemplate.update("insert into user(id,username,password) values(?,?,?)", 4,"zhangsan", "1111");//插入用户数据
System.out.println(i);
}
1.5.2修改数据
@Test//测试修改数据
public void test2() {
int i = jdbcTemplate.update("update user set username=?, password=? where id=?", "lisi", "12.12",4);//修改用户数据
System.out.println(i);
}
1.5.3删除数据
@Test//测试删除数据
public void test5() {
ApplicationContext app=new ClassPathXmlApplicationContext("applicationContext.xml");//获取应用上下文对象
JdbcTemplate jdbcTemplate = (JdbcTemplate) app.getBean("jdbcTemplate");//通过标识id获取JdbcTemplate对象
int i = jdbcTemplate.update("delete from user where id=?", 4);//删除用户数据
System.out.println(i);
}
1.5.4查询数据
我们之前通过getBean的方法获取了JdbcTemplate对象,查询了所有用户的数据,这里我们可以修改一下,通过注入的方法获取JdbcTemplate对象,进行查询,
@Test//测试查询多个对象
public void test4() {
List<User> list = jdbcTemplate.query("select * from user",new BeanPropertyRowMapper<User>(User.class));//查询所有用户数据
System.out.println(list);
}
然后我们看看查询单个用户的数据,
@Test//测试查询单个对象
public void test5() {
User user = jdbcTemplate.queryForObject("select * from user where id=?", new BeanPropertyRowMapper<User>(User.class), 1);//查询id为1的用户信息
System.out.println(user);
}
接着是查询聚合属性数据,比如我们查询一下用户表中的用户总数,
@Test//测试查询聚合属性(比如查询user表中的用户总数)
public void test6() {
Integer count = jdbcTemplate.queryForObject("select count(*) from user",Integer.class);//查询user表中的用户总数
System.out.println(count);
}
二、 Spring的事务控制
Spring 支持两种事务控制的方法,分别为编程式和声明式:
- 编程式:使用TransactionTemplate或者直接使用底层的PlatformTransactionManager,利用代码的方式实现事务控制
- 声明式:是建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。利用配置的方式实现事务控制
Spring 声明式事务控制底层就是AOP。
声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。
和编程式事务相比,声明式事务唯一不足地方是,它的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。
首先我们学习一下编程式事务控制的三大相关对象。
2.1编程式事务控制相关对象
2.1.1PlatformTransactionManager
PlatformTransactionManager 接口是 Spring 的事务管理器,它里面提供了我们常用的操作事务的方法。
方法 | 说明 |
TransactionStatus getTransaction(TransactionDefination defination) | 获取事务的状态信息 |
void commit(TransactionStatus status) | 提交事务 |
void rollback(TransactionStatus status) | 回滚事务 |
注意:PlatformTransactionManager 是接口类型,不同的 Dao 层技术则有不同的实现类,例如:
Dao 层技术是 jdbc 或 mybatis 时,实现类为:
org.springframework.jdbc.datasource.DataSourceTransactionManager
Dao 层技术是 hibernate 时,实现类为:
org.springframework.orm.hibernate5.HibernateTransactionManager
2.1.2TransactionDefinition
TransactionDefinition 是事务的定义信息对象,里面有如下方法:
方法 | 说明 |
int getIsolationLevel() | 获得事务的隔离级别 |
int getPropogationBehavior() | 获得事务的传播行为 |
int getTimeout() | 获得超时时间 |
boolean isReadOnly() | 是否只读 |
1、事务的隔离级别
设置隔离级别,可以解决事务并发产生的问题,如脏读、不可重复读和虚读。
- ISOLATION_DEFAULT:即数据库的默认隔离级别,不同数据库默认级别不同
- ISOLATION_READ_UNCOMMITTED:不能解决任何问题
- ISOLATION_READ_COMMITTED:可以解决脏读问题
- ISOLATION_REPEATABLE_READ:可以解决脏读、不可重复读问题
- ISOLATION_SERIALIZABLE:可以解决脏读、不可重复读和虚读问题
2、事务的传播行为
事务传播行为是用来描述某一个事务传播行为修饰的方法被嵌套进另一个方法时事务如何传播。
主要有以下几种事务传播行为:
- REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。一般的选择(默认值)
- SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行(没有事务)
- MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常
- REQUERS_NEW:新建事务,如果当前在事务中,把当前事务挂起。
- NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
- NEVER:以非事务方式运行,如果当前存在事务,抛出异常
- NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行 REQUIRED 类似的操作
- 超时时间:默认值是-1,没有超时限制。如果有,以秒为单位进行设置
- 是否只读:建议查询时设置为只读
2.1.3TransactionStatus
TransactionStatus 接口提供的是事务具体的运行状态,方法介绍如下:
方法 | 说明 |
boolean hasSavepoint() | 是否存储回滚点 |
boolean isCompleted() | 事务是否完成 |
boolean isNewTransaction() | 是否是新事务 |
boolean isRollbackOnly() | 事务是否回滚 |
2.2转账业务Demo及存在的问题
我们先实现一个转账业务的Demo,实现用户A给用户B转账的功能,并且后台数据库的数据也会发生相应改变。
第一步还是在pom.xml中带入jar包坐标(包括spring-context、AspectJ、spring-test、spring-jdbc、spring-tx、mysql、c3p0、druid、junit这些包)
第二步我们在数据库中建好数据表Account和对应的数据,
第三步我们创建对应的实体类对象Account,完成set和get方法,
class Account {
private String name;
private double money;
//...
}
第四步我们创建Dao层,建立接口和对应的实现函数,这里我们创建对应的JdbcTemplate对象,使用注入的方法进行赋值,
interface AccountDao {
public void rollIn(String account, double money);
public void rollOut(String account, double money);
}
class AccountDaoImpl implements AccountDao {
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void rollIn(String account, double money) {
jdbcTemplate.update("update account set money=money+? where name=?",money,account);//执行转入操作
}
public void rollOut(String account, double money) {
jdbcTemplate.update("update account set money=money-? where name=?",money,account);//执行转出操作
}
}
第五步我们创建Service层,定义接口并实现转账的基本方法,同样的我们通过注入的方式获取到Dao层的对象。
interface AccountService {
public void transfer(String accountOut,String accountIn,double money);
}
class AccountServiceImpl implements AccountService {
private AccountDao accountDao;
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
public void transfer(String accountOut, String accountIn, double money) {
accountDao.rollOut(accountOut,money);//先从源账户转出
accountDao.rollIn(accountIn,money);//再转入到目标账户
}
}
第六步我们创建一个假的web层(Controller包),去调用service层的方法,(这里我们同样可以创建service层变量,利用注入的方式进行赋值)
class AccountController {
public static void main(String[] args) {
ApplicationContext app=new ClassPathXmlApplicationContext("applicationContext.xml");//获取应用上下文类
AccountService accountService=app.getBean(AccountService.class);//获取业务层账户类
accountService.transfer("aaa","bbb",10);//aaa给bbb转账10块
}
}
第七步我们对xml文件进行配置,包括上面Bean对象的创建,jdbc配置文件的读取,数据源对象的配置,JdbcTemplate对象的配置。
<?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">
<!--加载jdbc的配置文件-->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<!--定义数据源Bean对象,设置好数据库的连接属性-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!--定义JdbcTemplate对象,设置好数据源属性-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--定义AccountDao对象,设置JdbcTemplate-->
<bean id="accountDao" class="Dao.AccountDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<!--定义AccountService对象,设置AccountDao-->
<bean id="accountService" class="Service.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>
</beans>
第八步我们利用Controller包里的代码进行测试,实施转账业务后数据库表数据如下:
这个转账业务有一个很致命的问题,我们的转账操作是先对A账户的金额进行减少,然后增加B账户的金额,如果这个间隔中发生了一个错误导致程序异常,那么就会产生A转账了,但是B却没有收到的结果。例如我们加入一个除数为0的异常:
public void transfer(String accountOut, String accountIn, double money) {
accountDao.rollOut(accountOut,money);//先从源账户转出
int i=1/0;//除数为0的异常
accountDao.rollIn(accountIn,money);//再转入到目标账户
}
运行之后发现有异常,查看数据库发现a的钱没了,但没进b的口袋里。。。。。
为了保证这个情况不会发生,我们需要进行事务控制,将转账业务定为一个事务,保证其原子性。
即转账业务中的转出和转入操作要么全部执行,要么都不执行,在某个操作失败后,要回滚到事务执行之前的状态。
事务的控制又分为编程式(写代码实现)和声明式(写配置实现)两种。
编程式事务控制利用代码实现,先开启事务,如果发生异常被捕捉后进行事务的回滚操作,如果没有异常则进行事务的提交。
public void transfer2(String accountOut, String accountIn, double money) {
try{
//开启事务
accountDao.rollOut(accountOut,money);//先从源账户转出
int i=1/0;//除数为0的异常
accountDao.rollIn(accountIn,money);//再转入到目标账户
//提交事务
}catch (Exception e){
//事务回滚
}
}
如果我们需要控制的事务非常多,那么就必须要在每一个需要控制的地方都写上这段代码,冗余度较高。结合之前学习AOP的思路,我们可以将事务控制作为一种通知(增强的方法),将需要事务控制的方法作为切点(被增强的方法),利用Spring的AOP来织入进行动态代理,这样就可以利用配置的形式来进行事务控制,即声明式事务控制。
声明式事务管理也有两种常用的方式,一种是基于tx和aop名字空间的xml配置文件,另一种就是基于@Transactional 注解。显然基于注解的方式更简单易用,更清爽。接下来我们将分别介绍声明式事务控制的xml实现和注解实现两种方法。
2.2基于XML的声明式事务控制
既然是配置的方式进行事务控制,那我们就要操作配置文件applicationContext.xml,
<!--配置平台事务管理器,我们用的是mysql,所以用对应的实现类-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--需要给管理器注入一个数据源对象,获取连接用于控制事务-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--通知(事物的增强)-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!--设置事务的属性信息-->
<tx:attributes>
<!--指定要增强哪个方法,即指定切点-->
<tx:method name="transfer"></tx:method>
<!--也可以指定所有方法都需要增强-->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!--配置事务的AOP织入-->
<aop:config>
<!--事务的增强用advisor标签代表切面,普通增强用aspect代表切面-->
<!--在切面中我们指定切点和通知对象,进行织入关系的配置-->
<aop:advisor advice-ref="txAdvice" pointcut="execution(* Service.AccountServiceImpl.*(..))"></aop:advisor>
</aop:config>
其中,<tx:method> 代表切点方法的事务参数的配置,例如:
<tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" timeout="-1" read-only="false"/>
有以下几种属性可以配置:
- name:切点方法名称
- isolation:事务的隔离级别
- propogation:事务的传播行为
- timeout:超时时间
- read-only:是否只读
配置好了之后我们就直接开始测试,还是在转账业务中间加入一个人为的除数为0的异常,可以看到还是依然报错,但是数据库中没有出现少钱的情况,
2.3基于注解的声明式事务控制
基于注解的方式就更简单了,直接将XML中的功能以注解的方式来表达。
我们首先将Bean对象的创建用注解的方式实现,第一个是Dao层和Service层的对象,
@Repository("accountDao")//设置Dao层的bean对象
class AccountDaoImpl implements AccountDao {
@Autowired//自动注入JdbcTemplate对象
private JdbcTemplate jdbcTemplate;
public void rollIn(String account, double money) {
jdbcTemplate.update("update account set money=money+? where name=?",money,account);//执行转入操作
}
public void rollOut(String account, double money) {
jdbcTemplate.update("update account set money=money-? where name=?",money,account);//执行转出操作
}
}
@Service("accountService")//设置Service层的bean对象
class AccountServiceImpl implements AccountService {
@Autowired//自动注入AccountDao对象
private AccountDao accountDao;
public void transfer(String accountOut, String accountIn, double money) {
accountDao.rollOut(accountOut,money);//先从源账户转出
//int i=1/0;//除数为0的异常
accountDao.rollIn(accountIn,money);//再转入到目标账户
}
}
接着是DataSource和JdbcTemplate对象,我们新建一个SpringConfiguration类作为Spring的核心配置类,然后引入DataSourceConfiguration数据源配置类,
@Configuration//设置为Spring核心配置类
@ComponentScans({@ComponentScan("Annotation")})//组件扫描,扫描Annotation包下的注解
@Import(DataSourceConfiguration.class)//导入数据源配置类
class SpringConfiguration {
}
@PropertySource("classpath:jdbc.properties")//将jdbc配置文件加载到Spring容器中
class DataSourceConfiguration {
//以注入的方式加载Spring容器中的jdbc配置属性
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Autowired//自动注入的方式获取数据源对象
private DataSource dataSource;
@Bean("dataSource")//Spring会将当前方法的返回值以指定名称加载到容器中
public DataSource getDataSource() throws PropertyVetoException {
ComboPooledDataSource dataSource=new ComboPooledDataSource();//创建C3P0数据源对象
dataSource.setDriverClass(driver);//设置驱动类
dataSource.setJdbcUrl(url);//设置数据库地址
dataSource.setUser(username);//设置用户名
dataSource.setPassword(password);//设置密码
return dataSource;//返回数据源对象
}
@Bean("jdbcTemplate")//Spring会将当前方法的返回值以指定名称加载到容器中
public JdbcTemplate getJdbcTemplate() {
JdbcTemplate jdbcTemplate=new JdbcTemplate();//创建jdbc模板对象
jdbcTemplate.setDataSource(dataSource);//设置数据源
return jdbcTemplate;//返回jdbc模板对象
}
}
到此我们的转账业务就可以进行了,但是还没有进行事务控制。我们加上事务控制的注解,
首先我们创建一个事务控制配置类TxConfiguration,在事务控制配置类总创建TransactionManager对象,并且设置数据源对象,最后在核心配置类中导入事务控制配置类
@EnableTransactionManagement//开启事务支持
class TxConfiguration {
@Autowired//自动注入数据源对象
private DataSource dataSource;
@Bean("transactionManager")//将该方法的返回值以指定名称注入到Spring容器
public DataSourceTransactionManager getDataSourceTransactionManager(){
DataSourceTransactionManager dataSourceTransactionManager=new DataSourceTransactionManager();//创建TransactionManager对象
dataSourceTransactionManager.setDataSource(dataSource);//设置数据源对象
return dataSourceTransactionManager;//返回TransactionManager对象
}
}
还有一个很关键的是,我们在要加强的类上面注明当前类需要被事务控制加强,在我们的转账业务Demo中,就是写在transfer方法上,
//如果该注解写在方法上,则表明加强本方法;如果注解写在类上,则表明加强该类的所有方法;如果类和方法都有注解,则使用就近原则
@Transactional(isolation = Isolation.DEFAULT)//设置该方法为切点(即要加强的方法),并设置一些事务控制的属性
public void transfer(String accountOut, String accountIn, double money) {
accountDao.rollOut(accountOut,money);//先从源账户转出
int i=1/0;//除数为0的异常
accountDao.rollIn(accountIn,money);//再转入到目标账户
}
最后我们测试同样不会出现钱没了的情况。