跳至主要内容

Dev's Ranting - OnCommittedResponseWrapper in Spring

一转眼已经到了十月,我的草稿箱里还存着一个八月遇到的诡异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(); } } }

评论