今天同事说加入了之前的一个验证模块的filter,发现系统的所有图表都不显示了,后台还出现了一个异常

java.lang.IllegalStateException: getWriter() has already been called for this response

at org.apache.catalina.connector.Response.getOutputStream(Response.java:580)

at org.apache.catalina.connector.ResponseFacade.getOutputStream(ResponseFacade.java:183)

at org.apache.catalina.servlets.DefaultServlet.serveResource(DefaultServlet.java:860)

at org.apache.catalina.servlets.DefaultServlet.doGet(DefaultServlet.java:398)

at javax.servlet.http.HttpServlet.service(HttpServlet.java:617)

at javax.servlet.http.HttpServlet.service(HttpServlet.java:717)

这个问题从来没有遇到过,赶紧看下源码。

这是filter的部分代码

public void doFilter(ServletRequest request, ServletResponse response,

        FilterChain chain) throws IOException, ServletException {

    HttpServletRequest httpServletRequest = (HttpServletRequest) request;

    HttpServletResponse httpServletResponse = (HttpServletResponse) response;

    httpServletRequest.setCharacterEncoding("UTF-8");

    httpServletResponse.setCharacterEncoding("UTF-8");

    String contextPath = httpServletRequest.getContextPath();

    String requestURI = httpServletRequest.getRequestURI();

    PrintWriter out = response.getWriter();

    // 获取请求的路径

    String path = StringUtils.removeStart(requestURI, contextPath);

    logger.info("request:" + path);

    try {

        if (StringUtils.isNullOrEmpty(web_path)) {

            web_path = httpServletRequest.getRealPath("/");

        }

        String encrypt = request.getParameter("sid");

        String module_encrypt = SecurityUtils.enCryption("...");

        if (StringUtils.isNotNullOrEmpty(encrypt)

                && module_encrypt.equals(encrypt)) {

            // 如果是module服务请求

            if (RestUtils.contains(path)) {

                out.write(RestUtils.get(path));

            } else {

                chain.doFilter(request, response);

            }

        } else {

            logger.log(Level.WARNING, "Authentication is not passed ! ip:"

                    + ip);

            // 跳转到一个错误页面

            RequestDispatcher dispatcher = request

                    .getRequestDispatcher(err_page);

            dispatcher.forward(request, response);

        }

    } catch (Exception e) {

        logger.log(Level.WARNING, "check identity error !", e);

    } 

问题就出在这里

chain.doFilter(request, response);

定位错误代码,关联上tomcat的源代码,异常来自DefaultServlet的doGet中调用的serveResource

ServletOutputStream ostream = null;

    PrintWriter writer = null;

    if (content) {

        // Trying to retrieve the servlet output stream

        try {

            ostream = response.getOutputStream();

        } catch (IllegalStateException e) {

            // If it fails, we try to get a Writer instead if we're

            // trying to serve a text file

            if ( (contentType == null)

                    || (contentType.startsWith("text"))

                    || (contentType.endsWith("xml")) ) {

                writer = response.getWriter();

            } else {

                throw e;

            }

        }

    }

是 ostream = response.getOutputStream();这句出现了问题,然后抛出了异常,这是怎么回事呢。。我想起了之前在做一个统计监控工具时候发生的问题,当一个response的流被读取一次之后,流就没了。再次调用response.getOutputStream();的时候,就会为空或者出现异常,看下response处理的代码

public ServletOutputStream getOutputStream() 

    throws IOException {

    if (usingWriter)

        throw new IllegalStateException

            (sm.getString("coyoteResponse.getOutputStream.ise"));

    usingOutputStream = true;

    if (outputStream == null) {

        outputStream = new CoyoteOutputStream(outputBuffer);

    }

    return outputStream;

}

单步调试的时候发现,代码直接执行了throw new IllegalStateException也就是说usingWriter为true,默认是为false的。到这里已经很清楚了。再回头看下filter的代码

httpServletRequest.setCharacterEncoding("UTF-8");

    httpServletResponse.setCharacterEncoding("UTF-8");

    String contextPath = httpServletRequest.getContextPath();

    String requestURI = httpServletRequest.getRequestURI();

    PrintWriter out = response.getWriter();

无论什么请求过来,都先获取了这个请求response的writer,看下getWriter方法,同样是Response类

public PrintWriter getWriter() 

    throws IOException {

    if (usingOutputStream)

        throw new IllegalStateException

            (sm.getString("coyoteResponse.getWriter.ise"));

    if (Globals.STRICT_SERVLET_COMPLIANCE) {

        setCharacterEncoding(getCharacterEncoding());

    }

    usingWriter = true;

    outputBuffer.checkConverter();

    if (writer == null) {

        writer = new CoyoteWriter(outputBuffer);

    }

    return writer;

在这里我们发现,usingWriter = true;设置了已经读取过了。那么下面的getOutputStream自然就会出现异常,异常发生的原因就是对一个response中的流进行了多次读取。既然这样。那我们改成什么时候用。什么时候获取就好了。不能每一个请求过来都先读取一边,那样就太有问题了。

这个问题解决了,还有一个问题,为啥图片不显示,js和css就没问题呢。。我们先请求一个js文件,然后跟踪代码到DefaultServlet的doGet中调用的serveResource方法,代码比较长,只贴关键代码

// Find content type.

    String contentType = cacheEntry.attributes.getMimeType();

    if (contentType == null) {

        contentType = getServletContext().getMimeType(cacheEntry.name);

        cacheEntry.attributes.setMimeType(contentType);

    }

执行到这里,contentType=“text/javascript”,这个就是关键,接下来还是出现异常的地方

ServletOutputStream ostream = null;

    PrintWriter writer = null;

    if (content) {

        // Trying to retrieve the servlet output stream

        try {

            ostream = response.getOutputStream();

        } catch (IllegalStateException e) {

            // If it fails, we try to get a Writer instead if we're

            // trying to serve a text file

            if ( (contentType == null)

                    || (contentType.startsWith("text"))

                    || (contentType.endsWith("xml")) ) {

                writer = response.getWriter();

            } else {

                throw e;

            }

        }

    }

关键在于13行开始,这里做了一个判断,如果为空或者以text开头,或者以xml结尾的,直接获取Writer,而getWriter中是否可以重复读取的参数是usingOutputStream,也就是writer中的流是否已被读取,查看filter的代码发现没有,那么这里就直接返回writer了,所以js和css等contentType以text开头的都没有问题,而图片的contentType是image/png,所以直接抛出异常。

ps:很多都是自己的理解,如有不对 ,多谢指出。。

Comments
Write a Comment