java - 通过 Spring MVC 下载大文件

标签 java spring spring-mvc

我有一个可以下载文件的休息方法。但是,在文件完全复制到输出流之前,似乎不会在 Web 客户端上开始下载,这对于大文件来说可能需要一段时间。

@GetMapping(value = "download-single-report")
public void downloadSingleReport(HttpServletResponse response) {

    File dlFile = new File("some_path");

    try {
        response.setContentType("application/pdf");
        response.setHeader("Content-disposition", "attachment; filename="+ dlFile.getName());
        InputStream inputStream = new FileInputStream(dlFile);
        IOUtils.copy(inputStream, response.getOutputStream());
        response.flushBuffer();
    } catch (FileNotFoundException e) {
        // error
    } catch (IOException e) {
        // error
    }
}

有没有一种方法可以“流式传输”文件,以便在我开始写入输出流时立即开始下载?

我也有一个类似的方法,它获取多个文件并将它们放入一个 zip,将每个 zip 条目添加到 zip 流,并且下载也仅在创建 zip 后开始:

        ZipEntry zipEntry = new ZipEntry(entryName);
        zipOutStream.putNextEntry(zipEntry);
        IOUtils.copy(fileStream, zipOutStream);

最佳答案

您可以使用InputStreamResource 返回流结果。我测试了,它立即开始复制到输出。

    @GetMapping(value = "download-single-report")
    public ResponseEntity<Resource> downloadSingleReport() {
        File dlFile = new File("some_path");
        if (!dlFile.exists()) {
            return ResponseEntity.notFound().build();
        }

        try {
            try (InputStream stream = new FileInputStream(dlFile)) {
                InputStreamResource streamResource = new InputStreamResource(stream);
                return ResponseEntity.ok()
                        .contentType(MediaType.APPLICATION_PDF)
                        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + dlFile.getName() + "\"")
                        .body(streamResource);
            }

            /*
            // FileSystemResource alternative
            
            FileSystemResource fileSystemResource = new FileSystemResource(dlFile);
            return ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_PDF)
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + dlFile.getName() + "\"")
                    .body(fileSystemResource);
           */ 
        } catch (IOException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

第二种选择是部分下载方法。

    @GetMapping(value = "download-single-report-partial")
    public void downloadSingleReportPartial(HttpServletRequest request, HttpServletResponse response) {
        File dlFile = new File("some_path");
        if (!dlFile.exists()) {
            response.setStatus(HttpStatus.NOT_FOUND.value());
            return;
        }
        try {
            writeRangeResource(request, response, dlFile);
        } catch (Exception ex) {
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        }
    }

    public static void writeRangeResource(HttpServletRequest request, HttpServletResponse response, File file) throws IOException {
        String range = request.getHeader("Range");
        if (StringUtils.hasLength(range)) {
            //http
            ResourceRegion region = getResourceRegion(file, range);
            long start = region.getPosition();
            long end = start + region.getCount() - 1;
            long resourceLength = region.getResource().contentLength();
            end = Math.min(end, resourceLength - 1);
            long rangeLength = end - start + 1;

            response.setStatus(206);
            response.addHeader("Accept-Ranges", "bytes");
            response.addHeader("Content-Range", String.format("bytes %s-%s/%s", start, end, resourceLength));
            response.setContentLengthLong(rangeLength);
            try (OutputStream outputStream = response.getOutputStream()) {
                try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
                    StreamUtils.copyRange(inputStream, outputStream, start, end);
                }
            }
        } else {
            response.setStatus(200);
            response.addHeader("Accept-Ranges", "bytes");
            response.setContentLengthLong(file.length());
            try (OutputStream outputStream = response.getOutputStream()) {
                try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
                    StreamUtils.copy(inputStream, outputStream);
                }
            }
        }
    }

    private static ResourceRegion getResourceRegion(File file, String range) {
        List<HttpRange> httpRanges = HttpRange.parseRanges(range);
        if (httpRanges.isEmpty()) {
            return new ResourceRegion(new FileSystemResource(file), 0, file.length());
        }
        return httpRanges.get(0).toResourceRegion(new FileSystemResource(file));
    }

Spring框架资源响应流程

Resource 响应由 ResourceHttpMessageConverter 类管理。在writeContent方法中,StreamUtils.copy被调用。

package org.springframework.http.converter;

public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<Resource> {
..
    protected void writeContent(Resource resource, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
        try {
            InputStream in = resource.getInputStream();
            try {
                StreamUtils.copy(in, outputMessage.getBody());
            }
            catch (NullPointerException ex) {
                // ignore, see SPR-13620
            }
            finally {
                try {
                    in.close();
                }
                catch (Throwable ex) {
                    // ignore, see SPR-12999
                }
            }
        }
        catch (FileNotFoundException ex) {
            // ignore, see SPR-12999
        }
    }
}

out.write(buffer, 0, bytesRead); 立即将数据发送到输出(我已经在我的本地机器上测试过)。传输全部数据时,调用 out.flush();

package org.springframework.util;

public abstract class StreamUtils {
..
    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;

        int bytesRead;
        for(byte[] buffer = new byte[4096]; (bytesRead = in.read(buffer)) != -1; byteCount += bytesRead) {
            out.write(buffer, 0, bytesRead);
        }

        out.flush();
        return byteCount;
    }
}

关于java - 通过 Spring MVC 下载大文件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45694168/

相关文章:

java - Jama 包,4x4 线性方程求解器

java - 如何使用 Spring Data REST 在 OneToMany 关系中添加对象

spring-mvc - spring-boot-starter-web 中默认的 JSON 错误响应来自哪里以及如何调整它?

java - 为什么在 SQL 中使用别名会返回错误?

java - startActivityForResult 之后 Android 旋转

java - native 查询 (JPA) 未重置并返回相同的旧结果

java - 通过 Spring 将字段注入(inject) Hibernate 加载的实体中

java - web.xml 中的错误页面给出 404

jquery - 使用 Spring 和 Ajax 从 Post 方法接收 Rest Controller 中的参数

java - SpringMVC - 维护国家等静态列表