扩展Hibernate对各类数据源的支持

网络整理 - 07-27
    Hibernate内嵌了对C3P0,Proxool,JNDI数据源等数据库连接池的支持。但当我们需要使用除了这几个数据源外的其他数据源的时候就有问题了,例如我们需要用Apache的开源连接池项目DBCP,或者说我们想要使用多数JDBC驱动程序中自带的XxxxDataSource时,Hibernate就没有提供对这方面的支持。庆幸的是Hibernate做为一个强大的数据持久层组件,它在实现数据库连接方面的扩展性也是非常强大的。本文将介绍两种如何在Hibernate项目中使用自定义数据源的方法。 

  本文假设你已经有Hibernate的开发经验。

  在开始之前应该先明确你的项目中的具体情况,也就是确认Hibernate内嵌的组件是否真的无法支持你的应用需要。例如C3P0或者Proxool已经可以满足大部分数据库的需要,又或者你的数据源是在应用服务器中配置的,那么你也没有必要进行扩展,你可以直接使用DatasourceConnectionProvider来让Hibernate使用你所定义的数据源。

  那么什么时候你需要扩展Hibernate对数据源的支持呢?可能你永远也用不上,但我一直在用。我用的原因可能不能成为正当的理由,因为C3P0或者Proxool总有些小地方的不足让我不爽,个人更偏向于DBCP连接池。或许本文应该改名为《让Hibernate支持DBCP数据源》,其实DBCP只不过是我的一个具体的例子,本文具有更普遍的应用意义。下面我们具体介绍两种不同的扩展思路。

  思路一:使用外部定义数据源

  假设我们已经有了一个WEB项目,该项目采用了Struts框架,而且我们已经在Struts中配置了数据源,也有不少的代码是依赖这个数据源运行的。现在我们需要给项目中加入对Hibernate的支持,但又不想去修改旧的已经成功稳定运行的代码了。那我们该怎么办?如果同样在Hibernate配置一个数据源指到同一个数据库,相信你也不乐意这样干,因为一旦配置上有修改那么Struts和Hibernate的配置都需要修改,这个也只是麻烦一点而已,最要命的是没法让原有的代码和Hibernate共用一个数据库连接,因此事务处理也就无从谈起。

  说那么多理由,无非就是为了让Hibernate可以使用Struts中配置的数据源,而我们暂且不去考虑这是否是最好的解决方法。

  在Hibernate中有一个UserSuppliedConnectionProvider类,其实这个类什么也不干,你一旦让它干点啥吧,它还净出异常,搞得你很是恼火。在Hibernate中,这个类的含义是要求用户自己来提供数据库连接的获取方法,同时当然也要自己负责关闭连接。

  为了使用Struts中配置的数据源,我们就不能直接调用SessionFactory.openSession()方法来获取Session实例,因为你如果没有在Hibernate中配置任何的数据库连接,那Hibernate会默认让UserSuppliedConnectionProvider类来跟你捣乱,你会收到很多异常信息,反复提醒我们必须自己提供数据库连接!我们要做还是调用openSession方法,不同的是需要先从Struts的数据源中获取数据库连接,然后传递该连接给openSession方法(参照 SessionFactory.openSession(Connection) 方法)。

  下面是代码片断

//获取Session实例
public Session getSession(){
 ServletContext contxt = ....
 SessionFactory sessions = ....

 DataSource ds = (DataSource)context.getAttribute(Globals.DATA_SOURCE_KEY);
 final Connection conn = ds.getConnection();
 return sessions.openSession(conn);
}

//释放Session
public void closeSession(Session ssn){
 ssn.connection().close();
 ssn.close();
}



  需要提醒大家注意的是closeSession方法,在该方法中我们必须手工去关闭session对应的数据库连接,我们前面已经提到了,UserSuppliedConnectionProvider类就是要求用户自己提供数据库连接已经连接的关闭。如果没有调用ssn.connection().close()方法,这会导致Struts的数据源的连接没有被释放。

  同理,上面提到的Struts只是一个应用普遍的例子,实际中你可以使用任何的外部连接池,你只需要将获取到的数据库连接传递给openSession方法,并自行负责释放数据库连接即可。应该说这是一种最简单的思路,好处是对系统的变动最小,兼容原来的代码。

  思路二:扩展ConnectionProvider

  Hibernate本身是通过ConnectionProvider接口来实现管理数据库连接的。例如其自带的C3P0ConnectionProvider,ProxoolConnectionProvider等。

  在这个思路中,我们希望可以直接在Hibernate的配置文件中配置数据库连接,也就是让Hibernate独揽数据库的管理,真正做到各司其职。为了更了解该接口的使用,你不妨阅读一下Hibernate提供的上面几个类的源码。

  接下来我们需要编写一个实现了ConnectionProvider接口的类,要求这个类能支持任何的符合DataSource接口规范的数据源,同时在Hibernate的配置文件中进行参数的设定。首先我们假定我们的类名是DataSourceConnProvider,那我们的配置信息在hibernate.cfg.xml中看起来应该像下面一样

