一转眼已经到了十月,我的草稿箱里还存着一个八月遇到的诡异bug:在我们的Spring Boot应用程序里,用到了Spring Security和Spring Session,这两个modules一直以来相安无事,直到有一天……
使用Hibernate的时候如果查询结果集合过大,会导致response无法迅速返回。这里有两个要命的问题,首先由于Query在执行的时候JDBC的block调用过长,HTTP Gateway会超时;其次结果集过大意味着Hibernate在真正写入response (例如将结果集转换为DTO再写入CSV)之前不得不在内存中持有所有结果的对象引用,最终会致内存占用的问题,要么GC要么OutOfMemory。
为了解决在查询结果下载时遇到的问题,我们用到了`StatelessSession`和`ScrollableResult`来获取一个相当大的`ResultSet`,通过Java 8 的`Stream` 来访问单个元素,从而实现stream downloading。但是在调试中发现无法得到完整的CSV结果,经常是写到半当中就EOF了?!
同事老R花了大半天的时间来debug,最终无比惊讶的发现是Spring在控制`HttpServletResponse`的committed事件时有隐藏手段——`OnCommittedResponseWrapper`。貌似这个class在 [Spring Session](https://github.com/spring-projects/spring-session/blob/master/spring-session-core/src/main/java/org/springframework/session/web/http/OnCommittedResponseWrapper.java) 和 [Spring Security](https://github.com/spring-projects/spring-security/blob/master/web/src/main/java/org/springframework/security/web/util/OnCommittedResponseWrapper.java) 内各自有一份copy,而且还并非完全一样。
/**
* Implement the logic for handling the {@link javax.servlet.http.HttpServletResponse}
* being committed.
*/
protected abstract void onResponseCommitted();
不仅如此,这个抽象类的方法也有着不同的实现。Spring Session的实现是在 `SessionRepositoryFilter` 内有一个 `private final class`
private final class SessionRepositoryResponseWrapper
extends OnCommittedResponseWrapper {
private final SessionRepositoryRequestWrapper request;
/**
* Create a new {@link SessionRepositoryResponseWrapper}.
* @param request the request to be wrapped
* @param response the response to be wrapped
*/
SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request,
HttpServletResponse response) {
super(response);
if (request == null) {
throw new IllegalArgumentException("request cannot be null");
}
this.request = request;
}
@Override
protected void onResponseCommitted() {
this.request.commitSession();
}
}
而 Spring Security 则是有一个扩展抽象类型:
public abstract class SaveContextOnUpdateOrErrorResponseWrapper
extends OnCommittedResponseWrapper {
在这两个类的实现都会影响到 Hibernate Session,而且由于 response wrapper 的各种decorating导致没有一个简单的办法可以避免 `onResponseCommitted`的调用。
最终,由于上述的各种限制,老R实现了这样一个方法来强制调用 `disableOnResponseCommitted()`。
private void disableResponseOnCommitted(HttpServletResponse servletResponse) {
if (servletResponse instanceof SaveContextOnUpdateOrErrorResponseWrapper) {
final SaveContextOnUpdateOrErrorResponseWrapper responseWrapper =
(SaveContextOnUpdateOrErrorResponseWrapper) servletResponse;
responseWrapper.disableSaveOnResponseCommitted();
ServletResponse innerResponse = responseWrapper.getResponse();
while (innerResponse instanceof HttpServletResponseWrapper) {
final String simpleName = innerResponse.getClass().getSimpleName();
if (simpleName.endsWith("SessionRepositoryResponseWrapper")) {
final Class superclass = innerResponse.getClass().getSuperclass();
try {
final Method method = superclass.getMethod("disableOnResponseCommitted");
method.setAccessible(true);
method.invoke(innerResponse);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
throw new RegistryInternalServerErrorException("Unable to invoke disableOnResponseCommitted",
e);
}
break;
}
innerResponse = ((HttpServletResponseWrapper) innerResponse).getResponse();
}
}
}
评论