从Spring Boot Rest服务下载文件

154

我正在尝试从Spring Boot Rest服务中下载文件。

@RequestMapping(path="/downloadFile",method=RequestMethod.GET)
    @Consumes(MediaType.APPLICATION_JSON_VALUE)
    public  ResponseEntity<InputStreamReader> downloadDocument(
                String acquistionId,
                String fileType,
                Integer expressVfId) throws IOException {
        File file2Upload = new File("C:\\Users\\admin\\Desktop\\bkp\\1.rtf");
        HttpHeaders headers = new HttpHeaders();
        headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
        headers.add("Pragma", "no-cache");
        headers.add("Expires", "0");
        InputStreamReader i = new InputStreamReader(new FileInputStream(file2Upload));
        System.out.println("The length of the file is : "+file2Upload.length());

        return ResponseEntity.ok().headers(headers).contentLength(file2Upload.length())
                .contentType(MediaType.parseMediaType("application/octet-stream"))
                .body(i);
        }

我尝试从浏览器下载文件,它开始下载,但总是失败。这个服务有什么问题导致下载失败吗?

7个回答

258

选项1 使用InputStreamResource

为给定的InputStream实现Resource

仅当没有其他特定的资源实现适用时才应使用此选项。特别是,在可能的情况下,应优先考虑ByteArrayResource或任何基于文件的资源实现。

@RequestMapping(path = "/download", method = RequestMethod.GET)
public ResponseEntity<Resource> download(String param) throws IOException {

    // ...

    InputStreamResource resource = new InputStreamResource(new FileInputStream(file));

    return ResponseEntity.ok()
            .headers(headers)
            .contentLength(file.length())
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .body(resource);
}

Option2 如 InputStreamResource 文档所建议 - 使用 ByteArrayResource

@RequestMapping(path = "/download", method = RequestMethod.GET)
public ResponseEntity<Resource> download(String param) throws IOException {

    // ...

    Path path = Paths.get(file.getAbsolutePath());
    ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path));

    return ResponseEntity.ok()
            .headers(headers)
            .contentLength(file.length())
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .body(resource);
}

4
我尝试将它做成 Word 文档的 .doc 格式,但在下载时格式消失了,文件被下载成没有文件扩展名的名称为“response”的文件。有什么建议吗? - Tulsi Jain
54
@TulsiJain 添加Content-Disposition HttpHeader: HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=myDoc.docx"); - fateddy
9
如果您不幸使用的是普通的 Spring 而不是 Spring Boot,请确保将 ResourceHttpMessageConverter 实例添加到您的 HttpMessageConverters 列表中。创建一个继承 WebMvcConfigurerAdapter@Configuration 类,实现 configureMessageConverters() 方法,并添加 converters.add(new ResourceHttpMessageConverter()); 这行代码。 - ashario
6
问题:选项1似乎没有关闭流。这是怎么实现的?选项2似乎在发送之前将整个文件加载到内存中。正确吗?还有其他选择吗?谢谢! - eventhorizon
3
对于大文件,ByteArrayResource是否适用?它不会占用整个堆空间吗?或者我们应该选择StreamingResponseBody以避免内存溢出? - jagga
显示剩余7条评论

65

以下示例代码适用于我,并且可能会对某些人有所帮助。

import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@RestController
@RequestMapping("/app")
public class ImageResource {

    private static final String EXTENSION = ".jpg";
    private static final String SERVER_LOCATION = "/server/images";

    @RequestMapping(path = "/download", method = RequestMethod.GET)
    public ResponseEntity<Resource> download(@RequestParam("image") String image) throws IOException {
        File file = new File(SERVER_LOCATION + File.separator + image + EXTENSION);

        HttpHeaders header = new HttpHeaders();
        header.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=img.jpg");
        header.add("Cache-Control", "no-cache, no-store, must-revalidate");
        header.add("Pragma", "no-cache");
        header.add("Expires", "0");

        Path path = Paths.get(file.getAbsolutePath());
        ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path));

        return ResponseEntity.ok()
                .headers(header)
                .contentLength(file.length())
                .contentType(MediaType.parseMediaType("application/octet-stream"))
                .body(resource);
    }

}

1
非常好,对我有用。 - Mayyar Al-Atari
1
这种方法可能适用于小文件,但如果处理较大的文件,直接写入OutputStream并避免将整个文件加载到内存中会更有效率。 - Yurii K

27

我建议使用StreamingResponseBody,因为它可以直接向响应(OutputStream)中写入数据,而不会阻塞Servlet容器线程。当您下载非常大的文件时,这是一种很好的方法。

@GetMapping("download")
public StreamingResponseBody downloadFile(HttpServletResponse response, @PathVariable Long fileId) {

    FileInfo fileInfo = fileService.findFileInfo(fileId);
    response.setContentType(fileInfo.getContentType());
    response.setHeader(
        HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=\"" + fileInfo.getFilename() + "\"");

    return outputStream -> {
        int bytesRead;
        byte[] buffer = new byte[BUFFER_SIZE];
        InputStream inputStream = fileInfo.getInputStream();
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
        }
    };
}

