通过Spring MVC下载大文件

时间:2017-08-15 13:45:24

标签: 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
        }
    }

是否有办法“流式传输”文件,以便在我开始写入输出流时立即启动downloda?

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

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

3 个答案:

答案 0 :(得分:1)

您可以使用 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;
    }
}

答案 1 :(得分:0)

使用

IOUtils.copyLarge(InputStream input, OutputStream output)
  

将字节从大(超过2GB)的InputStream复制到OutputStream。   此方法在内部缓冲输入,因此无需使用BufferedInputStream。

     

缓冲区大小由DEFAULT_BUFFER_SIZE给出。

IOUtils.copyLarge(InputStream input, OutputStream output, byte[] buffer)
  

将字节从大(超过2GB)的InputStream复制到OutputStream。   此方法使用提供的缓冲区,因此无需使用BufferedInputStream。

http://commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/IOUtils.html

答案 2 :(得分:0)

您可以使用“StreamingResponseBody” 文件下载将在块写入输出流时立即开始。下面是代码片段

@GetMapping (value = "/download-single-report")
public ResponseEntity<StreamingResponseBody> downloadSingleReport(final HttpServletResponse response) {
    final File dlFile = new File("Sample.pdf");
    response.setContentType("application/pdf");
    response.setHeader(
            "Content-Disposition",
            "attachment;filename="+ dlFile.getName());

    StreamingResponseBody stream = out -> FileCopyUtils.copy(new FileInputStream(dlFile), out);

    return new ResponseEntity(stream, HttpStatus.OK);
}