【问题标题】:Return file from Spring @Controller having OutputStream从具有 OutputStream 的 Spring @Controller 返回文件
【发布时间】:2015-02-28 17:35:51
【问题描述】:

我想从 Spring 控制器返回一个文件。我已经有了可以给我任何 OutputStream 实现的 API,然后我需要将它发送给用户。

所以流程是这样的:

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

我想我需要输入流来做这件事,我还发现了 Apache Commons api 功能,看起来像这样:

IOUtils.copy(InputStream is, OutputStream os)

但问题是,它将其转换到另一端 -> 不是从 osis,而是从 is操作系统

编辑

要清楚,因为我看到答案不是正确的:
我使用 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,但不知道如何去做。此外,我认为有更好的方法来解决这个问题(可能以某种方式从输出流返回字节数组)

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

编辑 2

我的应用程序的“流程”是这样的:@Joeblade

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

  2. Spring Controller 捕获 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 下载文件,并将其全部放入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. 控制器接收文件(我根本不知道我上面所说的格式)然后传递给用户

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

【问题讨论】:

    标签: java spring file spring-mvc


    【解决方案1】:

    从 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 关闭流。

    【讨论】:

    • @John Smith,我正在尝试下载音频文件(.wav),您的解决方案在我的开发机器(Windows 操作系统)上运行良好,但是当我在 Ubuntu Server 上部署时,它只返回 200 个字节的结果在无法播放的损坏文件中...知道在 ubuntu 上运行解决方案有什么问题吗??
    • 这里是同一种解决方案的参考(刷新文件流以响应):twilblog.github.io/java/spring/rest/file/stream/2015/08/14/…
    • (对不起,如果这是迂腐的话,但我很好奇其中的区别)。这种解决方案是否会流式传输响应本身?或者响应不是流式传输,而是首先流创建(在非流响应中返回之前)?
    【解决方案2】:

    最好的解决方案是使用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);
    }
    

    【讨论】:

    • 我们不是要关闭流吗?我的意思是,流是如何关闭的。
    • Stream 被包装到 HttpServletResponse 中,所以接收者应该关闭它。我认为 Servlet 容器必须在后台处理这个问题。
    • HttpHeaders 不是必需的。你可以写return ResponseEntity.ok().contentLength(Files.size(Paths.get(filePath)).body(inputStreamResource)
    【解决方案3】:

    您可以使用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 时,您绝对可以返回包含您的文件的 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();
    } 
    

    【讨论】:

    • 是的,工作得很好:) 但我的问题是创建多个流是否是一种好习惯?我应该在哪里关闭(如果有的话)这些创建的流中的任何一个?我希望性能尽可能好
    • 嗯,字节流的问题是你把所有的东西都保存在内存中。如果文件很大,也意味着您在内存中保留了大量字节。最好的方法可能是直接传递response.getOutputStream()。为什么这对你不起作用?
    • 我实际上不知道,当我从控制器返回 void 并将文件保存到 response.getOutputStream() 时,当我输入 URL 时没有任何反应,但是当我从控制器返回 byte[] 并添加一些响应的标题一切都是正确的;还有一个问题:如何检查文件是否为空? (当传递文件名时,Dropbox 中不存在该文件名,然后我得到的不是带有适当例如 404 代码的空响应,而是空文件和 200 OK)
    【解决方案4】:

    我推荐阅读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,它将继续读取,直到输出流关闭。所以请确保它在开始之前已经关闭(如果您在同一个线程上运行写入/读取)或使用另一个线程写入输出缓冲区并在最后关闭它。

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

    【讨论】:

    • @azalut 如果这没有帮助,那么我不确定我是否理解这个问题:D
    • apperciate 您的更新 :) 我已经尝试过您的回答,它很好,但仍然不完美;当我输入向我发送文件的 URL 时,我会收到它,但它是空的并且没有扩展名。从保管箱下载文件后,我希望以某种方式将该文件发送给用户;我会更新一下我的帖子,也许你可以帮助我更多:)
    • 我发现了问题;当我输入 url:name.extension 时,只有名称保存到 @PathVariable 而扩展名没有,所以我不得不使用一些 reg-exp 来匹配它,现在工作正常:) 虽然我仍然需要检查某个地方,如果我的文件不为空
    【解决方案5】:

    在您的情况下,最节省内存的解决方案是将响应 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)。

    【讨论】:

      【解决方案6】:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-04-26
        • 2020-07-04
        • 2012-06-01
        • 2021-02-22
        • 2014-11-22
        • 1970-01-01
        相关资源
        最近更新 更多