再遇org.apache.catalina.connector.ClientAbortException: java.net.SocketException: 断开的管道 (Write failed)

再遇org.apache.catalina.connector.ClientAbortException: java.net.SocketException: 断开的管道 (Write failed)

优付商户平台“付款记录”页面,商户操作员点击“下载结算凭证”按钮,系统会将所选条件的交易的回单文件以zip包的形式返回给浏览器页面。

 

 

 

由于程序涉及到复杂计算,同时涉及到读库、网络、磁盘IO,耗时比较长。为了防止重复请求,今天,我用redis分布式锁做了防重复提交控制。

@RequestMapping(value = "/downLoadBill")
public void downLoadBill(HttpServletRequest request, HttpServletResponse response) throws Exception {
    UserVO userVO=(UserVO) request.getSession().getAttribute("userVO");
    log.info("==MERCHANT==结算凭证下载,执行开始==企业id={}", userVO.getMERID());
    response.setCharacterEncoding(Constant.CHARSET);

    String lockKey="downLoadBill:"+userVO.getMERID();
    String lockValue = UUID.randomUUID().toString();
    boolean getLock = JedisUtils.tryGetDistributedLock(lockKey, lockValue,15000);
    if (!getLock) {
        log.info("结算凭证下载中,请勿重复提交");
        response.getWriter().write("您似乎进行了重复提交操作。请重新发起请求,因数据量大,希望您耐心等待系统响应!");
        return;
    }
    
    ....
    OutputStream responseStream = new BufferedOutputStream(response.getOutputStream());
    response.setContentType("application/octet-stream");
    response.setHeader("Content-Disposition", "attachment;filename=" + new String(file.getName().getBytes("UTF-8"),"ISO-8859-1"));
    responseStream.write(buffer);
    responseStream.flush();
    
    ....
}    

 

那么, 当商户操作人员在页面重复点击时,页面交互如下:

 

 

页面交互倒是OK,不过呢,通过监控运行日志,发现程序有报异常:org.apache.catalina.connector.ClientAbortException: java.net.SocketException: 断开的管道 (Write failed)
同样,查看浏览器的网络请求,也发现,重复点击调用了两次接口,不过,第一次的直接爆红,第二次的正常响应。

如下是往response写入字节流的代码

    try (BufferedInputStream fis = new BufferedInputStream(new FileInputStream(file.getPath()));
         OutputStream responseStream = new BufferedOutputStream(response.getOutputStream())) {
        // 以流的形式下载文件。
        byte[] buffer = new byte[fis.available()];
        fis.read(buffer);
        fis.close();
        // 清空response
        response.reset();

        response.setContentType("application/octet-stream");
        response.setHeader("Content-Disposition", "attachment;filename=" + new String(file.getName().getBytes("UTF-8"), "ISO-8859-1"));
        responseStream.write(buffer);
        responseStream.flush();
        responseStream.close();
    } catch (IOException ex) {
        ex.printStackTrace();
    }

 

如下是两次请求的log:

