高级Java连接以及事务划分和传播
连接和事务的划分和传播并不像看起来那样容易。我们可能已经阅读过有关DAO设计中的基本设计问题的文本" Dao设计问题":连接有逃避DAO层的趋势,因此可以划分连接/事务的寿命。如果我们还没有阅读本文,那么在继续阅读之前最好这样做。
作用域=分界
在本文中,我有时会交替使用术语"作用域"和"分界"。 "范围"是指连接生命周期的范围或者事务的范围。使用开始范围和结束范围来划分范围。
典型程序流程:事件-控制-道-连接
使用数据库的应用程序中的典型控制流程如下所示:
事件
控制DAO连接首先,发生一个事件,例如,用户单击桌面应用程序中的按钮,或者HTTP请求到达Web应用程序。其次,控件处理事件。该控件通常确定是否有必要访问数据库。如果是,则控件访问DAO层。 DAO层访问标准JDBC数据库连接以执行其工作。
控制方法范围
控件可以在同一DAO实例上调用一个或者多个方法,也可以在多个DAO实例上调用方法。这意味着连接的范围通常与对事件做出反应的控制方法的范围相同。
想象一下这个调用栈:
Event
--> Control.execute()
--> PersonDao.readPerson()
--> Connection //read person via SQL.
--> ProjectDao.readProject()
--> Connection // read project via SQL.
--> ProjectDao.assignPersonToProject()
--> Connection // update project assignments via SQL.
注意如何从控件的execute()方法内部调用两个不同的DAO。总共数据库连接被访问了3次。我们不希望每个DAO方法都打开和关闭自己的数据库连接,更不用说在自己的事务中运行了。我们希望每个DAO方法共享相同的连接,甚至可能共享相同的事务。
为此,我们需要在Control.execute()方法内部获取数据库连接(或者等效对象),并将该连接(或者其他对象)传递给每个DAO。例如,它可能看起来像下面的粗略伪代码:
Control.execute() {
Connection connection = null;
try{
getConnection();
PersonDao personDao = new PersonDao(connection);
ProjectDao projectDao = new ProjectDao(connection);
Person person = personDao.readPerson(...);
Project project = projectDao.readProject(...);
projectDao.assignPersonToProject(person, project);
} finally {
if(connection != null) connection.close();
}
}
现在,为什么这很烦人呢?
好吧,因为现在DAO层不再隐藏Connection。此外,我们不能轻松地将DAO注入到Control实例中。好吧,如果之后我们可以对它们调用setConnection()方法,则可以。但是最好注入完全配置的DAO实例。
DaoManager解决方案
在文本Dao Manager中,我描述了上一节中提到的问题的解决方案,即数据库连接从DAO泄漏出去。
" DaoManager"是我们放置在"控件"和" DAO"之间的类。 " DaoManager"天生具有单个数据库连接(或者懒惰地获得一个数据库连接的能力)。然后,从" DaoManager"中,"控件"可以访问所有" DAO"。每个DAO都是延迟创建和缓存的,并将来自'DaoManager`的连接注入到其构造函数中。
这是它外观的粗略草图:
控制
DaoManager Dao A Dao B Dao C这样,可以将DaoManager注入到完全配置的Control中。如果DaoManager注入了DataSource(或者Butterfly Persistence中的PersistenceManager),则在任何DAO'需要连接的情况下,DaoManager`可以延迟获取连接。
DaoManager还有一个可以执行一些代码并随后关闭连接或者提交事务的方法。这是从"控件"内部调用该方法的样子:
public class Control {
protected DaoManager daoManager = null;
public Control(DaoManager daoManager){
this.daoManager = daoManager;
}
public void execute(){
this.daoManager.executeAndClose(new DaoCommand(){
public Object execute(DaoManager manager){
daoManager.getDaoA();
daoManager.getDaoB();
daoManager.getDaoC();
}
});
}
}
一旦DaoManager.executeAndClose()方法完成,将关闭DaoManager内部的数据库连接。
有关DaoManager工作原理的更多信息,请参见文本Dao Manager。
即使在大多数情况下," DaoManager"解决方案似乎都能很好地工作,但在某些情况下,这还是不够的。我将在下一部分中介绍这些情况。
DaoManager的局限性
从上一节的代码示例中可以看到,当由DaoManager.executeAndClose()管理时,连接范围是executeAndClose()方法的边界。
但是,如果我们有多个将对给定事件做出响应的"控件",并且我们希望在控件之间共享数据库连接该怎么办?
例如,如果控件被组织成一棵树怎么办,就像我们使用Butterfly Web UI或者Wicket一样呢?像这样:
事件事件
控件DAO连接控件DAO连接控件DAO连接甚至更难,进入侦听同一事件的独立控件的列表。如果每个控件都独立注册为侦听器,例如在桌面应用程序中,则可能是这种情况。一个按钮。外观如下:
控制ScopingDataSource
Control DAO连接Control DAO连接Control DAO连接在这种情况下,很难(如果不是不可能)使用DaoManager解决方案。
考虑一两秒钟。想象一下,在上面两个图中的"控件"和" DAO"之间是否有一个" DaoManager"。
正是DaoManager的executeAndClose()方法确定了底层连接的寿命。如果从每个控件的" execute()"方法(或者调用控件中的中央执行方法)中调用此方法,则每个控件将分别打开和关闭连接。这正是我们试图避免的事情。或者,至少能够在需要时避免。当然,在某些情况下,我们实际上可能希望控件使用独立的连接。
ScopingDataSource解决方案
" ScopingDataSource"是解决连接和事务寿命(范围)问题的另一种解决方案。这是我为我的持久性API Persister先生实施的一种解决方案,现在在Butterfly Persistence API中继续使用。 " ScopingDataSource"将从版本5.2.0或者5.4.0移至Butterfly Persistence,该版本将于2009年发布。
ScopingDataSource是标准Java接口javax.sql.DataSource的实现。它是对标准DataSource实现的包装。这意味着,不是直接调用DataSource实例来获取连接,而是调用了ScopingDataSource。
ScopingDataSource具有以下范围划分方法:
beginConnectionScope();
endConnectionScope();
beginTransactionScope();
endTransactionScope();
abortTransactionScope();
这些方法划分了连接和事务寿命的开始和结束时间。以下各节将更详细地说明这些方法。
连接范围
控件始于调用ScopingDataSource.beginConnectionScope()。调用此方法后,只要调用此方法的踏板调用ScopingDataSource.getConnection()方法,就会返回相同的连接实例。
由ScopingDataSource返回的Connection是真实的Connection实例的包装。这个ScopingConnection忽略了所有对close()方法的调用,因此基础连接可以被重用。
当我们准备关闭连接时,控件将调用ScopingDataSource.endConnectionScope(),并且当前打开的连接(如果有)被关闭。从这里开始,ScopingDataSource的行为就像常规的DataSource一样,每次对getConnection()的调用都返回一个新的Connection。
原理说明如下:
控制ScopingDataSource
beginConnectionScope()端点EndConnectionScope()getConnection()getConnection()对" beginConnectionScope()"和" endConnectionScope()"的调用不必位于同一方法内,也不必位于同一类内。它们只需要位于同一执行线程中即可。
ScopingDataSource解决方案的优点是DAO不需要了解任何信息。DAO可以在其构造函数中使用" DataSource",并从中获取连接。无论DAO是在DAO内部获得单个连接并在方法之间共享它,还是获得一个新的连接并在每个方法内再次关闭它都无关紧要。DAO也可以在其构造器中进行"连接"操作,而仍然无需了解范围界定的工作原理。简而言之,我们在DAO设计中拥有很大的自由度。
包装的连接可能导致问题的唯一时间是,如果我们使用的是需要原始连接的API。 Oracle的Advanced Queue(AQ)API出现了此问题(或者至少在2005年出现过此问题)。该API不适用于连接包装。仅适用于Oracle自己的" Connection"实现。死人烦!
这是一个代码草图,显示了如何使用" ScopingDataSource":
public class DBControlBase {
protected ScopingDataSource scopingDataSource = null;
public DBControlBase(ScopingDataSource dataSource){
this.scopingDataSource = dataSource;
}
public void execute(){
Exception error = null;
try{
this.scopingDataSource.beginConnectionScope();
doExecute();
} catch(Exception e){
error = e;
} finally {
this.scopingDataSource.endConnectionScope(e);
}
}
public void doExecute() {
PersonDao personDao = new PersonDao (this.scopingDataSource);
ProjectDao projectDao = new ProjectDao(this.scopingDataSource);
//do DAO work here...
}
}
我们可以扩展DBControlBase并覆盖doExecute()方法,然后为我们完成所有连接作用域。
但是,等等,效果与我们使用DaoManager所获得的效果几乎不一样吗?是的,在上面的代码草图中。但是,我们现在有更多选择。连接范围划分方法调用不必嵌入到控件中。它们也可以在Control.execute()方法外部或者父控件内部被调用。看起来是这样的:
{分块}
beginConnectionScope()endConnectionScope()控制Dao B getConnection()getConnection()一旦开始尝试这个想法,就有很多可能性。
另一个选择是像Spring一样使用方法拦截。如果Control类实现了接口,则可以实现实现相同接口的动态代理。然后,控件将包装在此动态代理中。当在控制界面上调用execute()方法时,此动态代理将调用beginConnectionScope(),然后调用控件的execute()方法,最后调用endConnectionScope()。这是动态代理的代码草图:
public class ConnectionScopeProxy implements InvocationHandler{
protected ScopingDataSource scopingDataSource = null;
protected Object wrappedTarget = null;
public ConnectionScopeProxy(
ScopingDataSource dataSource,
Object target) {
this.scopingDataSource = dataSource;
this.wrappedTarget = target;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Exception error = null;
try{
this.scopingDataSource.beginConnectionScope();
method.invoke(this.wrappedTarget, args);
} catch(Exception e){
error = e;
} finally {
this.scopingDataSource.endConnectionScope(error);
}
}
}
交易范围
事务作用域的定义与连接作用域的定义几乎相同。唯一的区别是,我们分别调用" beginTransactionScope()"和" endTransactionScope()"。
当在事务范围内从" ScopingDataSource"获得连接时,将调用" connection.setAutoCommit(false)"。这将连接置于事务状态。
事务作用域结束时,将提交事务并关闭连接。如果在提交期间发生错误,则事务将自动中止。如果在调用endTransactionScope()方法之前引发了异常,则应捕获该异常,并使用该异常调用abortTransactionScope(Exception)。
这是一个代码草图:
Exception error = null;
try{
scopingDataSource.beginTransactionScope();
//do DAO work inside transaction
} catch(Exception e){
error = e;
} finally{
if(error == null){
scopingDataSource.endTransactionScope();
} else {
scopingDataSource.abortTransactionScope(e);
}
}
事务作用域可以嵌套在连接作用域内。如果事务作用域嵌套在连接作用域内,则结束事务作用域将不会关闭连接。这样,我们可以在单个连接范围内嵌套多个事务范围,从而导致在同一连接上提交多个事务。
结束语,我将显示一个事务范围动态代理代码草图:
public class TransactionScopeProxy implements InvocationHandler{
protected ScopingDataSource scopingDataSource = null;
protected Object wrappedTarget = null;
public TransactionScopeProxy(
ScopingDataSource dataSource,
Object target) {
this.scopingDataSource = dataSource;
this.wrappedTarget = target;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Exception error = null;
try{
this.scopingDataSource.beginTransactionScope();
method.invoke(this.wrappedTarget, args);
} catch(Exception e){
error = e;
} finally {
if(error == null){
this.scopingDataSource.endTransactionScope();
} else {
this.scopingDataSource.abortTransactionScope(error);
}
}
}
}

