Return 来自 Spring @Controller 的文件具有 OutputStream

Return file from Spring @Controller having OutputStream

我想 return 来自 Spring 控制器的文件。我已经有 API 可以给我任何 OutputStream 的实现,然后我需要将它发送给用户。

所以流程是这样的:

获取输出流 -> 服务将此输出流传递给控制器​​ -> 控制器必须将其发送给用户

我想我需要输入流来完成它,我还发现 Apache Commons api 功能如下所示:

IOUtils.copy(InputStream is, OutputStream os)

但问题是,它将它转换到另一边 -> 不是从 osis,而是从 [=52] =]是到os.

编辑

说清楚,因为我看到答案不正确:
我使用 Dropbox api 并在 OutputStream 中接收文件,我希望在输入一些 URL

时将此输出流发送给用户
FileOutputStream outputStream = new FileOutputStream(); //can be any instance of OutputStream
DbxEntry.File downloadedFile = client.getFile("/fileName.mp3", null, outputStream);

这就是为什么我在谈论将 outputstream 转换为 inputstream,但不知道如何去做。此外,我想有更好的方法来解决这个问题(也许 return 字节数组以某种方式来自输出流)

我试图通过参数将 servlet outputstream [response.getOutputstream()] 传递给从 dropbox 下载文件的方法,但它根本不起作用

编辑 2

我的应用程序的 "flow" 是这样的:@Joeblade

  1. 用户输入url:/download/{file_name}

  2. Spring 控制器捕获 url 并调用 @Service 层下载 文件并将其传递给该控制器:

    @RequestMapping(value = "download/{name}", method = RequestMethod.GET)
    public void getFileByName(@PathVariable("name") final String name, HttpServletResponse response) throws IOException {
        response.setContentType("audio/mpeg3");
        response.setHeader("Content-Disposition", "attachment; filename=" + name);
        service.callSomeMethodAndRecieveDownloadedFileInSomeForm(name); // <- and this file(InputStream/OutputStream/byte[] array/File object/MultipartFile I dont really know..) has to be sent to the user
    }
    
  3. 现在 @Service 调用 Dropbox API 并通过指定的 file_name[=71 下载文件=],并将其全部放入 OutputStream,然后将其(以某种形式..可能是 OutputStream,byte[] 数组或任何其他对象 - 我不知道哪个更好用)传递给控制器​​:

    public SomeObjectThatContainsFileForExamplePipedInputStream callSomeMethodAndRecieveDownloadedFileInSomeForm(final String name) throws IOException {
        //here any instance of OutputStream - it needs to be passed to client.getFile lower (for now it is PipedOutputStream)
        PipedInputStream inputStream = new PipedInputStream(); // for now
        PipedOutputStream outputStream = new PipedOutputStream(inputStream);
    
    
        //some dropbox client object
        DbxClient client = new DbxClient();
        try {
            //important part - Dropbox API downloads the file from Dropbox servers to the outputstream object passed as the third parameter
            client.getFile("/" + name, null, outputStream);
        } catch (DbxException e){
            e.printStackTrace();
        } finally {
            outputStream.close();
        }
        return inputStream;
    }
    
  4. Controler 收到文件(我根本不知道我上面说的是哪种形式)然后将其传递给 用户

所以事情是通过调用 dropboxClient.getFile() 方法接收 OutputStream 和下载的文件,然后必须将包含下载文件的 OutputStream 发送给用户,怎么做?

从 HttpServletResponse 获取 OutputStream 并将文件写入其中(在此示例中使用来自 Apache Commons 的 IOUtils)

@RequestMapping(value = "/download", method = RequestMethod.GET)
public void download(HttpServletResponse response) {
    ...
    InputStream inputStream = new FileInputStream(new File(PATH_TO_FILE)); //load the file
    IOUtils.copy(inputStream, response.getOutputStream());
    response.flushBuffer();
    ...
}

确保在出现异常时使用 try/catch 关闭流。

我推荐阅读 this answer

@ResponseBody
@RequestMapping("/photo2", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE)
public byte[] testphoto() throws IOException {
    InputStream in = servletContext.getResourceAsStream("/images/no_image.jpg");
    return IOUtils.toByteArray(in);
}

michal.kreuzman回答

我本来打算自己写一些类似的东西,但当然已经有人回答了。

如果你只想传递流而不是首先将所有内容都放在内存中,你可以使用 this answer 我没有测试过这个(不是在工作中)但它看起来是合法的:)

