今天同事说加入了之前的一个验证模块的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:很多都是自己的理解,如有不对 ,多谢指出。。