顺便说一下:使用 StreamingResponseBody 时,强烈建议配置在Spring MVC中用于执行异步请求的TaskExecutor。TaskExecutor是一个抽象了可运行对象执行过程的接口。

更多信息: https://medium.com/swlh/streaming-data-with-spring-boot-restful-web-service-87522511c071


https://stackoverflow.com/users/8650621/felipe-desiderati 我们需要关闭inputStream吗? - jagga
2
不会,因为它是可自动关闭的。只需检查我们是否返回了一个 lambda,框架会在之后调用它。 - Felipe Desiderati
我们可以在文件大小不超过10MB且以字节数组形式存储在数据库中时使用ByteArrayResource吗?还是StreamingResponseBody更好的选择? https://stackoverflow.com/users/8650621/felipe-desiderati - jagga

13

我想分享一个使用JavaScript(ES6)、ReactSpring Boot后端的简单文件下载方法:

  1. Spring Boot Rest Controller

来自org.springframework.core.io.Resource的资源

    @SneakyThrows
    @GetMapping("/files/{filename:.+}/{extraVariable}")
    @ResponseBody
    public ResponseEntity<Resource> serveFile(@PathVariable String filename, @PathVariable String extraVariable) {

        Resource file = storageService.loadAsResource(filename, extraVariable);
        return ResponseEntity.ok()
               .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"")
               .body(file);
    }
  1. 使用AXIOS进行React中的API调用

将responseType设置为arraybuffer,以指定响应中包含的数据类型。

export const DownloadFile = (filename, extraVariable) => {
let url = 'http://localhost:8080/files/' + filename + '/' + extraVariable;
return axios.get(url, { responseType: 'arraybuffer' }).then((response) => {
    return response;
})};

最后一步 > 下载
借助js-file-download,您可以触发浏览器将数据保存到文件中,就像下载一样。

DownloadFile('filename.extension', 'extraVariable').then(
(response) => {
    fileDownload(response.data, filename);
}
, (error) => {
    // ERROR 
});

1
我遇到了这个问题,并对CONTENT_DISPOSITION头文件中文件名用双引号括起来感到好奇。事实证明,如果文件名中有空格,则在没有双引号的情况下,您将无法在响应中获得完整的文件名。做得好,@fetahokey。 - Dana

10
如果您需要从服务器文件系统下载一个大文件,那么使用 ByteArrayResource 会占用所有Java堆空间。在这种情况下,您可以使用 FileSystemResource

1
如果文件以字节数组的形式存储在数据库中,我们能否使用文件系统资源?并且在单击时需要下载该文件。另外,“巨型文件”的定义是什么?我的文件大小为255213字节。 - jagga
@jagga 我认为更好的解决方案是使用ByteArrayResource(请参见此处的Option 2 https://dev59.com/xlsV5IYBdhLWcg3w4iFA#35683261),例如超过500MB的大文件。 - Taras Melon

4
    @GetMapping("/downloadfile/{productId}/{fileName}")
public ResponseEntity<Resource> downloadFile(@PathVariable(value = "productId") String productId,
        @PathVariable String fileName, HttpServletRequest request) {
    // Load file as Resource
    Resource resource;

    String fileBasePath = "C:\\Users\\v_fzhang\\mobileid\\src\\main\\resources\\data\\Filesdown\\" + productId
            + "\\";
    Path path = Paths.get(fileBasePath + fileName);
    try {
        resource = new UrlResource(path.toUri());
    } catch (MalformedURLException e) {
        e.printStackTrace();
        return null;
    }

    // Try to determine file's content type
    String contentType = null;
    try {
        contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
    } catch (IOException ex) {
        System.out.println("Could not determine file type.");
    }

    // Fallback to the default content type if type could not be determined
    if (contentType == null) {
        contentType = "application/octet-stream";
    }

    return ResponseEntity.ok().contentType(MediaType.parseMediaType(contentType))
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
            .body(resource);
}

为了测试它,请使用Postman

http://localhost:8080/api/downloadfile/GDD/1.zip


0

使用Apache IO也可以是复制流的另一个选择。

@RequestMapping(path = "/file/{fileId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> downloadFile(@PathVariable(value="fileId") String fileId,HttpServletResponse response) throws Exception {

    InputStream yourInputStream = ...
    IOUtils.copy(yourInputStream, response.getOutputStream());
    response.flushBuffer();
    return ResponseEntity.ok().build();
}

maven 依赖

    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-io</artifactId>
        <version>1.3.2</version>
    </dependency>

直接使用inputStream返回InputStreamResource,无需复制流。 - Steph

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接