高级Java连接以及事务划分和传播

时间:2020-01-09 10:35:41  来源:igfitidea点击:

连接和事务的划分和传播并不像看起来那样容易。我们可能已经阅读过有关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);
      }
    }
  }
}