文章目录
(一)案例中添加转账方法并演示事务问题(二)分析事务的问题并编写ConnectionUtils
(三)编写事务管理工具类并分析连接和线程解绑
(四)编写业务层和持久层事务控制代码并配置spring的ioc
(五)测试转账并分析案例中的问题
(六)代理的分析
(七)基于接口的动态代理回顾
(八)基于子类的动态代理
(九)使用动态代理实现事务控制
(一)案例中添加转账方法并演示事务问题
我们创建一个新项目,并且把之前的基于xml配置的案例的代码全部复制过来
我们接下来增加一个转账方法,先写业务层接口,如下:
IAccountService.java:
/**
* 转账
*
* @param sourceName 转出账户名称
* @param targetName 转入账户名称
* @param money 转账金额
*/
void transfer(String sourceName, String targetName, Float money);
然后写业务层实现类,我们先写步骤,因为还没有写好持久层,如下:
AccountServiceImpl.java:
@Override
public void transfer(String sourceName, String targetName, Float money) {
//1.根据名称查询转出账户
//2.根据名称查询转入账户
//3.转出账户减钱
//4.转入账户加钱
//5.更新转出账户
//6.更新转入账户
}
接着写持久层接口,如下:
IAccountDao.java:
/**
* 根据名称查询账户
*
* @param accountName
* @return 如果有唯一的一个结果就返回,如果没有结果就返回null
* 如果结果集超过一个就抛异常
*/
Account findAccountByName(String accountName);
然后写持久层实现类,如下:
AccountDaoImpl.java:
@Override
public Account findAccountByName(String accountName) {
try {
List<Account> accounts = runner.query("select * from account where name = ? ", new BeanListHandler<Account>(Account.class), accountName);
if (accounts == null || accounts.size() == 0) {
return null;
}
if (accounts.size() > 1) {
throw new RuntimeException("结果集不唯一,数据有问题");
}
return accounts.get(0);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
最后完善业务层实现类,如下:
AccountServiceImpl.java:
@Override
public void transfer(String sourceName, String targetName, Float money) {
//1.根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//3.转出账户减钱
source.setMoney(source.getMoney() - money);
//4.转入账户加钱
target.setMoney(target.getMoney() + money);
//5.更新转出账户
accountDao.updateAccount(source);
//6.更新转入账户
accountDao.updateAccount(target);
}
我们测试一下
转帐前
@Test
public void testTransfer() {
as.transfer("aaa", "bbb", 100f);
}
转账后
转账是成功的,但是涉及到业务的问题,如果业务层实现类有其中一个环节出问题,都会导致灾难
转帐前
转账后
这是因为:不满足事务的一致性(减钱的事务提交了,加钱的事务没有提交,甚至都没有执行到)
(二)分析事务的问题并编写ConnectionUtils
我们的事务控制应该写在业务层实现类,在那之前我们先写两个工具类
ConnectionUtils.java:
/**
* 连接的工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定
*/
public class ConnectionUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
private DataSource dataSource;
/**
* 等待Spring用set方法注入
*
* @param dataSource
*/
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 获取当前线程上的连接
*
* @return
*/
public Connection getThreadConnection() {
try {
//1.先从ThreadLocal上获取
Connection conn = tl.get();
//2.判断当前线程上是否有连接
if (conn == null) {//无连接,要获取一个
//3.从数据源中获取一个连接,并且存入ThreadLocal中(和线程绑定)
conn = dataSource.getConnection();
tl.set(conn);
}
//4.返回当前线程上的连接
return conn;//有连接,直接返回
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
(三)编写事务管理工具类并分析连接和线程解绑
TransactionManager.java:
/**
* 和事务管理相关的工具类,它包含了开启事务、提交事务、回滚事务和释放连接等方法
*/
public class TransactionManager {
private ConnectionUtils connectionUtils;
/**
* 等待Spring用set方法注入
*
* @param connectionUtils
*/
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
/**
* 开启事务
*/
public void beginTransaction() {
try {
connectionUtils.getThreadConnection().setAutoCommit(false);//获取绑定了当前线程的连接,并且关闭自动提交事务
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 提交事务
*/
public void commit() {
try {
connectionUtils.getThreadConnection().commit();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 回滚事务
*/
public void rollback() {
try {
connectionUtils.getThreadConnection().rollback();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 释放连接
*/
public void release() {
try {
connectionUtils.getThreadConnection().close();//归还到服务器线程池中
} catch (Exception e) {
e.printStackTrace();
}
}
}
分析:
- 我们都知道我们使用了数据库连接池,它的好处显而易见,使应用刚开始加载时获取一定数量的数据库连接对象放入一个容器中,后续无需再重复获取,可以节省时间和资源
- 其实服务器(比如tomcat)也会有一个线程池,当服务器刚启动时会初始化一定数量的线程放入一个容器中,接下来每次访问都是从线程池中拿出一个线程给我们使用,我们用完就归还
- 此时我们调用TransactionManager的
release()方法
从而调用Connection的close()方法
,此时并非真正关闭线程,只是把线程归还给服务器,此时线程依然绑定着一个数据库连接对象 - 此时我们ConnectionUtils的
getThreadConnection()方法
就会出大问题,会一直认为当前线程存在数据库连接对象,可以直接获取,然而该对象已经不再是我们需要的对象了,如下: - 我们完成一个事务的控制后,应该对线程和数据库连接对象进行解绑操作
(当然我们现在是基于JavaSE工程不会存在这个问题,因为没有服务器,但是后续改成JavaEE就会出现大问题了,现在先把潜在的问题解决了)
ConnectionUtils.java:
/**
* 使连接对象与当前线程解绑
*/
public void removeConnection() {
tl.remove();
}
(四)编写业务层和持久层事务控制代码并配置spring的ioc
我们现在可以控制事务了,我们先在业务层编写事务控制代码
我们同样让spring使用set方法注入TransactionManager对象
AccountServiceImpl.java:
private TransactionManager tsManager;
/**
* 使用set方法注入TransactionManager
*
* @param tsManager
*/
public void setTsManager(TransactionManager tsManager) {
this.tsManager = tsManager;
}
接着我们就可以给每一个方法都加上事务,如下:
@Override
public List<Account> findAllAccount() {
try {
//1.开启事务
tsManager.beginTransaction();
//2.执行操作
List<Account> accounts = accountDao.findAllAccount();
//3.提交事务
tsManager.commit();
//4.返回结果
return accounts;
} catch (Exception e) {
//5.回滚操作
tsManager.rollback();
throw new RuntimeException(e);
} finally {
//6.释放连接
tsManager.release();
}
}
@Override
public Account findAccountById(Integer accountId) {
try {
//1.开启事务
tsManager.beginTransaction();
//2.执行操作
Account account = accountDao.findAccountById(accountId);
//3.提交事务
tsManager.commit();
//4.返回结果
return account;
} catch (Exception e) {
//5.回滚操作
tsManager.rollback();
throw new RuntimeException(e);
} finally {
//6.释放连接
tsManager.release();
}
}
@Override
public void saveAccount(Account account) {
try {
//1.开启事务
tsManager.beginTransaction();
//2.执行操作
accountDao.findAllAccount();
//3.提交事务
tsManager.commit();
} catch (Exception e) {
//5.回滚操作
tsManager.rollback();
} finally {
//6.释放连接
tsManager.release();
}
}
@Override
public void updateAccount(Account account) {
try {
//1.开启事务
tsManager.beginTransaction();
//2.执行操作
accountDao.updateAccount(account);
//3.提交事务
tsManager.commit();
} catch (Exception e) {
//5.回滚操作
tsManager.rollback();
} finally {
//6.释放连接
tsManager.release();
}
}
@Override
public void deleteAccount(Integer accountId) {
try {
//1.开启事务
tsManager.beginTransaction();
//2.执行操作
accountDao.deleteAccount(accountId);
//3.提交事务
tsManager.commit();
} catch (Exception e) {
//5.回滚操作
tsManager.rollback();
} finally {
//6.释放连接
tsManager.release();
}
}
@Override
public void transfer(String sourceName, String targetName, Float money) {
try {
//1.开启事务
tsManager.beginTransaction();
//2.执行操作
//2.1根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.2根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//2.3转出账户减钱
source.setMoney(source.getMoney() - money);
//2.4转入账户加钱
target.setMoney(target.getMoney() + money);
//2.5更新转出账户
accountDao.updateAccount(source);
int i = 1 / 0;
//2.6更新转入账户
accountDao.updateAccount(target);
//3.提交事务
tsManager.commit();
} catch (Exception e) {
//5.回滚操作
tsManager.rollback();
} finally {
//6.释放连接
tsManager.release();
}
}
接下来解决持久层的事务问题,先看看下面的分析
此时我们依然无法控制事务,因为我们的QueryRunner构造时传入了一个连接池对象,如下:
如果构造QueryRunner时传入了一个连接池对象,以后每条语句都会从构造时传入的连接池中获取链接对象,这样每条语句都有单独的链接对象就不能控制事务了我们不能在构造QueryRunner时传入连接池对象,如下:
我们同样让spring用set方法注入ConnectionUtils对象,以获取Connection对象
AccountDaoImpl.java:
private ConnectionUtils connectionUtils;
/**
* 等待spring用set方法注入
*
* @param connectionUtils
*/
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
然后给每一个方法传递连接对象,如下:
最后在bean.xml配置我们所需要的新的依赖即可
bean.xml:
(注意:这里的tsManager应该取名为txManager更为合适,tx是事务的意思,后面都相同)
<!-- 配置Connection的工具类 ConnectionUtils-->
<bean id="connectionUtils" class="com.zzq.utils.ConnectionUtils">
<!-- 注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务管理器-->
<bean id="tsManager" class="com.zzq.utils.TransactionManager">
<!-- 注入ConnectionUtils-->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
(五)测试转账并分析案例中的问题
为了待会儿看得清楚一点,我们打印异常
并且把aaa和bbb的账户都改为1000块钱
运行后发现控制事务成功了
总结:
- AccountDaoImpl需要用到ConnectionUtils,AccountServiceImpl需要用到TransactionManager,依赖关系非常复杂
- 我们现在虽然实现了事务控制,但是代码非常的臃肿,我们要简化代码
- 当然Spring是集成了事务控制的,但是我们先不用Spring的,先自己尝试简化代码
我们理想中的业务层实现类代码应该是这个样子的,如下:
(下面这个类当作是一个新的类,名字为AccountServiceImpl,而之前那个旧的类重命名为AccountServiceImpl_OLD)
@Override
public List<Account> findAllAccount() {
return accountDao.findAllAccount();
}
@Override
public Account findAccountById(Integer accountId) {
return accountDao.findAccountById(accountId);
}
@Override
public void saveAccount(Account account) {
accountDao.findAllAccount();
}
@Override
public void updateAccount(Account account) {
accountDao.updateAccount(account);
}
@Override
public void deleteAccount(Integer accountId) {
accountDao.deleteAccount(accountId);
}
@Override
public void transfer(String sourceName, String targetName, Float money) {
//2.1根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.2根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//2.3转出账户减钱
source.setMoney(source.getMoney() - money);
//2.4转入账户加钱
target.setMoney(target.getMoney() + money);
//2.5更新转出账户
accountDao.updateAccount(source);
int i = 1 / 0;
//2.6更新转入账户
accountDao.updateAccount(target);
}
其实这就是我们最原始的业务层实现类代码,如果不涉及事务问题还好,但是后面的transfer()
方法显然需要控制事务,这种写法的问题先然是很严重的,那我们有什么办法解决吗?
我们在Spring的第一篇博客提到过,依赖分两种:类之间的依赖和方法之间的依赖
关于类之间的依赖我们已经学会使用bean.xml或者注解去解决了,那么方法之间依赖怎么解决?我们先看看方法之间的依赖是怎么一回事,如下:
我们把TransactionManager的beginTransaction()方法
改成beginTransaction1()方法
发现AccountServiceImpl_OLD几乎每一个方法都报错,而AccountServiceImpl没有任何报错
如果想要不报错,就要对AccountServiceImpl_OLD的每一个方法都做出修改,这是相当麻烦的
并且这只是一个Service,如果有上百个Service依赖于TransactionManager的beginTransaction()方法
,那修改起来是相当麻烦的
这说明AccountServiceImpl_OLD具有很强的方法之间的耦合,这是不可取的,我们可以用代理解决
(六)代理的分析
我们专门新建一个普通的maven项目来讲解动态代理,如下:
我们接下来看一个例子来解释什么是动态代理
早期,电脑生产厂家同时负责生产和销售和售后三个服务
后期,电脑生产厂家生意越做越大,发现无法同时三个服务,于是找了一些代理商
此时代理商负责销售和售后两个服务,而生产厂家只负责生产,这样子生产厂家的生意也越做越大了
这相当于:使用动态代理增强了生产厂家对象的功能
(七)基于接口的动态代理回顾
我们接下来用代码实现上面的案例
首先代理商是不会随便代理的,会对厂家提出一定的要求,比如说厂家可以低价卖电脑给代理商、厂家可以给代理商提供售后服务。在Java中可以使用接口来确保这些要求被满足,如下:
/**
* 代理商对生产厂家要求的接口
*/
public interface IProducer {
/**
* 销售
*
* @param money
*/
void saleProduct(float money);
/**
* 售后
*
* @param money
*/
void afterService(float money);
}
同时厂家需要继承该接口,如下:
/**
* 一个生产者
*/
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);
}
}
我们先不写代理商,先让消费者直接从厂家购买电脑试试
/**
* 模拟一个消费者
*/
public class Client {
public static void main(String[] args) {
Producer producer = new Producer();
producer.saleProduct(10000f);//消费者花1万块钱向厂家买电脑
}
}
之后,随着生意越做越大,厂家找来了一个代理商
我们先回顾一下动态代理的特点,如下:
特点:字节码随用随创建,随用随加载
区别:装饰着模式上来就要加载一个类,而动态代理随用随加载
作用:不修改源码的基础上对方法增强
分类:
基于接口的动态代理(现在讲)
基于子类的动态代理(待会儿讲)
基于接口的动态代理:
涉及的类:Proxy
提供者:JDK官方
如何创建代理对象:
使用Proxy类中的newProxyInstance方法
创建代理对象的要求:
被代理类最少实现一个接口,如果没有则不能使用
newProxyInstance方法的参数:
ClassLoader:类加载器
它是用于加载代理对象字节码的,和被代理对象使用相同的类加载器。是固定写法(写被代理对象的类加载器)
Class[]:字节码数组
它是用于让代理对象和被代理对象有相同的方法(只要两者都实现了同一个接口,那么两者的方法必然相同,所以我们传接口的字节码文件即可)。固定写法(写接口字节码文件)
InvocationHandler:处理器
用于提供增强的代码
它是让我们写如何代理,我们一般都是写一个该接口的实现类
通常情况下都是匿名内部类,但不是必须的
此接口的实现类都是谁用谁写
/**
* 动态代理
*/
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(),
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
});
}
我们发现整个动态代理的编写最难的是InvocationHandler匿名内部类的编写,前面都是固定的写法
接下来详细看看InvocationHandler方法,如下:
作用:执行被代理对象的任何接口的方法都会经过该方法(类似于Filter)
参数:
proxy:代理对象的引用(通常不会使用)
method:当前执行的方法
args:当前执行方法所需要的参数
返回值:
跟被代理对象的返回值一样
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())) {
returnValue = method.invoke(producer, money * 0.8f);
}
return returnValue;
}
注意一个细节:匿名内部类访问外部对象时,该外部对象需要被final修饰,如下:
运行结果如下:
厂家卖电脑给代理商,卖了8000块钱
之后代理商可以以10000块钱的价格卖给消费者,赚中间差价其实这种基于接口的动态代理有一个致命的缺点,就是被代理的类必须实现接口
如果不实现接口是无法代理的,会抛出异常,如下:
(八)基于子类的动态代理
那我们要如何代理一个普通的Java类呢?我们可以使用基于子类的动态代理
刚才使用的基于接口的动态代理是JDK官方支持的,但是现在要讲的基于子类的动态代理需要第三方的支持,所以我们先导入maven的坐标,如下:
<dependencies>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.1_3</version>
</dependency>
</dependencies>
创建cglib包,并且刚才的Client类和Producer类拷贝进去
不让Producer类实现任何接口
在Client类中把跟基于接口的动态代理相关的方法去掉
下面先介绍一下基于子类的动态代理,如下:
特点:
字节码随用随创建,随用随加载
作用:
不修改源码的基础上对方法增强
涉及的类:
Enhancer
提供者:
第三方cglib库
如何创建代理对象:
使用Enhancer类中的create方法
创建代理对象的要求:
被代理类不能是最终类(因为最终类创建子类)
create方法的参数:
class:字节码
它是用于指定被代理对象的字节码
callback:用于提供增强的代码
我们一般写的都是该接口的子接口实现类:MethodInterceptor(为方法拦截的意思)
下面看看代码实现
Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法都会经过该方法
* @param proxy
* @param method
* @param args
* @param methodProxy
* @return
* @throws Throwable
*/
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
return null;
}
});
难点在于intercept()
方法,其实它的形参变量名被修改了之后变得很像我们之前基于接口的动态代理的匿名内部类的方法体
proxy、method和args的作用跟之前一样,不再重复介绍
methodProxy:当前执行方法的代理对象(不常用)
返回值也是跟被代理对象执行方法的返回值一样
完整代码如下:
final Producer producer = new Producer();
Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法都会经过该方法
* @param proxy
* @param method
* @param args
* @param methodProxy
* @return
* @throws Throwable
*/
public Object intercept(Object proxy, 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;
}
});
cglibProducer.saleProduct(10000f);
}
运行结果跟之前一样,不再重复展示
这里对比一下两种代理的返回值,如下:
那动态代理能对我们的转账案例提供什么帮助呢?
- 我们之前讲过的close()方法并不能真正的关闭连接对象,还需要手动解绑
我们可以对Connection类的close()
方法进行增强,让它自带解绑 - 待会儿讲的事务控制也可以动态代理
(九)使用动态代理实现事务控制
我们回到刚才银行转账的案例中,如下:
我们创建一个工厂,专门用来给Service创建代理对象,如下:
/**
* 用于创建Service的代理对象的工厂
*/
public class BeanFactory {
private IAccountService accountService;
private TransactionManager tsManager;
/**
* 使用set方法注入TransactionManager
*
* @param tsManager
*/
public void setTsManager(TransactionManager tsManager) {
this.tsManager = tsManager;
}
/**
* 等待spring用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
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtValue = null;
try {
//1.开启事务
tsManager.beginTransaction();
//2.执行操作
rtValue = method.invoke(accountService, args);
//3.提交事务
tsManager.commit();
//4.返回结果
return rtValue;
} catch (Exception e) {
//5.回滚操作
tsManager.rollback();
throw new RuntimeException(e);
} finally {
//6.释放连接
tsManager.release();
}
}
});
}
}
然后把我们所欠缺的依赖在bean.xml补全
<!-- 配置代理的Service对象-->
<bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>
<!-- 配置BeanFactory对象-->
<bean id="beanFactory" class="com.zzq.factory.BeanFactory">
<!-- 注入Service对象-->
<property name="accountService" ref="accountService"></property>
<!-- 注入事务管理器-->
<property name="tsManager" ref="tsManager"></property>
</bean>
注意:此时有两个依赖是同类型的,都是AccountService类型
此时我们的测试类不能再用简单的Autowired注解了,二选一没办法选
要加多一个Qualifier注解指定具体的依赖
运行效果是成功的,可以实现事务控制,这里不再演示
我们的业务层实现类代码变得简单了,但是bean.xml代码就很复杂了,如下:
我们有没有更好的解决办法?我们可以使用AOP去代替动态代理实现事务控制
在本文集最后一篇博客会分别从XML和注解去改造本案例