servlet如何正确处理302跳转

栏目: 服务器 · 发布时间: 7年前

内容简介:某天,将线上的resin容器替换为tomcat.过了一段时间发现有个接口处理失败,提示异常.查看应用日志发现如下的日志:查询相关接口的代码发现,代码对关于302临时跳转的详细解释可以参考

某天,将线上的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跳转解释

关于302临时跳转的详细解释可以参考 HTTP_302 .

也可以参考RFC规范 http://www.ietf.org/rfc/rfc3986.txt

再次就不再赘述.

servletapi对302处理的规定

/**
* 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容器在实现机制上可能不尽相同.

项目中发现的问题主要有两个原因:

  1. 代码有bug.这个是主要原因.
  2. servlet容器实现不同.

下面就分析下该方法在resin和tomcat中实现的细节:

resin对302的处理

在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

tomcat context配置

详细配置项参考: 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 来处理.

StandardWrapperValve
@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处理逻辑

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);
    }        
}

总结

  1. 线上替换应用组件,尤其是底层的应用软件需要非常注意.不可全量替换.一定要逐步替换.虽然已经在测试环境,线上灰度替换了一台机器,
    但是因为访问量小,导致问题发现的比较晚,直至大批量替换才发现问题.还有就是:同一个规范,但不同的实现细节还是有差异.
  2. 应用开发一定需要遵守API规范,否则会导致奇怪问题的发生.好多人总是说遇到的问题多,踩坑多,那是因为你从来不仔细阅读相关api规范文档或官方文档.不遵守规范导致的问题能叫坑吗?
  3. 在使用任何一项技术时,优先查询官方文档.不要随手google或baidu.实话说,网上的文章质量参差不齐,难免找到理解错误的文档.
  4. 养成阅读源代码的习惯,好处不言而喻.如果3年前没有读过tomcat6的源代码,今天排查起来问题就非常困难了.

参考资料


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Spring Into HTML and CSS

Spring Into HTML and CSS

Molly E. Holzschlag / Addison-Wesley Professional / 2005-5-2 / USD 34.99

The fastest route to true HTML/CSS mastery! Need to build a web site? Or update one? Or just create some effective new web content? Maybe you just need to update your skills, do the job better. Welco......一起来看看 《Spring Into HTML and CSS》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

html转js在线工具
html转js在线工具

html转js在线工具