<!-- Connection Pool settings -->
<property name="connection.provider_class">
com.liusoft.dlog4j.db.DataSourceConnProvider</property>
<property name="dscp.datasource">org.apache.commons.dbcp.BasicDataSource</property>
<property name="dscp.driverClassName">sun.jdbc.odbc.JdbcOdbcDriver</property>
<property name="dscp.url">jdbc:odbc:dlog4j</property>
<property name="dscp.username">admin</property>
<property name="dscp.password"></property>
<property name="dscp.initialSize">1</property>
<property name="dscp.maxActive">200</property>
<property name="dscp.maxWait">2000</property>
<property name="dscp.defaultAutoCommit">false</property>
<property name="dscp.defaultReadOnly">false</property>
<property name="dscp.removeAbandoned">true</property>
<property name="dscp.removeAbandonedTimeout">120</property>
<!--
<property name="dscp.defaultTransactionIsolation">1</property>
-->
<property name="dscp.poolPreparedStatements">true</property>
<property name="dscp.maxOpenPreparedStatements">1000</property>

 

  在上面的配置信息中,connection.provider_class是Hibernate本身用来指定不同ConnectionProvider实现类。接下来我们规定了我们的扩展所使用的配置键值都是以dscp.开头,同时我们使用dscp.datasource来指定具体实现了DataSource接口的类名,例如如果使用DBCP这个连接池,那么这个类名应该是org.apache.commons.dbcp.BasicDataSource。对于其他以dscp.开头的且不是dscp.datasource的配置信息都会直接赋值给DataSource的实现类。例如上面的配置中,driverClassName、url、username、password等配置信息都是BasicDataSource类的属性。 下面是我们所实现的DataSourceConnProvider类的源码。

package com.liusoft.dlog4j.db;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.beanutils.BeanUtils;
import org.hibernate.HibernateException;
import org.hibernate.connection.ConnectionProvider;

import com.liusoft.dlog4j.Globals;
import com.liusoft.dlog4j.util.StringUtils;

/**
* 让Hibernate支持各种数据源
* @author Winter Lau
*/
public class DataSourceConnProvider implements ConnectionProvider {

private final static String BASE_KEY = "dscp.";
private final static String ENCODING_KEY = "dscp.encoding";
private final static String DATASOURCE_KEY = "dscp.datasource";

protected DataSource dataSource;

/* (non-Javadoc)
* @see org.hibernate.connection.ConnectionProvider#configure(java.util.Properties)
*/
public void configure(Properties props) throws HibernateException {
 String dataSourceClass = null;
 Properties new_props = new Properties();
 Iterator keys = props.keySet().iterator();
 while(keys.hasNext()){
  String key = (String)keys.next();
  if(DATASOURCE_KEY.equalsIgnoreCase(key)){
   dataSourceClass = props.getProperty(key);
  }
  else if(key.startsWith(BASE_KEY)){
   String value = props.getProperty(key);
   value = StringUtils.replace(value, "{DLOG4J}", Globals.WEBAPP_PATH);
   new_props.setProperty(key.substring(BASE_KEY.length()), value);
  }
 }
 if(dataSourceClass == null)
  throw new HibernateException("Property 朙dscp.datasource朙 no defined.");
  try {
   dataSource = (DataSource)Class.forName(dataSourceClass).newInstance();
   BeanUtils.populate(dataSource, new_props);
  } catch (Exception e) {
   throw new HibernateException(e);
  }
}

/* (non-Javadoc)
* @see org.hibernate.connection.ConnectionProvider#getConnection()
*/
public Connection getConnection() throws SQLException { 
 final Connection conn = dataSource.getConnection();
 if(useProxy && conn!=null){
  return (new _Connection(conn,encoding)).getConnection();
 }
 return conn;
}

/* (non-Javadoc)
* @see org.hibernate.connection.ConnectionProvider#closeConnection(java.sql.Connection)
*/
public void closeConnection(Connection conn) throws SQLException {
 if(conn!=null && !conn.isClosed())
  conn.close();
}

/* (non-Javadoc)
* @see org.hibernate.connection.ConnectionProvider#close()
*/
public void close() throws HibernateException {
 if(dataSource != null)
  try {
   Method mClose = dataSource.getClass().getMethod("close",null);
   mClose.invoke(dataSource, null);
  } catch (Exception e) {
   throw new HibernateException(e);
  }
  dataSource = null;
}

/* (non-Javadoc)
* @see org.hibernate.connection.ConnectionProvider#supportsAggressiveRelease()
*/
public boolean supportsAggressiveRelease() {
 return false;
}

}



  在DataSourceConnProvider类中,configure方法会在Hibernate进行初始化的过程中被调用,我们根据配置的DataSource类名创建数据源实例,并将配置参数赋值给该实例后即完成了数据源的初始化。接下来就是实现了getConnection和closeConnection方法分别是获取数据库连接和关闭连接的方法。方法close用来关闭整个数据源,该方法会在Hibernate释放时被调用。
 
  你也可以使用其他一些不同的数据源而不一定非是DBCP数据源。配置完毕后接下来的事情就简单了,直接调用SessionFactory.openSession()方法获取Session实例,直接调用session.close()释放该实例,无需再手工去关闭session所封装的connection接口。

  相比较上面两种思路而言,各有千秋。如果你真的有必要扩展Hibernate对数据源的支持,如果你没有兼容旧代码这个问题需要考虑的话,那我更倾向于第二种思路。因为它使得整个项目的各个层次分工非常清晰,而且除了ConnectionProvider 类以外应用的代码也相对简单。