Cannot call sendError() after the response has been committed问题产生原因之一和当前解决方案

问题背景

后台生成文件并返回zip供前端下载,直接用响应流构造zip输出流,代码如下

@Override
    public void downloadCode(HttpServletResponse response, List<Long> tableIds) {
        boolean failure = false;
        try (ZipOutputStream zip = new ZipOutputStream(response.getOutputStream())) {
            response.setHeader("Content-Disposition", "attachment; filename=\"generation.zip\"");
            response.setContentType("application/octet-stream; charset=UTF-8");
            for (Long tableId : tableIds) {
                generateCode(tableId, zip);
            }
        } catch (Exception e) {
            log.error("代码生成出错", e);
            failure = true;
        } finally {
            if (failure) {
                try {
                    response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                    response.sendError(500, "代码生成出错,无法下载");
                } catch (IOException ex) {
                    log.error("写报错信息出错", ex);
                }
            }
        }
    }
private void generateCode(Long tableId, ZipOutputStream zip) {
        // 查询表信息
        GenTable table = tableMapper.queryTableAndColumns(tableId, null, null);
        // 设置父表信息
        setSubTable(table);
        // 设置主键列信息
        setPkColumn(table);

        VelocityUtil.initVelocity();

        VelocityContext context = VelocityUtil.prepareContext(table);

        // 获取模板列表
        List<String> templates = VelocityUtil.getTemplateList(table.getTplCategory());
        for (String template : templates) {
            try {
                // 渲染模板
                StringWriter sw = new StringWriter();
                Template tpl = Velocity.getTemplate(template, CharSet.UTF_8);
                tpl.merge(context, sw);
                // 添加到zip
                zip.putNextEntry(new ZipEntry(VelocityUtil.getFileName(template, table)));
                zip.write(sw.toString().getBytes(StandardCharsets.UTF_8));
                sw.close();
                zip.flush();
                zip.closeEntry();
            } catch (IOException e) {
                log.error("写zip失败,表名:" + table.getTableName(), e);
                throw new UziException("代码生成失败!", 500);
            } catch (RuntimeException e) {
                log.error("渲染模板失败", e);
                throw new UziException("代码生成失败!", 500);
            }
        }
    }

调试时,忘记导入模板,导致代码报错,但是前端没有任何提示(已经做了全局统一异常响应处理)

原因分析

1、首先是找报错点(偏业务),获取不到模板
在这里插入图片描述
最终异常抛出点
2、正常debug,发现也进到了捕获异常处理的地方(上面106行报错,被118行捕获到),但是前端此时已经收到响应,200状态,但是是空文件。那就奇怪了,我已经断点住了,为啥请求结束了
在这里插入图片描述
3、这个现象很奇怪,但也很明显,就是一旦抛出异常,输出流自动关闭,请求结束(即servlet生命周期结束),再结合servlet的输出流不需要手动关闭,这种原因的可能性很大。那么下面就是来证实我的猜测(结果猜测正确,过程放到问题解决部分)
4、既然是servlet出的问题,那么就来调试看下servlet的源码,看下整个请求过程
servlet是继承的GenericServlet,有个很重要的方法service ()(GenericServlet有两个生命周期函数init()和destroy()这里不展开说了,只是init()方法仅在服务器装载Servlet时才由服务器执行一次,而每次客户向服务器发请求时,服务器就会调用Service()方法),进到service方法后进行调试
在这里插入图片描述
调用FrameworkServlet的processRequest方法
在这里插入图片描述
调用DispatcherServlet的doDispatch方法
在这里插入图片描述
此时终于找到了我们的目标controller,继续走
在这里插入图片描述
进到我自己异常捕获逻辑(前端没有响应)
在这里插入图片描述

再进到外层的异常捕获逻辑,貌似好像没有问题,但是此时却发现前端响应已经返回了,仔细观察后发现,response.isCommitted()此时由false变成了true(后面在我们调用sendError时,因为判断出response.isCommitted()是true时,会抛出我们开始出现的异常)。那么重新来一遍,看看这个值是如何被设置成true的
重新请求后,在进入我的最外层的异常捕获之前,先进入了CoyoteOutputStream类,该类继承ServletOutputStream,重写了ServletOutputStream继承自OutputStream的close方法,最终调用了OutputBuffer的doFlush方法
在这里插入图片描述
在这里插入图片描述
最终response设置committed为true,到此为止,为什么报Cannot call sendError() after the response has been committed的原因找到了,那么为什么自动先响应了呢?继续
随后OutputBuffer关闭,并触发Response的action方法
在这里插入图片描述
在这里插入图片描述
最终调用了finishResponse方法,里面关闭了socket
后面再补充具体是怎么结束请求的,当前先搞清楚是怎么触发调用OutputStream的close方法的?待叙。。。

解决方案

问题很明显,就是servlet自动关闭流导致请求结束造成的,那么最简单的解决方案就是先准备好数据,最后再往响应流里面写,那么异常导致的请求关闭只可能是写时的IO异常,业务的异常就能正常提示到前端了,代码如下:

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ZipOutputStream zip = new ZipOutputStream(outputStream);
        try {
            response.setHeader("Content-Disposition", "attachment; filename=\"generation.zip\"");
            response.setContentType("application/octet-stream; charset=UTF-8");
            for (Long tableId : tableIds) {
                generateCode(tableId, zip);
            }
            zip.close(); // 当不用响应流创建zip时,需要手动关闭zip流(响应流会自动关闭)
            response.getOutputStream().write(outputStream.toByteArray());
        } catch (Exception e) {
            log.error("代码生成出错", e);
            try {
                response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                response.sendError(500, "代码生成出错,无法下载");
            } catch (IOException ex) {
                log.error("响应报错信息出错", e);
            }
        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                log.error("最终关闭流失败!", e);
            }
        }

如果有更好的解决方案,请多指教。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr Yin

您获益,我得意,您打赏,我敬礼

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值