最终实现的解决方案如下所示:
一般的想法是维护当前正在运行查询的所有 Hibernate 会话到发起它们的用户的 HttpSession 的映射,这样当用户关闭浏览器时,我们就能够终止正在运行的查询。
这里有两个主要挑战需要克服。一种是将 HTTP session-id 从 Web 层传播到 EJB 层,而不干扰沿途的所有方法调用——即不篡改系统中的现有代码。第二个挑战是弄清楚如何在数据库已经开始返回结果并且 Hibernate 正在用结果填充对象时取消查询。
第一个问题得到了解决,因为我们认识到沿堆栈调用的所有方法都由同一个线程处理。这是有道理的,因为我们的应用程序都存在于一个容器和does not have any remote calls 中。既然如此,我们创建了一个 Servlet 过滤器,它拦截对应用程序的每次调用,并添加一个带有当前 HTTP 会话 ID 的 ThreadLocal 变量。这样一来,HTTP session-id 将可用于沿线下方的每个方法调用。
第二个挑战有点棘手。我们发现负责运行查询并随后填充 POJO 的 Hibernate 方法称为 doQuery 并位于 org.hibernate.loader.Loader.java 类中。 (我们碰巧使用的是 Hibernate 3.5.3,但新版本的 Hibernate 也是如此。):
private List doQuery(
final SessionImplementor session,
final QueryParameters queryParameters,
final boolean returnProxies) throws SQLException, HibernateException {
final RowSelection selection = queryParameters.getRowSelection();
final int maxRows = hasMaxRows( selection ) ?
selection.getMaxRows().intValue() :
Integer.MAX_VALUE;
final int entitySpan = getEntityPersisters().length;
final ArrayList hydratedObjects = entitySpan == 0 ? null : new ArrayList( entitySpan * 10 );
final PreparedStatement st = prepareQueryStatement( queryParameters, false, session );
final ResultSet rs = getResultSet( st, queryParameters.hasAutoDiscoverScalarTypes(), queryParameters.isCallable(), selection, session );
final EntityKey optionalObjectKey = getOptionalObjectKey( queryParameters, session );
final LockMode[] lockModesArray = getLockModes( queryParameters.getLockOptions() );
final boolean createSubselects = isSubselectLoadingEnabled();
final List subselectResultKeys = createSubselects ? new ArrayList() : null;
final List results = new ArrayList();
try {
handleEmptyCollections( queryParameters.getCollectionKeys(), rs, session );
EntityKey[] keys = new EntityKey[entitySpan]; //we can reuse it for each row
if ( log.isTraceEnabled() ) log.trace( "processing result set" );
int count;
for ( count = 0; count < maxRows && rs.next(); count++ ) {
if ( log.isTraceEnabled() ) log.debug("result set row: " + count);
Object result = getRowFromResultSet(
rs,
session,
queryParameters,
lockModesArray,
optionalObjectKey,
hydratedObjects,
keys,
returnProxies
);
results.add( result );
if ( createSubselects ) {
subselectResultKeys.add(keys);
keys = new EntityKey[entitySpan]; //can't reuse in this case
}
}
if ( log.isTraceEnabled() ) {
log.trace( "done processing result set (" + count + " rows)" );
}
}
finally {
session.getBatcher().closeQueryStatement( st, rs );
}
initializeEntitiesAndCollections( hydratedObjects, rs, session, queryParameters.isReadOnly( session ) );
if ( createSubselects ) createSubselects( subselectResultKeys, queryParameters, session );
return results; //getResultList(results);
}
在此方法中,您可以看到首先以老式java.sql.ResultSet 的形式从数据库中获取结果,然后在每个集合上循环运行并从中创建一个对象。在循环之后调用的initializeEntitiesAndCollections() 方法中执行了一些额外的初始化。稍作调试后,我们发现大部分性能开销都在方法的这些部分,而不是从数据库中获取java.sql.ResultSet的部分,但cancelQuery方法仅在第一部分有效.因此,解决方案是在 for 循环中添加一个附加条件,以检查线程是否被中断,如下所示:
for ( count = 0; count < maxRows && rs.next() && !currentThread.isInterrupted(); count++ ) {
// ...
}
以及在调用initializeEntitiesAndCollections() 方法之前执行相同的检查:
if (!Thread.interrupted()) {
initializeEntitiesAndCollections(hydratedObjects, rs, session,
queryParameters.isReadOnly(session));
if (createSubselects) {
createSubselects(subselectResultKeys, queryParameters, session);
}
}
另外,通过在第二次检查中调用Thread.interrupted(),标志被清除并且不影响程序的进一步运行。现在,当要取消查询时,取消方法会访问存储在映射中的 Hibernate 会话和线程,以 HTTP 会话 ID 作为键,调用会话上的 cancelQuery 方法并调用会话的 interrupt 方法线程。