@RequestMapping(value = "report1", method = RequestMethod.GET, produces = "application/pdf")
@ResponseBody
public void getReport1(OutputStream out) {
    InputStream in; // retrieve this from wherever you are receiving your stream
    byte[] buffer = new byte[1024];
    int len;
    while ((len = in.read(buffer)) != -1) {
        out.write(buffer, 0, len);
    }
    in.close();
    out.flush(); // out.close? 
}

事实是,这与 IOUtils.copy / IOUtils.copyLarge 几乎相同。线路:2128 你说的那个抄错方向了

但是首先要确保你理解你的问题。如果您想从输出流(用于写入的对象)读取并写入输入流(要读取的对象),那么我认为您真正想要的是写入一个也提供读取选项的对象。

为此,您可以使用 PipedInputStream 和 PipedOutputStream。它们连接在一起,以便写入输出流的字节可以从相应的输入流中读取。

所以在您接收字节的位置,我假设您正在将字节写入输出流。 这样做:

// set up the input/output stream so that bytes written to writeToHere are available to be read from readFromhere
PipedInputStream readFromHere = new PipedInputStream();
PipedOutputStream writeToHere = new PipedOutputStream(readFromHere);

// write to the outputstream as you like
writeToHere.write(...)

// or pass it as an outputstream to an external method
someMather(writeToHere);

// when you're done close this end.
writeToHere.close();


// then whenever you like, read from the inputstream
IOUtils.copy(readFromHere, out, new byte[1024]); 

如果你使用IOUtils.copy它将继续读取直到输出流关闭。所以确保它在开始之前已经关闭(如果你 运行 write/read 在同一个线程上)或者使用另一个线程写入输出缓冲区并在最后关闭它。

如果这仍然不是您要查找的内容,那么您必须完善您的问题。

您可以使用 ByteArrayOutputStreamByteArrayInputStream。示例:

// A ByteArrayOutputStream holds the content in memory
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

// Do stuff with your OutputStream

// To convert it to a byte[] - simply use
final byte[] bytes = outputStream.toByteArray();

// To convert bytes to an InputStream, use a ByteArrayInputStream
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);

您可以对其他 流对 执行相同的操作。例如。文件流:

// Create a FileOutputStream
FileOutputStream fos = new FileOutputStream("filename.txt");

// Write contents to file

// Always close the stream, preferably in a try-with-resources block
fos.close();

// The, convert the file contents to an input stream
final InputStream fileInputStream = new FileInputStream("filename.txt");

并且,当使用 Spring MVC 时,您绝对可以 return 包含您的文件的 byte[]。只需确保使用 @ResponseBody 注释您的回复即可。像这样:

@ResponseBody
@RequestMapping("/myurl/{filename:.*}")
public byte[] serveFile(@PathVariable("file"} String file) throws IOException {
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 
    DbxEntry.File downloadedFile = client.getFile("/" + filename, null, outputStream);
    return outputStream.toByteArray();
} 

写入响应输出流时要记住的一件事是,对您定期包装它的任何编写器调用 flush() 是一个非常好的主意。这样做的原因是断开的连接(例如由用户取消下载引起)可能不会在很长一段时间内抛出异常,如果有的话。这实际上可能是您容器上的资源泄漏。

最可取的解决方案是使用 InputStreamResourceResponseEntity。您只需手动设置 Content-Length

@RequestMapping(value = "/download", method = RequestMethod.GET)
public ResponseEntity download() throws IOException {
    String filePath = "PATH_HERE";
    InputStream inputStream = new FileInputStream(new File(filePath));
    InputStreamResource inputStreamResource = new InputStreamResource(inputStream);
    HttpHeaders headers = new HttpHeaders();
    headers.setContentLength(Files.size(Paths.get(filePath)));
    return new ResponseEntity(inputStreamResource, headers, HttpStatus.OK);
}

在您的情况下,最节省内存的解决方案是将响应 OutputStream 直接传递给 Dropbox API:

@GetMapping(value = "download/{name}")
public void getFileByName(@PathVariable("name") final String name, HttpServletResponse response)
        throws IOException, DbxException {
    response.setContentType("audio/mpeg3");
    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + name + "\"");
    response.setContentLength(filesize); // if you know size of the file in advance

    new DbxClient().getFile("/" + name, null, response.getOutputStream());
}

API读取的数据将直接发送给用户。不需要任何类型的额外字节缓冲区。


至于PipedInputStream/PipedOutputStream,它们是用于2个线程之间的阻塞通信。 PipedOutputStream 在 1024 字节后(默认情况下)阻止写入线程,直到其他线程开始从管道末端读取 (PipedInputStream)。