BUG的环境,WEB服务器是TOMCAT8.5, 框架是SPRING MVC 3.2.4 ,开发环境IDEA 2020,JDK1.8 ; 这是个老项目改造,我也帮同事定位的。
1. 异常情况描述:
通过chrome浏览器或ajax请求时,chrome能接收到服务段返回的数据,ajax请求直接抛network error的错误信息。网上主要又两种说法:
一种说法是返回JSON格式数据不完整导致。
另一种说法是 Transfer-Encoding:chunked 块传输有BUG。
2. BUG处理过程:
处理过程一波三折,一开始总是各种怀疑与验证。认为前端同事的问题,认为请求头的问题,认为返回头的问题。然后搜索相同的BUG情况,网i上说的两种情况也不符合。但是那天晚上在加时用 BING搜英文确实有搜到说,Tomcat 8对Spring MVC的异步处理有BUG。接着排除了前端的问题才把注意力集中在后端上来,我知道 Spring MVC DispatcherServlet 的请求时异步处理的,DispatcherServlet 的 doDispath 的部分源码:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest, false);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
String requestUri = urlPathHelper.getRequestUri(request);
logger.debug("Last-Modified value for [" + requestUri + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
try {
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
}
applyDefaultViewName(request, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Error err) {
triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err);
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
return;
}
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
代码中 WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); 就是获取处理异步请求的对象。并且发现请求过来后TOMCAT 确实有异常:
java.lang.IllegalStateException: Cannot create a session after the response has been committed
at org.apache.catalina.connector.Requisest.doGetSession(Request.java:3034)
at org.apache.catalina.connector.Request.getSession(Request.java:2468)
at org.apache.catalina.connector.RequestFacade.getSession(RequestFacade.java:896)
at org.apache.catalina.connector.RequestFacade.getSession(RequestFacade.java:908)
at javax.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:240)
报异常也很明确,说response 已经是提交的状态,所以不能创建session 了,根据异常的字面意思,就是非法的状态异常。这个异常会抛到 DispatcherServlet 里,所以导致前端报了 ERR_INCOMPLETE_CHUNKED_ENCODING 的异常。而这个创建 session 是项目的 拦截器调用的,代码如下:
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object arg2, ModelAndView view) throws Exception {
Conf CONF = (Conf) request.getSession().getServletContext().getAttribute("APP_CONF");
if (CONF == null) {
CONF = confService.findConf();
request.getSession().getServletContext().setAttribute("APP_CONF", CONF);
}
}
在 request.getSession() 需要创建 session 对象,上面的异常这么清晰,一开始是没有的,而是在项目中把Tomcat 8的源代码引入项目,然后找到抛出异常的代码。代码如下:org.apache.catalina.connector.Request.doGetSession 中部分代码:
if (!create) {
return (null);
}
if (response != null
&& context.getServletContext()
.getEffectiveSessionTrackingModes()
.contains(SessionTrackingMode.COOKIE)
&& response.getResponse().isCommitted()) {
throw new IllegalStateException(
sm.getString("coyoteRequest.sessionCreateCommitted"));
}
找到了异常,这里涉及到,Spring MVC 框架的 HandlerInterceptor 拦截器的接口处理过程,显然 preHandle()方法是在Handle之前处理的,如果要是在这里调用 request.getSession() 应该是可以的,结果的确是这样处理的,当然也可以将写在 postHandle()的移动到 preHandle()中,我不清楚原来同仁为何这么写。
所以在 preHandle()中 加如下的代码:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object obj) throws Exception {
response.addHeader("Access-Control-Allow-Origin","*");
HttpSession httpSession = request.getSession();
return true;
}
这样可以避免在 postHandle()中去创建 session 。BUG到这里已经处理成功了。
3. BUG原理与总结:
这种BUG其实不容易发生,只有在请求的过程中 HandlerInterceptor postHandle()之前一直没有创建 session 时才发生。BUG原理其实 Spring MVC 请求做异步处理引起的。
还有就是处理这种不常见的BUG主要还是需要源码 DEBUG 进去看每一变量的情况,
还有就是用比较方法去排除怀疑,比如让前端去请求一个原来可以正常使用的API接口,如果成功了,说明是现在的后端服务接口有BUG了。