遇到一个非常有意思的异常.
在使用tomcat(jetty也一样)时,如果client请求,server已经收到请求后,但是client突然关闭了请求,会出现无线循环的抛异常.
Resolved exception caused by Handler execution: org.apache.catalina.connector.ClientAbortException: java.io.IOException: Broken pipe
这种异常会导致栈溢出.但是实际上并不影响服务的正常运行,也就是说如果你不看日志的话,几乎感知不到这种异常.所以这种异常的最大问题就是导致日志疯长.当然这取决于你服务器对栈深的规定,等到栈溢出了,异常就停止了.因为抛出栈溢出了.
下面的程序通过调整执行次数和sleep时间,可以重现这种异常.
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
sendRequest();
}
}
public static String sendRequest() {
CloseableHttpClient httpCilent = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://127.0.0.1:8080");
new Thread(() -> {
try {
CloseableHttpResponse resp = httpCilent.execute(httpGet);
System.out.println(resp.getEntity().toString());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
httpCilent.close();// 释放资源
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
try {
//设定sleep,来更好的实现close的时机
Thread.sleep(10);
httpCilent.close();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
return null;
}
至于说为啥不影响服务的运行,我认为不管是tomcat还是spring,对于每一个新的请求都是开辟一个新的thread来处理,所以每个请求之间相互不干扰.
现在说说这个错误出现的原因.
先看看clientAbortException都在哪里调用了(我用的是springboot),可以用你的IDE查看下:
org.apache.catalina.connector.ClientAbortException.ClientAbortException(Throwable)
org.apache.catalina.connector.OutputBuffer.doFlush(boolean)
org.apache.catalina.connector.OutputBuffer.realWriteBytes(ByteBuffer)
这里显示有俩处调用.经过调试发现实际错误发生在doFlush里面.
protected void doFlush(boolean realFlush) throws IOException {
if (suspended) {
return;
}
try {
doFlush = true;
if (initial) {
coyoteResponse.sendHeaders();
initial = false;
}
if (cb.remaining() > 0) {
flushCharBuffer();
}
if (bb.remaining() > 0) {
flushByteBuffer();
}
} finally {
doFlush = false;
}
if (realFlush) {
coyoteResponse.action(ActionCode.CLIENT_FLUSH, null);
// If some exception occurred earlier, or if some IOE occurred
// here, notify the servlet with an IOE
if (coyoteResponse.isExceptionPresent()) {
throw new ClientAbortException(coyoteResponse.getErrorException());
}
}
}
可以看到最后判断是否有异常,如果有,抛出clientAbortException.抛出异常后(是tomcat抛出的),spring就捕获了异常,然后spring的dispatch就会经过各种封装各种filter处理.
filterChain.doFilter(request, response);
目的就是向client输出错误信息,最后会调用:
org.springframework.util.StreamUtils里的copy方法.
public static int copy(InputStream in, OutputStream out) throws IOException {
Assert.notNull(in, "No InputStream specified");
Assert.notNull(out, "No OutputStream specified");
int byteCount = 0;
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead = -1;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
byteCount += bytesRead;
}
out.flush();
return byteCount;
}
看到没,最后调用了out.flush()后,又重新会调用到tomcat里面的doFlush方法,就又发现client丢失,又再次抛出异常.循环就此开始了.
那么如何避免这种错误了?
我的答案是避免不了.也可能是我的见识目前不知道咋解决.不过可以避免大量的日志输出,我们定义个filter,然后在filter里面捕获异常.
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
final HttpServletResponse res = (HttpServletResponse) response;
final HttpServletRequest req = (HttpServletRequest) request;
try {
// 请求转发
chain.doFilter(req, res);
} catch (Exception e) {
/*
* 异常拦截,server请求收到,但是client取消,导致死循环栈溢出
* Resolved exception caused by Handler execution:
* org.apache.catalina.connector.ClientAbortException:
* java.io.IOException: Broken pipe
* 但是这里捕获的其实是栈溢出错误.
*/
logger.warn("捕获到ClientAbortException");
}
}
这样我们就可以避免大量日志输出了.
这个异常还是蛮有意思的,有种你可以忽略她,但不可以忘记她的感觉.