扩展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 类以外应用的代码也相对简单。