某天,将线上的resin容器替换为tomcat.过了一段时间发现有个接口处理失败,提示异常.查看应用日志发现如下的日志:
Caused by: javax.servlet.ServletException: java.lang.IllegalStateException: Cannot create a session after the response has been committed
at org.apache.jsp.WEB_002dINF.content.order.page.error_jsp._jspService(error_jsp.java:293) ~[na:na]
at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70) ~[jasper.jar:8.5.12]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) ~[servlet-api.jar:na]
at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:443) ~[jasper.jar:8.5.12]
... 38 common frames omitted
查询相关接口的代码发现,代码对 302
跳转的逻辑处理有问题,具体如下:
@Actions(value = {
@Action(value = "dopay", results = {@Result(name = ERROR, location = "/WEB-INF/content/order/page/error.jsp")}),
})
@ActionMonitor(value = "pay.doPay")
public String doPay() {
// 省略代码
String _result = dealWapClient(params);
// 问题之所在, 当dealWapClient处理成功时,返回值就是null
// 此时,返回ERROR, Struts2会继续执行,渲染错误页面(客户端就能看到错误页面了)
// tomcat 能看到, resin下看不到,原因下面分析
if (_result == null) {
return ERROR;
}
// 省略代码
}
public String dealWapClient(Map<String, String> params) {
// 省略代码
redirect(returnParams, returnUrl);
return null;
// 省略代码
}
}
关于302临时跳转的详细解释可以参考 HTTP_302 .
也可以参考RFC规范 http://www.ietf.org/rfc/rfc3986.txt
再次就不再赘述.
/**
* Sends a temporary redirect response to the client using the
* specified redirect location URL and clears the buffer. The buffer will
* be replaced with the data set by this method. Calling this method sets the
* status code to {@link #SC_FOUND} 302 (Found).
* This method can accept relative URLs;the servlet container must convert
* the relative URL to an absolute URL
* before sending the response to the client. If the location is relative
* without a leading '/' the container interprets it as relative to
* the current request URI. If the location is relative with a leading
* '/' the container interprets it as relative to the servlet container root.
* If the location is relative with two leading '/' the container interprets
* it as a network-path reference (see
* <a href="http://www.ietf.org/rfc/rfc3986.txt">
* RFC 3986: Uniform Resource Identifier (URI): Generic Syntax</a>, section 4.2
* "Relative Reference").
*
* <p>If the response has already been committed, this method throws
* an IllegalStateException.
* After using this method, the response should be considered
* to be committed and should not be written to.
*
* @param location the redirect location URL
* @exception IOException If an input or output exception occurs
* @exception IllegalStateException If the response was committed or
* if a partial URL is given and cannot be converted into a valid URL
*/
public void sendRedirect(String location) throws IOException;
翻译过来意思就是: 通过该方法告诉客户端临时重定向到一个指定的URL,并且清空缓存区,之前还没有发送到客户端的数据.
并使用该方法设置的数据填充缓存区.
该方法设置http响应的状态码为302.
如果重定向的地址为相对地址,该方法内部会将相对地址转为绝对地址.
如果response已经committed,再次调用该方法会抛出 IllegalStateException
异常.
调用该方法后,response对象的状态应该是 committed
,并且不应该再写入数据.
servlet-api已经详细说明了该方法的用法和需要注意的事项.但是不同的servlet容器在实现机制上可能不尽相同.
项目中发现的问题主要有两个原因:
下面就分析下该方法在resin和tomcat中实现的细节:
在resin中, HttpServletResponse
接口的实现类是 HttpServletResponseImpl
.代码如下:
abstract public class AbstractCauchoResponse implements CauchoResponse {
}
public interface CauchoResponse extends HttpServletResponse {
}
public final class HttpServletResponseImpl extends AbstractCauchoResponse
implements CauchoResponse
{
/**
* Sends a redirect to the browser. If the URL is relative, it gets
* combined with the current url.
*
* @param url the possibly relative url to send to the browser
*/
@Override
public void sendRedirect(String url)
throws IOException
{
if (url == null)
throw new NullPointerException();
if (isCommitted())
throw new IllegalStateException(L.l("Can't sendRedirect() after data has committed to the client."));
_responseStream.clearBuffer();
// server/10c4
// reset();
resetBuffer();
setStatus(SC_MOVED_TEMPORARILY);
String encoding = getCharacterEncoding();
boolean isLatin1 = "iso-8859-1".equals(encoding);
String path = encodeAbsoluteRedirect(url);
setHeader("Location", path);
if (isLatin1)
setHeader("Content-Type", "text/html; charset=iso-8859-1");
else
setHeader("Content-Type", "text/html; charset=utf-8");
String msg = "The URL has moved <a href=/"" + path + "/">here</a>";
// The data is required for some WAP devices that can't handle an
// empty response.
if (_writer != null) {
_writer.println(msg);
}
else {
ServletOutputStream out = getOutputStream();
out.println(msg);
}
// closeConnection();
_request.saveSession(); // #503
// 非常重要,这个就是resion和tomcat的不同之处.
// 已经关闭了,肯定不能再写入数据.
close();
}
@Override
public void close()
throws IOException
{
// tck - jsp include
AbstractHttpResponse response = _response;
if (response != null) {
response.close();
}
}
}
resin处理`302`方式其实非常简单,步骤如下:
1. 清空缓存区内容并进行重置
2. 设置302状态码
3. 设置`Location` 和 `Content-Type` 响应头
4. 写响应体数据
5. 保存session
6. 关闭连接
整个处理流程非常简单明了.
### tomcat对302的处理
在tomcat中,`HttpServletResponse`接口的实现类是`ResponseFacade`.该类指示一个Facade,
代码如下:
```java ResponseFacade
public class ResponseFacade implements HttpServletResponse {
@Override
public void sendRedirect(String location)
throws IOException {
if (isCommitted()) {
throw new IllegalStateException
(sm.getString("coyoteResponse.sendRedirect.ise"));
}
response.setAppCommitted(true);
response.sendRedirect(location);
}
}
真正的处理由 Response
来进行
public class Response implements HttpServletResponse {
/**
* Send a temporary redirect to the specified redirect location URL.
*
* @param location Location URL to redirect to
*
* @exception IllegalStateException if this response has
* already been committed
* @exception IOException if an input/output error occurs
*/
@Override
public void sendRedirect(String location) throws IOException {
sendRedirect(location, SC_FOUND);
}
/**
* Internal method that allows a redirect to be sent with a status other
* than {@link HttpServletResponse#SC_FOUND} (302). No attempt is made to
* validate the status code.
*
* @param location Location URL to redirect to
* @param status HTTP status code that will be sent
* @throws IOException an IO exception occurred
*/
public void sendRedirect(String location, int status) throws IOException {
if (isCommitted()) {
throw new IllegalStateException(sm.getString("coyoteResponse.sendRedirect.ise"));
}
// Ignore any call from an included servlet
if (included) {
return;
}
// 清空缓存区内容并进行重置
resetBuffer(true);
// Generate a temporary redirect to the specified location
try {
String locationUri;
// Relative redirects require HTTP/1.1
if (getRequest().getCoyoteRequest().getSupportsRelativeRedirects() &&
getContext().getUseRelativeRedirects()) {
locationUri = location;
} else {
locationUri = toAbsolute(location);
}
setStatus(status);
setHeader("Location", locationUri);
// 这里有个小魔法
if (getContext().getSendRedirectBody()) {
PrintWriter writer = getWriter();
writer.print(sm.getString("coyoteResponse.sendRedirect.note",
Escape.htmlElementContent(locationUri)));
flushBuffer();
}
} catch (IllegalArgumentException e) {
log.warn(sm.getString("response.sendRedirectFail", location), e);
setStatus(SC_NOT_FOUND);
}
// 设置缓存区的suspended标志位
// 从应用视图的角度看,该响应已经结束了. 但其实连接并没有关闭.
setSuspended(true);
}
}
tomcat处理 302
步骤如下:
Location
详细配置项参考: https://tomcat.apache.org/tomcat-7.0-doc/config/context.html
其中和302处理相关的一个配置项为: sendRedirectBody
,文档解释如下:
If true, redirect responses will include a short response body that includes details of the redirect as recommended by RFC 2616. This is disabled by default since including a response body may cause problems for some application component such as compression filters.
根据 RFC 2616 规范,302跳转是可以带有响应体数据的(resin就按规范进行了实现).tomcat默认处理是不带的,原因是可能与其它组件冲突,例如压缩组件.
如果将 sendRedirectBody
的值设为true,则tomcat在处理302时,在写完响应体数据后,会执行缓存区的刷新,客户端能收到对应的响应头数据,完成跳转,且不会应为后续继续写数据导致客户端不能正常跳转.
因为默认是false,导致302响应头数据没有及时发送给客户端,在 sendRedirect
后如果应用发生了异常,则已经设置了的302响应码会被500所替代,客户端不能正常跳转.
###tomcat sendRedirect
后不能跳转的逻辑分析
tomcat处理请求的流程一部分流程如下:
StandardHostValve
-> StandardContextValve
-> StandardWrapperValve
请求入口由 StandardWrapperValve
处理,结束还是需要 StandardHostValve
来处理.
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Allocate a servlet instance to process this request
try {
if (!unavailable) {
servlet = wrapper.allocate();
}
} catch (UnavailableException e) {
} catch (ServletException e) {
exception(request, response, e);
} catch (Throwable e) {
exception(request, response, e);
servlet = null;
}
// 当请求发送异常时, 已经设置的302状态码此时变为500
private void exception(Request request, Response response,
Throwable exception) {
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, exception);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setError();
}
}
StandardHostValve处理逻辑
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
try {
// 省略代码
try {
if (!asyncAtStart || asyncDispatching) {
context.getPipeline().getFirst().invoke(request, response);
} else {
// Make sure this request/response is here because an error
// report is required.
if (!response.isErrorReportRequired()) {
throw new IllegalStateException(sm.getString("standardHost.asyncStateError"));
}
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
container.getLogger().error("Exception Processing " + request.getRequestURI(), t);
// If a new error occurred while trying to report a previous
// error allow the original error to be reported.
if (!response.isErrorReportRequired()) {
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
throwable(request, response, t);
}
}
// 在sendRedirect方法设置的suspended标志位此时又被置为false
// 也就是说 response由可以使用了.这就是resin和tomcat设计实现的不同
// Now that the request/response pair is back under container
// control lift the suspension so that the error handling can
// complete and/or the container can flush any remaining data
response.setSuspended(false);
Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
// Protect against NPEs if the context was destroyed during a
// long running request.
if (!context.getState().isAvailable()) {
return;
}
// Look for (and render if found) an application level error page
if (response.isErrorReportRequired()) {
if (t != null) {
throwable(request, response, t);
} else {
status(request, response);
}
}
if (!request.isAsync() && !asyncAtStart) {
context.fireRequestDestroyEvent(request.getRequest());
}
} finally {
// Access a session (if present) to update last accessed time, based
// on a strict interpretation of the specification
if (ACCESS_SESSION) {
request.getSession(false);
}
context.unbind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
}
}
https://zh.wikipedia.org/wiki/HTTP_302
https://tools.ietf.org/html/rfc2616#section-10.3.3