事务控制,动态代理,动态代理实现事务控制
环境:
Idea:2019.3.1
系统:windows10 家庭版
Jdk: 8
spring:5.0.3 release
spring文档
项目代码
一、事务控制
背景
操作账户的项目结构如下
正常流程是
首先创建一个账户业务层对象accountServiceImpl
再创建一个持久层对象accountDaoImpl
最后accountServiceImpl调用accountDao通过ConnectionUtils对象获取Conncetion连接,再执行操作,这就有个问题,按这种来写,如果有一个功能需要多条代码,例如,转账,A向B转账
/**
* 转账
* @param sourceName:转出账户名称
* @param targetName:转入账户名称
* @param money:转账金额
*/
public void transfer(String sourceName, String targetName, Float money) {
try {
//1.根据名称查询转出账户
Account sourceAccount = accountDao.findAccountByName(sourceName);
//2.根据名称查询转入账户
Account targetAccount = accountDao.findAccountByName(targetName);
//3.转出账户减钱
sourceAccount.setMoney(sourceAccount.getMoney() - money);
//4.转入账户加钱
targetAccount.setMoney(targetAccount.getMoney() + money);
//5.更新转出账户
accountDao.updateAccount(sourceAccount);
//6.更新转入账户
accountDao.updateAccount(targetAccount);
}catch (Exception e){
}
我们在这6条操作中加入一条int sum = 1/0
sourceAccount.setMoney(sourceAccount.getMoney() - money);
int sum = 1/0 <--------------------------------------------------这里
//4.转入账户加钱
targetAccount.setMoney(targetAccount.getMoney() + money);
执行程序后我们发现程序出错,转账失败,但是转出账户已经减钱了,这是因为这6条语句的事务都是相互独立的,后面语句出错也不会影响前面的事务在出错前已经提交了,我们要解决这个问题,就要将这些语句变成一个事务
事务控制
1、新建一个事务控制器类TransactionManager
该类的内容如下
import java.sql.SQLException;
/**
* 和事务相关的工具类,它包含了开启事务,提交事务,回滚事务,释放连接
*/
public class TransactionManager {
private ConnectionUtils connectionUtils;
/**
* 开始事务
*/
public void beginTranscation(){
try {
//关闭事务的自动提交,即改为手动提交
connectionUtils.getThreadConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 提交事务
*/
public void commit(){
try {
connectionUtils.getThreadConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 回滚事务
*/
public void rollback(){
try {
connectionUtils.getThreadConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 释放连接
*/
public void release(){
try {
connectionUtils.getThreadConnection().close();//并不是真的关闭,是将conn连接还回连接池中
connectionUtils.removeConnection();//解绑,将连接对象和线程解绑
} catch (SQLException e) {
e.printStackTrace();
}
}
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
}
2、修改ConnectionUtils,用ThreadLoacl<>容器将Connection连接与当前线程绑定,要使一个线程中只有一个能控制事务的对象
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* 连接的工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定
*/
public class ConnectionUtils {
//threadlocal是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
//数据源
private DataSource dataSource;
/**
*获取connection对象
*/
public Connection getThreadConnection(){
try {
//1.先从ThreadLocal上获取Connection对象
Connection conn = tl.get();
//2.判断当前线程上是否有连接
if(conn == null){
//3.从数据源中获取一个连接,并且存入ThreadLocal中
conn = dataSource.getConnection();
tl.set(conn);
}
//4.返回当前线程上的连接
return conn;
}catch (SQLException e){
throw new RuntimeException();
}
}
/**
* 将线程和连接解绑
*/
public void removeConnection(){
tl.remove();
}
/**
*设置数据源
*/
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
}
3、在业务层AccountServiceImpl中加入事务
public void transfer(String sourceName, String targetName, Float money) {
try {
//一.开启事务
transactionManager.beginTranscation();
//二.执行操作
//1.根据名称查询转出账户
Account sourceAccount = accountDao.findAccountByName(sourceName);
//2.根据名称查询转入账户
Account targetAccount = accountDao.findAccountByName(targetName);
//3.转出账户减钱
sourceAccount.setMoney(sourceAccount.getMoney() - money);
//4.转入账户加钱
targetAccount.setMoney(targetAccount.getMoney() + money);
//5.更新转出账户
accountDao.updateAccount(sourceAccount);
//6.更新转入账户
accountDao.updateAccount(targetAccount);
//三.提交事务
transactionManager.commit();
//四.返回结果
}catch (Exception e){
//五.回滚事务
transactionManager.rollback();
}finally {
//六.释放连接
transactionManager.release();
}
}
将业务操作代码放在第二步,当业务操作代码出问题,事务不会提交,相反,一切正常,事务就会提交,这样就解决了之前的事务问题
二、动态代理
特点:字节码随用随创建,随用随加载
作用:不修改源码的基础上对方法增强
分类:
1.基于接口的动态代理
2.基于子类的动态代理
基于接口的动态代理
1.涉及的类:
Proxy
2.提供者:
JDK官方
3.如何创建代理对象:
使用Proxy类中的newProxyInstance方法
4.创建基于接口的代理对象的要求:
被代理类最少实现一个接口,如果没有则不能使用
5.newProxyInstance方法参数:
ClassLoader:类加载器,用于加载代理对象字节码的,和被代理对象使用相同的类加载器,固定写法,类.getClass().getClassLoader();
Class[]:它是用于让代理对象和被代理对象有相同方法,固定写法,类.getClass().getInterfaces();
InvocationHandler:它是让我们写如何代理。我们一般都是写一些该接口的实现类,通常情况下都是匿名内部类,但不是必须的,此接口的实现类都是谁用谁写
Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler(){
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {}
})
例子
proxy类直接导入使用即可
Producer接口
/**
* 对生产厂家要求的接口
*/
public interface IProducer {
/**
* 销售
* @param money
*
*/
public void saleProduct(float money);
/**
* 售后
* @param money
*/
public void afterService(float money);
}
Producer类
/**
* 一个生产者
*/
public class Producer implements IProducer{
/**
* 销售方法
* @param money
*
*/
public void saleProduct(float money){
System.out.println("销售产品,拿到钱:"+money);
}
/**
* 售后方法
* @param money
*/
public void afterService(float money){
System.out.println("提供售后服务,拿到钱:" + money);
}
}
测试类Client
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 模拟一个消费着
*/
public class Client {
public static void main(String[] args) {
//创建一个生产商
final Producer producer = new Producer();
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() {
/**
*
* 作用:执行被代理对象的任何接口方法都会经过该方法
* 参数:
* @param proxy 代理对象的引用
* @param method 当前执行的方法
* @param args 当前执行方法所需参数
* @return 和被代理对象具有相同返回值
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//这里进行方法增强,比如,这里我们可以抽取部分提成
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())){
//提成20%
returnValue = method.invoke(producer,money*0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
}
}
运行结果
基于子类的动态代理
1.涉及的类:
Enhancer
2.提供者:
第三方cglib库
3.如何创建代理对象:
使用Enhancer类中的create方法
4.创建基于子类的代理对象的要求:
被代理对象不能是最终类
5.create方法参数:
Class:字节码,用于指定被代理对象的字节码,固定写法,类.getClass()
Callback:用于让我们写如何代理,一般写一个该接口的实现类,常用MethodInterceptor,通常情况下都是写匿名内部类,但不是必须的,谁用谁写
Enhancer.create(producer.getClass(), new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
}
});
例子
首先导入Enhancer类所需cglib库的坐标
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
Producer类
package com.cbw.cglib;
/**
* 一个生产者
*/
public class Producer {
/**
* 销售
* @param money
*
*/
public void saleProduct(float money){
System.out.println("销售产品,拿到钱:"+money);
}
/**
* 售后
* @param money
*/
public void afterService(float money){
System.out.println("提供售后服务,拿到钱:" + money);
}
}
测试类Client
package com.cbw.cglib;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 模拟一个消费着
*/
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/*
*动态代理:
* 特点:字节码随用随创建,随用随加载
* 作用:不修改源码的基础上对方法增强
* 分类:
* 1.基于接口的动态代理
* 2.基于子类的动态代理
* 这里我们使用基于子类的动态代理:
* 涉及的类:Enhancer
* 提供者:第三方cglib库
* 学习过程:
* 1.如何创建代理对象:
* 使用Enhancer类中的create方法
* 2.创建代理对象的要求:
* 被代理对象不能是最终类
* 3.create方法参数:
* Class:字节码,用于指定被代理对象的字节码,固定写法,类.getClass()
* Callback:用于让我们写如何代理,一般写一个该接口的实现类,常用MethodInterceptor,通常情况下都是写匿名内部类,但不是必须的,谁用谁写
*/
Producer proxyProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//这里进行方法增强,比如,这里我们可以抽取部分提成
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())){
returnValue = method.invoke(producer,money*0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
}
}
运行结果
AccountServiceImpl是之前事务所用到的,所以里面每个方法都套了一遍事务
而AccountServiceImpl_new则是无添加事务的普通service,我们使用这个,然后其他文件照旧
三、动态代理实现事务控制
我们使用基于接口的动态代理,项目结构如下
1、在之前事务的项目下新建BeanFactory类,这是创建service代理对象的工厂,我们使用动态代理将事务放在代理对象的增强方法invoke中
import com.cbw.service.IAccountService;
import com.cbw.utils.TransactionManager;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 用于创建service的代理对象的工厂
*/
public class BeanFactory {
//accountService对象
private IAccountService accountService;
//事务管理器对象
private TransactionManager transactionManager;
/**
* 用于注入transactionManager的set方法
* @param transactionManager
*/
public final void setTransactionManager(TransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
/**
* 用于注入accountService的set方法
* @param accountService
*/
public final void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}
/**
* 获取service代理对象的方法
* @return
*/
public IAccountService getAccountService(){
return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() {
/**
* 添加事务的支持
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtValue = null;
try {
//1.开启事务
transactionManager.beginTranscation();
//2.执行操作
rtValue = method.invoke(accountService,args);
//3.提交事务
transactionManager.commit();
//4.返回结果
return rtValue;
}catch (Exception e){
//5.回滚事务
transactionManager.rollback();
}finally {
//6.释放连接
transactionManager.release();
}
return rtValue;
}
});
}
}
根据上面事务和动态代理的知识,不难理解这个类。
这个类有一个核心方法,就是getAccountService() ,用于获取accountService的代理对象,这里简写了,直接return (IAccountService) Proxy.newProxyInstance(…..);这个对象就是accountService的代理对象
然后 invoke(….) 方法中,首先定义一个rtValue对象,这个是用来接收方法的返回值的,然后执行之前写的事务6步骤,在第2步执行method.invoke(accountService,args)方法,accountService参数是被代理的对象,args则是该方法的参数数组,执行完方法后返回值由rtValue来接收,然后第4步返回返回值,这里我们用Object类型来定义rtValue,所以在外面的return还要强转一下
补充:
因为增强方法实在匿名内部类中,又因为我们对象是用set方法注入的,所以我们要在accountService和transactionManager两个对象的set方法加上final关键字
2、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.配置代理的service对象,使用的是普通工厂的方法获取bean对象-->
<bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>
<!-- 2.配置factory对象-->
<bean id="beanFactory" class="com.cbw.factory.BeanFactory">
<!-- 注入service对象-->
<property name="accountService" ref="accountService"></property>
<!-- 注入事务管理器对象,这个在第8步-->
<property name="transactionManager" ref="transactionManager"></property>
</bean>
<!-- 3.配置service对象-->
<bean id="accountService" class="com.cbw.service.impl.AccountServiceImpl_new">
<!-- 注入accountDao-->
<property name="accountDao" ref="accountDao"></property>
<!--注入事务管理器对象-->
<property name="transactionManager" ref="transactionManager"></property>-->
</bean>
<!-- 4.配置Dao对象-->
<bean id="accountDao" class="com.cbw.dao.impl.AccountDaoImpl">
<!-- 注入runner-->
<property name="runner" ref="runner"></property>
<!--注入ConnectionUtils-->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
<!-- 5.配置runner对象-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>
<!-- 6.配置数据源-->
<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/myspringspace?useUnicode=true&characterEncoding=utf8"></property>
<property name="user" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<!-- 7.配置ConncetionUtils工具类-->
<bean id="connectionUtils" class="com.cbw.utils.ConnectionUtils">
<!-- 注入连接池-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 8.创建事务管理器对象-->
<bean id="transactionManager" class="com.cbw.utils.TransactionManager">
<!-- 注入connectionUtils对象-->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
</beans>
用了c3p0连接池,还有apache的dbUtils工具包,实现的内容是一样的,用原生jdbc也是一样的效果,除此之外,没什么要补充的
补充:
第1步中获取的bean是accountService的代理对象所以也是IAccountService接口的实现类,再加上第3步中,我们又配置了一个具有相同接口的bean对象,所以我们在使用@Autowried自动注入的时候,要加上@Qualifier(“id”)指定注入的bean类型,不然会出错
3、TestAccount测试类
Junit整合spring在上一个笔记
import com.cbw.domain.Account;
import com.cbw.service.IAccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
/**
* 使用junit单元测试测试我们的配置
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class accountTest {
@Autowired
@Qualifier("proxyAccountService")
private IAccountService accountService;
/**
* 测试转账方法
*/
@Test
public void testTransfer(){
System.out.println("转账前");
testFindAll();
accountService.transfer("aaa","bbb",200f);
System.out.println("转账后");
testFindAll();
}
/**
* 测试查找全部方法
*/
@Test
public void testFindAll(){
List<Account> accountList = accountService.findAllAccount();
System.out.println(accountList);
}
}
测试aaa给bbb转账200块,测试结果
转账成功,忽略中间的日志
我们再来测试一下,操作如果出问题,事务功能能否实现,在accountServiceImpl_new文件的transfer方法中加入 int i=1/0;
记住,aaa的钱是800,bbb是1200,如果转账出错,不会变的,测试结果
钱并没有变动,奇怪的是没有报错1/by zero