The correct way to download files in SpringBoot~

Spring Boot cooperates with axios to realize file download function

foreword

Recently, I encountered a strange requirement. The front-end requests to download compressed files through post, and at the same time, some data will be passed to the back-end to generate compressed packages. At this time, the back-end interface is not just to generate a compressed file stream and output it to the front-end. It must have the ability to report errors and handle exceptions. That is, if the backend reports an error, the frontend should not be able to download the file stream.

more general solution

Generally speaking, Spring Boot generates a file stream for the front-end to download, and will directly write the file stream to it HttpServletResponse.getOutputStream(). However, there will be a problem in this way. No matter how the back-end reports an error, the front-end can successfully download the file, because status=200. That is, it is written as follows:

@PostMapping(value = "/project/code/download")
public void downloadCode(@RequestBody TProjectInfo pf, HttpServletResponse response) {
    
    
    // 1.生成源码文件
    // 2.压缩文件
    // 3.设置回复的一些参数
    // 4.将压缩文件写入网络流
    log.info("request param: {}", pf);
    OutputStream os = null;
        try {
    
    
            // 配置文件下载
            // 下载文件能正常显示中文
            String filename = pf.getProjectName() + System.currentTimeMillis() + ".zip";
            response.setHeader("Content-Disposition", "attachment;filename=" + filename);
            response.setHeader("Content-Type", "application/octet-stream");
            response.setContentType("application/octet-stream; charset=UTF-8");

            os = response.getOutputStream();

            projectService.generateCode(pf, os);

            log.info("Download  successfully!");
        } catch (Exception e) {
    
    
            log.error("Download  failed: {}", e.getMessage());
            throw new OptErrorException(OptStatus.FAIL.code, "文件下载失败");
        } finally {
    
    
            if (os != null) {
    
    
                try {
    
    
                    os.close();
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
}

Analyze the reasons

Because the backend directly writes to the OutputStream, all exception captures will be covered, so the frontend can directly fetch the byte stream data from the data.

correct solution

In fact, it is necessary for the backend to return json data when an error occurs. When downloading correctly, you can directly fetch the data and fetch the byte stream, so use ResponseEntityReturn.

@PostMapping(value = "/project/code/download")
public ResponseEntity<InputStreamResource> downloadCode(@RequestBody TProjectInfo pf) {
    
    
   log.info("request param: {}", pf);

   String filename = pf.getProjectName() + System.currentTimeMillis() + ".zip";
   byte[] bytes = projectService.generateCode(pf);
   ByteArrayInputStream bais = new ByteArrayInputStream(bytes);

   HttpHeaders headers = new HttpHeaders();
   headers.add("Content-Disposition", String.format("attachment; filename=\"%s\"", filename));
   return ResponseEntity.ok()
           .headers(headers)
           .contentType(MediaType.parseMediaType("application/octet-stream"))
           .body(new InputStreamResource(bais));
}

The exception here is handled uniformly using RestExceptionAdvice:

@RestControllerAdvice
public class ExceptionAdvice {
    
    
    private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);

    /**
     * 处理数据绑定异常
     *
     * @param bindException 数据绑定异常
     * @return 统一响应基本结果
     */
    @ExceptionHandler(value = BindException.class)
    public ResponseEntity<Result<Object>> handleValidateException(BindException bindException) {
    
    
        logger.error("数据绑定异常,{}", bindException.getMessage(), bindException);
        return of(HttpStatus.BAD_REQUEST, "数据绑定异常");
    }


    /**
     * 统一处理 405 异常
     *
     * @param exception 请求方法不支持异常
     * @return 统一响应结果
     */
    @ExceptionHandler(value = HttpRequestMethodNotSupportedException.class)
    public ResponseEntity<Result<Object>> handleMethodNotSupportedException(HttpRequestMethodNotSupportedException exception) {
    
    
        if (exception != null) {
    
    
            logger.error("Http 405, {}", exception.getMessage(), exception);
        }

        return of(HttpStatus.METHOD_NOT_ALLOWED, "方法不被支持");

    }

    private ResponseEntity<Result<Object>> of(HttpStatus status, String msg) {
    
    
        return ResponseEntity.status(status).body(Result.builder().code(OptStatus.FAIL.code).msg(msg).build());
    }

    /**
     * 统一处理自定义操作错误异常
     *
     * @param exception 操作错误异常
     * @return 统一响应结果
     */
    @ExceptionHandler(value = OptErrorException.class)
    public ResponseEntity<Result<Object>> handleOptErrorException(OptErrorException exception) {
    
    
        return of(HttpStatus.INTERNAL_SERVER_ERROR, exception.getOptMsg());
    }

    /**
     * 统一处理 服务器内部 异常
     *
     * @param e 异常
     * @return 统一响应结果
     */
    @ExceptionHandler(value = Throwable.class)
    public ResponseEntity<Result<Object>> handle500(Throwable e) {
    
    
        if (e != null) {
    
    
            logger.error("Http 500, {}", e.getMessage(), e);
        }
        return of(HttpStatus.INTERNAL_SERVER_ERROR, "服务器内部未知错误");
    }

}

The above is the whole process of the solution. It may be a bit one-sided, and it is purely a little personal opinion in practice. Welcome everyone to exchange and learn!

Guess you like

Origin blog.csdn.net/dubulingbo/article/details/129641709