2021-06-23 18:05:46.966 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2125] ==MERCHANT==结算凭证下载,执行开始==企业id=89900000222116027420
2021-06-23 18:05:47.001(Timestamp), com.yft.service.impl.TPlatOrderServiceImpl(String), selectPlatOrderPage(String), selectPlatOrderPage(String), 192.168.40.69(String)
2021-06-23 18:05:47.008 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2199] ====MERCHANT==结算凭证下载,要下载的结算凭证共有{}==19
2021-06-23 18:05:47.013 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2202] ====MERCHANT==结算凭证下载,定义临时文件夹==/home/zipTempPath/89900000222116027420/20210623/
2021-06-23 18:05:47.014 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2208] ====MERCHANT==结算凭证下载,循环处理订单platOrder.orderid:2021061014474300562655,获取结算凭证==/home/zipTempPath/89900000222116027420/20210623/20210610_2021061014474300562655.pdf
2021-06-23 18:05:47.043 [ INFO] [downLoadBill_1624442477021S655] [com.yft.controller.SettleController:2125] ==MERCHANT==结算凭证下载,执行开始==企业id=89900000222116027420
2021-06-23 18:05:47.044 [ INFO] [downLoadBill_1624442477021S655] [com.yft.controller.SettleController:2132] 结算凭证下载中,请勿重复提交
2021-06-23 18:05:47.109 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2208] ====MERCHANT==结算凭证下载,循环处理订单platOrder.orderid:2021061014474300562654,获取结算凭证==/home/zipTempPath/89900000222116027420/20210623/20210610_2021061014474300562654.pdf
2021-06-23 18:05:47.117 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2208] ====MERCHANT==结算凭证下载,循环处理订单platOrder.orderid:2021061014093100562651,获取结算凭证==/home/zipTempPath/89900000222116027420/20210623/20210610_2021061014093100562651.pdf
2021-06-23 18:05:47.124 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2208] 
====MERCHANT==结算凭证下载,循环处理订单platOrder.orderid:2021060718400100562574,获取结算凭证==/home/zipTempPath/89900000222116027420/20210623/20210607_2021060718400100562574.pdf
....
2021-06-23 18:05:47.649 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2208] ====MERCHANT==结算凭证下载,循环处理订单platOrder.orderid:2021060718380400562573,获取结算凭证==/home/zipTempPath/89900000222116027420/20210623/20210607_2021060718380400562573.pdf
2021-06-23 18:05:47.656 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2225] ====MERCHANT==结算凭证下载,定义zip压缩文件路径==20210623180547.zip
org.apache.catalina.connector.ClientAbortException: java.net.SocketException: 断开的管道 (Write failed)
    at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:410)
    at org.apache.tomcat.util.buf.ByteChunk.append(ByteChunk.java:352)
    at org.apache.catalina.connector.OutputBuffer.writeBytes(OutputBuffer.java:435)
    at org.apache.catalina.connector.OutputBuffer.write(OutputBuffer.java:423)
    at org.apache.catalina.connector.CoyoteOutputStream.write(CoyoteOutputStream.java:91)
    at org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper$SaveContextServletOutputStream.write(SaveContextOnUpdateOrErrorResponseWrapper.java:457)
    at java.io.BufferedOutputStream.write(BufferedOutputStream.java:122)
    at java.io.FilterOutputStream.write(FilterOutputStream.java:97)
    at com.yft.util.DownloadFileUtil.downloadFile(DownloadFileUtil.java:99)
    at com.yft.controller.SettleController.downLoadBill(SettleController.java:2236)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    。。。
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:318)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:748)
    Suppressed: org.apache.catalina.connector.ClientAbortException: java.net.SocketException: 断开的管道 (Write failed)
        at org.apache.catalina.connector.OutputBuffer.doFlush(OutputBuffer.java:370)
        at org.apache.catalina.connector.OutputBuffer.flush(OutputBuffer.java:334)
        at org.apache.catalina.connector.CoyoteOutputStream.flush(CoyoteOutputStream.java:101)
        at org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper$SaveContextServletOutputStream.flush(SaveContextOnUpdateOrErrorResponseWrapper.java:376)
        at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:141)
        at java.io.FilterOutputStream.close(FilterOutputStream.java:158)
        at com.yft.util.DownloadFileUtil.downloadFile(DownloadFileUtil.java:105)
        ... 77 more
    Caused by: java.net.SocketException: 断开的管道 (Write failed)
        at java.net.SocketOutputStream.socketWrite0(Native Method)
        at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)
        at java.net.SocketOutputStream.write(SocketOutputStream.java:155)
        at org.apache.coyote.http11.InternalOutputBuffer.realWriteBytes(InternalOutputBuffer.java:216)
        at org.apache.tomcat.util.buf.ByteChunk.flushBuffer(ByteChunk.java:442)
        at org.apache.coyote.http11.InternalOutputBuffer.flush(InternalOutputBuffer.java:120)
        at org.apache.coyote.http11.AbstractHttp11Processor.action(AbstractHttp11Processor.java:849)
        at org.apache.coyote.Response.action(Response.java:171)
        at org.apache.catalina.connector.OutputBuffer.doFlush(OutputBuffer.java:366)
        ... 83 more
Caused by: java.net.SocketException: 断开的管道 (Write failed)
    at java.net.SocketOutputStream.socketWrite0(Native Method)
    at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)
    at java.net.SocketOutputStream.write(SocketOutputStream.java:155)
    at org.apache.coyote.http11.InternalOutputBuffer.realWriteBytes(InternalOutputBuffer.java:216)
    at org.apache.tomcat.util.buf.ByteChunk.flushBuffer(ByteChunk.java:442)
    at org.apache.tomcat.util.buf.ByteChunk.append(ByteChunk.java:347)
    at org.apache.coyote.http11.InternalOutputBuffer$OutputStreamOutputBuffer.doWrite(InternalOutputBuffer.java:239)
    at org.apache.coyote.http11.filters.ChunkedOutputFilter.doWrite(ChunkedOutputFilter.java:119)
    at org.apache.coyote.http11.AbstractOutputBuffer.doWrite(AbstractOutputBuffer.java:192)
    at org.apache.coyote.Response.doWrite(Response.java:495)
    at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:405)
    ... 85 more
2021-06-23 18:05:48.194 [ INFO] [downLoadBill_1624442477031S903] [com.yft.qrcodeUtil.FileOperateUtil:54] 执行linux命令删除目录,linux cmd=/bin/rm -rf /home/zipTempPath/89900000222116027420/20210623/

 

 

如下是谷歌浏览器F12调试器窗口的网络请求截图:

 

 

 

 

 

 

那么,为什么会出现“ClientAbortException: java.net.SocketException: 断开的管道 (Write failed)”异常呢?

原因是:浏览器重复提交时,由于是同步请求,当第二次的请求到达时,浏览器已经关闭了第一次的请求。而此时呢,server端对第一次请求的处理尚未结束(线程仍处于RUNNABLE状态),等到往响应流里写数据时,由于客户端连接已断开,所以出现“断开的管道 (Write failed)”异常,因为是响应异常,故而异常类型是SocketException。

如下图示: