高级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); } } } }