【问题标题】:How to provide a file download from a JSF backing bean?如何从 JSF 支持 bean 提供文件下载?
【发布时间】:2020-03-14 00:15:18
【问题描述】:

有没有办法从 JSF 支持 bean 操作方法提供文件下载? 我已经尝试了很多东西。主要问题是我不知道如何获取响应的OutputStream 以便将文件内容写入。我知道如何使用Servlet 来执行此操作,但这不能从 JSF 表单中调用,并且需要新的请求。

如何从当前FacesContext获取响应的OutputStream

【问题讨论】:

    标签: jsf jsf-2 download backing-beans


    【解决方案1】:

    简介

    您可以通过ExternalContext 获得一切。在 JSF 1.x 中,您可以通过 ExternalContext#getResponse() 获取原始的 HttpServletResponse 对象。在 JSF 2.x 中,您可以使用一系列新的委托方法,例如 ExternalContext#getResponseOutputStream(),而无需从 JSF 底层获取 HttpServletResponse

    在响应中,您应该设置Content-Type 标头,以便客户端知道哪个应用程序与提供的文件相关联。并且,您应该设置Content-Length 标头,以便客户端可以计算下载进度,否则将是未知的。并且,如果您想要 另存为 对话框,则应将 Content-Disposition 标头设置为 attachment,否则客户端将尝试内联显示它。最后只需将文件内容写入响应输出流即可。

    最重要的部分是调用FacesContext#responseComplete()通知JSF在你将文件写入响应后它不应该执行导航和渲染,否则响应的结尾会被页面的HTML内容污染,或者在较旧的 JSF 版本中,当 JSF 实现调用 getWriter() 以呈现 HTML 时,您将收到带有类似 getoutputstream() has already been called for this response 的消息的 IllegalStateException

    关闭 ajax / 不要使用远程命令!

    您只需要确保操作方法不是由 ajax 请求调用,而是在您使用<h:commandLink><h:commandButton> 触发时由正常请求调用。 Ajax 请求和远程命令由 JavaScript 处理,由于安全原因,JavaScript 没有工具来强制 Save As 与 ajax 响应的内容进行对话。

    如果您正在使用例如PrimeFaces <p:commandXxx>,那么您需要确保通过 ajax="false" 属性显式关闭 ajax。如果您使用的是 ICEfaces,则需要在命令组件中嵌套 <f:ajax disabled="true" />

    通用 JSF 2.x 示例

    public void download() throws IOException {
        FacesContext fc = FacesContext.getCurrentInstance();
        ExternalContext ec = fc.getExternalContext();
    
        ec.responseReset(); // Some JSF component library or some Filter might have set some headers in the buffer beforehand. We want to get rid of them, else it may collide.
        ec.setResponseContentType(contentType); // Check http://www.iana.org/assignments/media-types for all types. Use if necessary ExternalContext#getMimeType() for auto-detection based on filename.
        ec.setResponseContentLength(contentLength); // Set it with the file size. This header is optional. It will work if it's omitted, but the download progress will be unknown.
        ec.setResponseHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); // The Save As popup magic is done here. You can give it any file name you want, this only won't work in MSIE, it will use current request URL as file name instead.
    
        OutputStream output = ec.getResponseOutputStream();
        // Now you can write the InputStream of the file to the above OutputStream the usual way.
        // ...
    
        fc.responseComplete(); // Important! Otherwise JSF will attempt to render the response which obviously will fail since it's already written with a file and closed.
    }
    

    通用 JSF 1.x 示例

    public void download() throws IOException {
        FacesContext fc = FacesContext.getCurrentInstance();
        HttpServletResponse response = (HttpServletResponse) fc.getExternalContext().getResponse();
    
        response.reset(); // Some JSF component library or some Filter might have set some headers in the buffer beforehand. We want to get rid of them, else it may collide.
        response.setContentType(contentType); // Check http://www.iana.org/assignments/media-types for all types. Use if necessary ServletContext#getMimeType() for auto-detection based on filename.
        response.setContentLength(contentLength); // Set it with the file size. This header is optional. It will work if it's omitted, but the download progress will be unknown.
        response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); // The Save As popup magic is done here. You can give it any file name you want, this only won't work in MSIE, it will use current request URL as file name instead.
    
        OutputStream output = response.getOutputStream();
        // Now you can write the InputStream of the file to the above OutputStream the usual way.
        // ...
    
        fc.responseComplete(); // Important! Otherwise JSF will attempt to render the response which obviously will fail since it's already written with a file and closed.
    }
    

    常用静态文件示例

    如果您需要从本地磁盘文件系统流式传输静态文件,请替换以下代码:

    File file = new File("/path/to/file.ext");
    String fileName = file.getName();
    String contentType = ec.getMimeType(fileName); // JSF 1.x: ((ServletContext) ec.getContext()).getMimeType(fileName);
    int contentLength = (int) file.length();
    
    // ...
    
    Files.copy(file.toPath(), output);
    

    常用动态文件示例

    如果您需要流式传输动态生成的文件,例如 PDF 或 XLS,则只需在此处提供 output,使用的 API 需要 OutputStream

    例如iText PDF:

    String fileName = "dynamic.pdf";
    String contentType = "application/pdf";
    
    // ...
    
    Document document = new Document();
    PdfWriter writer = PdfWriter.getInstance(document, output);
    document.open();
    // Build PDF content here.
    document.close();
    

    例如Apache POI HSSF:

    String fileName = "dynamic.xls";
    String contentType = "application/vnd.ms-excel";
    
    // ...
    
    HSSFWorkbook workbook = new HSSFWorkbook();
    // Build XLS content here.
    workbook.write(output);
    workbook.close();
    

    请注意,您不能在此处设置内容长度。因此,您需要删除该行来设置响应内容长度。这在技术上没有问题,唯一的缺点是最终用户会看到一个未知的下载进度。如果这很重要,那么您确实需要先写入本地(临时)文件,然后再提供它,如前一章所示。

    实用方法

    如果您使用的是 JSF 实用程序库 OmniFaces,那么您可以使用三种方便的 Faces#sendFile() 方法之一,采用 FileInputStreambyte[],并指定是否该文件应作为附件 (true) 或内联 (false) 下载。

    public void download() throws IOException {
        Faces.sendFile(file, true);
    }
    

    是的,这段代码是完整的。您不需要自己调用responseComplete() 等等。此方法还可以正确处理特定于 IE 的标头和 UTF-8 文件名。你可以找到source code here

    【讨论】:

    • 这么简单!我一直想知道如何根据他们的展示为 PrimeFaces 提供下载,因为它需要p:fileDownloadInputStream 基础设施,而且我还没有管理如何将OutputStream 转换为InputStream。现在很明显,即使是动作侦听器也可以更改响应内容类型,然后响应将被视为用户代理端的文件下载。谢谢!
    • 有没有办法使用 HTTP GET 而不是 HTTP POST(h:commandButton 和 h:commandLink)来做到这一点?
    • @Alfredo:是的,在无标记视图中使用preRenderView 侦听器。下载(以及服务)JSON 的类似问题在这里得到解答:stackoverflow.com/questions/8358006/…
    • @BalusC 您涵盖了所有 jsf 主题 - 感谢您让我的生活更轻松,先生!
    【解决方案2】:
    public void download() throws IOException
    {
    
        File file = new File("file.txt");
    
        FacesContext facesContext = FacesContext.getCurrentInstance();
    
        HttpServletResponse response = 
                (HttpServletResponse) facesContext.getExternalContext().getResponse();
    
        response.reset();
        response.setHeader("Content-Type", "application/octet-stream");
        response.setHeader("Content-Disposition", "attachment;filename=file.txt");
    
        OutputStream responseOutputStream = response.getOutputStream();
    
        InputStream fileInputStream = new FileInputStream(file);
    
        byte[] bytesBuffer = new byte[2048];
        int bytesRead;
        while ((bytesRead = fileInputStream.read(bytesBuffer)) > 0) 
        {
            responseOutputStream.write(bytesBuffer, 0, bytesRead);
        }
    
        responseOutputStream.flush();
    
        fileInputStream.close();
        responseOutputStream.close();
    
        facesContext.responseComplete();
    
    }
    

    【讨论】:

      【解决方案3】:

      这对我有用:

      public void downloadFile(String filename) throws IOException {
          final FacesContext fc = FacesContext.getCurrentInstance();
          final ExternalContext externalContext = fc.getExternalContext();
      
          final File file = new File(filename);
      
          externalContext.responseReset();
          externalContext.setResponseContentType(ContentType.APPLICATION_OCTET_STREAM.getMimeType());
          externalContext.setResponseContentLength(Long.valueOf(file.lastModified()).intValue());
          externalContext.setResponseHeader("Content-Disposition", "attachment;filename=" + file.getName());
      
          final HttpServletResponse response = (HttpServletResponse) externalContext.getResponse();
      
          FileInputStream input = new FileInputStream(file);
          byte[] buffer = new byte[1024];
          final ServletOutputStream out = response.getOutputStream();
      
          while ((input.read(buffer)) != -1) {
              out.write(buffer);
          }
      
          out.flush();
          fc.responseComplete();
      }
      

      【讨论】:

      • 2个工作日后,这解决了我的问题,稍作改动:)非常感谢。
      • @ÖMERTAŞCI:发生了什么变化,
      【解决方案4】:

      这是我的解决方案,BalusC's answer的扩展

      public static void download(
         ByteArrayOutputStream baos, 
         String downloadFileName, 
         String contentType
      ) {
          FacesContext context = FacesContext.getCurrentInstance();
          ExternalContext externalContext = context.getExternalContext();
          externalContext.responseReset();
          HttpServletResponse response = (HttpServletResponse) externalContext.getResponse();
          response.reset();
          response.setContentType(contentType);
          response.setHeader("Expires", "0");
          response.setHeader("Cache-Control", "must-revalidate, post-check=0, pre-check=0");
          response.setHeader("Pragma", "public");
          Integer size = baos.size();
          response.setHeader("Content-Length", size.toString());
      
          response.setHeader(
             "Content-Disposition", 
             "attachment; filename=\"" + downloadFileName + "\""
          );
      
          try {
             try (OutputStream responseOs = response.getOutputStream()) {
                 baos.writeTo(responseOs);
             }
          }
          catch (IOException e) {
             throw new IOUncheckedException(e);
          }
      
          context.responseComplete();
      }
      

      【讨论】:

        【解决方案5】:

        这里是完整的代码 sn -p http://bharatonjava.wordpress.com/2013/02/01/downloading-file-in-jsf-2/

         @ManagedBean(name = "formBean")
         @SessionScoped
         public class FormBean implements Serializable
         {
           private static final long serialVersionUID = 1L;
        
           /**
            * Download file.
            */
           public void downloadFile() throws IOException
           {
              File file = new File("C:\\docs\\instructions.txt");
              InputStream fis = new FileInputStream(file);
              byte[] buf = new byte[1024];
              int offset = 0;
              int numRead = 0;
              while ((offset < buf.length) && ((numRead = fis.read(buf, offset, buf.length -offset)) >= 0)) 
              {
                offset += numRead;
              }
              fis.close();
              HttpServletResponse response =
                 (HttpServletResponse) FacesContext.getCurrentInstance()
                .getExternalContext().getResponse();
        
             response.setContentType("application/octet-stream");
             response.setHeader("Content-Disposition", "attachment;filename=instructions.txt");
             response.getOutputStream().write(buf);
             response.getOutputStream().flush();
             response.getOutputStream().close();
             FacesContext.getCurrentInstance().responseComplete();
           }
         }
        

        如果您希望文件在运行时生成,您可以更改文件读取逻辑。

        【讨论】:

        • 这只会让你获得输入文件的一部分,如果它大于 1024 字节!
        猜你喜欢
        • 2015-08-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多