最近在搞大数据量的Excel文件的导出功能。利用数据库游标查询加上poi的sxssf技术实现数据库数据的平稳转换为流返回给前端。前端通过fetch将流一点点存到内存中,利用a标签下载下来。但是在开发过程中遇到了一个特殊的异常处理问题。困扰了很久,特记录一下。如果有遇到相同问题的同学可参考一下。

不罗嗦直接代码

1.controller层

    @PostMapping("/user/getPage")
    ResponseEntity<StreamingResponseBody> getPage(@RequestBody User user, HttpServletResponse response) throws FileNotFoundException {
        StreamingResponseBody streamingResponseBody = new StreamingResponseBody() {
            @Override
            public void writeTo(OutputStream outputStream) throws IOException {
                userService.queryStream(user,outputStream);
                outputStream.close();
        return  ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=FileName.xlsx")
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(streamingResponseBody);

2.service实现类

 @Override
    public void queryStream(User user, OutputStream outputStream) throws IOException{
        //防止读取数据量过大前端超时
        outputStream.write("=start=".getBytes());
        outputStream.flush();
        File file = new File("/poi/temp");
        if(!file.exists()){
            file.mkdirs();
        TempFile.setTempFileCreationStrategy(new DefaultTempFileCreationStrategy(file));
        SXSSFWorkbook sxssfWorkbook = new SXSSFWorkbook();
        SXSSFSheet sheet = sxssfWorkbook.createSheet();
        SXSSFRow row0 = sheet.createRow(0);
        String[] props = new String[]{"id","name","age","mail","tel"};
        for (int i = 0; i < 5; i++) {
            String pname  = props[i];
            SXSSFCell cell = row0.createCell(i);
            cell.setCellValue(pname);
        Long aLong = Long.valueOf(user.getAge());
        Map<String,Integer> map = new ConcurrentHashMap<>();
        //处理游标查询里异常
        map.put("isError", 0);
        userMapper.dynamicSelectLargeData(aLong, new ResultHandler<User>() {
            Integer size = 1;
            @Override
            public void handleResult(ResultContext<? extends User> resultContext) {
                try {
                    User userObj = resultContext.getResultObject();
                    SXSSFRow row = sheet.createRow(size);
                    for (int i = 0; i < 5; i++) {
                        SXSSFCell cell = row.createCell(i);
                        if(i == 0){
                            cell.setCellValue(userObj.getId());
                        }else if (i ==1){
                            cell.setCellValue(userObj.getName());
                        }else if (i ==2){
                            cell.setCellValue(userObj.getAge());
                        }else if (i ==3){
                            cell.setCellValue(userObj.getMail());
                        }else if (i ==4){
                            cell.setCellValue(userObj.getTel());
                    if (size % 100 == 0){
                        outputStream.write(1);
                        outputStream.flush();
//                        boolean committed = response.isCommitted();
//                        System.out.println("reponse是否已关闭:" + committed);
                    size++;
                    System.out.println("+++++++++++++当前条数:" + size + "+++++++++++++++++++");
                    map.put("size", size);
                }catch (Exception e){
                    System.out.println("异常退出流查询");
                    resultContext.stop();
                    e.printStackTrace();
                    map.put("isError", 1);
        try {
            if(map.get("isError") != 1){
                outputStream.write(("=end=;size=" + map.get("size")).getBytes());
                outputStream.flush();
                Thread.sleep(300);
                sxssfWorkbook.write(outputStream);
        } catch (IOException | InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("下载过程异常");
            e.printStackTrace();
            return;
        }finally {
            try {
                //删除sxssf产生的临时文件
                File[] files = file.listFiles();
                for (File fileTemp : files) {
                    fileTemp.delete();
                sxssfWorkbook.close();
            } catch (IOException e) {
                e.printStackTrace();

问题来了:

1.超时问题

       虽然StreamingResponseBody返回是分块返回,但游标查询是循环读取数据库里数据,如果数据量很大仍会导致请求在返回数据时超时。控制台会打印Async request timeout。从而导致请求数据写入到输出流异常,代码走到 outputstream.flush()这里会异常。

解决办法,通过实现 WebMvcConfigurer 类,配置异步请求的超时时间。

@Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        //-1 为不超时。建议根据项目自己设置毫秒值
        //springboot默认是30秒
        configurer.setDefaultTimeout(-1);

2.浏览器断开问题。

在下载过程中如果浏览器突然关闭,导致连接断开。代码走到 outputstream.flush()这里又会异常。更严重的问题是下次请求过来浏览器里查看直接报500。对服务器很不友好。(猜测原因springboot关闭了流,但没有通知servlet关闭本次请求)

解决办法 将 queryStream 办法里的outputstream.flush()导致的异常全部抛一个IOException给到StreamingResponseBody里重写writeto方法里。因为这个方法也会抛一个io异常出去。springboot在识别到io异常时会通知servlet关闭本次请求,响应。问题解决

有返回值:用户需要Callable、Futrue等来接收最终的处理结果,但这个过程是异步非阻塞的。 无返回值:用户提交请求到接口之后不需要任何返回值,请求到达服务端之后就没有任何关系了,用户可以不等待而去做其他的业务操作。 多线程调用方法 Callable:有返回值的线程方法,call 将会对用户请求做出结果反馈。 Runnable:线程的接口,... * @Description: 支持返回值为StreamingResponseBodyResponseEntity<StreamingResponseBody> public boolean supportsReturnType(MethodPar
StreamingResponseBody-处理Servlet异步I/O请求 StreamingResponseBodySpring 4.2版本添加的一个新的接口,在Controller里处理输出流时非常有用。 我们在java中创建I/O输入输出流时,一般用完流后都要关闭流,但是在Controller里面,处理Http request是异步的,这个时候如果往request里写入流
Spring MVC中每个控制器中可以定义多个请求处理方法,我们把这种请求处理方法简称为Action,每个请求处理方法可以有多个不同的参数,以及一个多种类型的返回结果。 一、Action参数类型 1.1、自动参数映射 1.1.1、基本数据类型 方法的参数可以是任意基本数据类型,如果方法参数名与http中请求的参数名称相同时会进行自动映射,视图user目录下的index.jsp与示例代码...
我们可以通过返回StreamingResponseBody来直接使用返回的OutputStream来自己控制数据返回。我们也可以使用ResponseEntity来定制状态和头的信息。 本例使用StreamingResponseBody返回一张图片,示例控制器如下: @RestController @RequestMapping("/async") @Slf4j public class AsyncController { @Value("classpath:wyn.jpg") //1 上篇博文:【小家Spring】高性能关键技术之—体验Spring MVC的异步模式(Callable、WebAsyncTask、DeferredResult) 基础使用篇 介绍了Spring MVC异步模式的基本使用,相信小伙伴们基本的使用都能运用自如了。 那么本篇文章主要介绍一下异步模式的高级使用(主要是DeferredResult)以及原理过程分析,废话不多说,进入正题 Deferred...
SpringMVC中,如要后台返回对象或json数据而不进行页面的跳转则需要在后台处理方法上添加@ResponseBody注释,若处理类中的所有方法都是返回数据而不进行页面的跳转处理则为处理类添加@RestController注解即可省去为每个处理方法添加@ResponseBody的麻烦,而没有@RequestMapping注解的普通方法不会受到影响。 后台处理类(由于该文主要是写前端与Spr
StreamingResponseBodySpring框架提供的一个接口,用于将响应的内容以流的形式返回给客户端。它可以用于处理大文件下载、实时数据推送等场景。 使用StreamingResponseBody时,我们需要实现它的writeTo方法,该方法接收一个OutputStream参数,我们可以将要返回的内容写入到该输出流中。Spring框架会自动将该输出流与响应的输出流关联起来,从而实现内容的流式传输。 下面是一个示例代码,演示了如何使用StreamingResponseBody返回一个文本文件的内容: ```java @GetMapping("/download") public StreamingResponseBody downloadFile() { return outputStream -> { // 从文件中读取内容,并写入到输出流中 File file = new File("path/to/file.txt"); try (InputStream inputStream = new FileInputStream(file)) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } catch (IOException e) { e.printStackTrace(); 在上述代码中,我们通过StreamingResponseBody的writeTo方法将文件的内容写入到输出流中。这样,当客户端请求该接口时,文件的内容会以流的形式返回给客户端进行下载。