【问题标题】:Spring Boot Exception during download results in empty file下载期间的Spring Boot异常导致空文件
【发布时间】:2019-08-28 09:19:05
【问题描述】:

Reproduction of the error here. 请在抛出异常之前移除 cmets 以查看问题。路径为:http://localhost:8080/api/download

原问题: 我允许用户在 Angular7 应用程序中使用 JasperReports 下载报告,但问题仅与 Spring Boot 相关。报告生成工作正常,但我无法让文件下载的行为符合我的预期。

目前: 下载链接使用href,链接使用target="_blank"。用户单击它,浏览器会打开一个新选项卡(在后台)并弹出File Save As 窗口。如果一切正常,则文件保存没有问题。但是,如果在某个地方生成PDF时出现异常,浏览器仍然会弹出File Save As窗口并允许用户保存文件,它会完成,但文件将是0 bytes

应该是:当出现异常时,浏览器应该打开一个带有某种错误消息的新选项卡,但如果没有错误,它应该显示“文件另存为”窗口。

代码:

@GetMapping("/salary-report/{id}")
public void generateSalaryReport(@PathVariable("id") long salaryReportId, HttpServletResponse response) throws IOException, JRException, SQLException {
    JasperPrint jasperPrint;
    var salaryReport = salaryReportRepositoryEx.findById(salaryReportId).orElseThrow(ResourceNotFoundException::new);

    try (OutputStream out = response.getOutputStream()) {
        HashMap<String, Object> parameters = new HashMap<>();

        parameters.put("ReportId", salaryReport.getId());

        // Set meta data
        response.setContentType("application/x-download");
        response.setHeader(
            "Content-Disposition",
            String.format("attachment; filename=\"%s%s-report%s-%s-%s.pdf\"",
                .... parameters
            )
        );

        // Set report
        jasperPrint = salaryReportJasperReport.render(parameters); // exception usually here
        JasperExportManager.exportReportToPdfStream(jasperPrint, out);
    } catch (Exception e) {
        // I tried changing the content type on Exception, but the same
        response.setContentType("text/plain");
        response.setHeader("Content-Disposition", null);
        throw e;
    }
}

带有链接的 HTML 代码 (Angular7):

<td>
    <a [href]="serverApiUrl+'/jasper/salary-report/'+salaryReport.id"
       target="_blank"
    >
        PDF Download
    </a>
</td>

编辑:如果我只是手动导航到完整的下载 URL,也会发生同样的事情。

Edit2:也在邮递员中尝试过。使用无效的报告 ID 会返回预期的 404 Json (ResourceNotFoundException),但继续使用代码总是返回 200 OK(即使我在 catch 块中手动将 HTTP 代码设置为 500)并且正文为空。

编辑3: 我尝试使用异常处理程序:

@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
public class JasperReportGenerationFailedException extends Exception {
    public JasperReportGenerationFailedException() {
        super();
    }

    public JasperReportGenerationFailedException(String message) {
        super(message);
    }

    public JasperReportGenerationFailedException(String message, Throwable cause) {
        super(message, cause);
    }

    public JasperReportGenerationFailedException(Throwable cause) {
        super(cause);
    }

    protected JasperReportGenerationFailedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

 @ExceptionHandler(JasperReportGenerationFailedException.class )
    public ResponseEntity<String> handleJasperException(JasperReportGenerationFailedException ex) {
        log.error("Salary Report generation failed.", ex);
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }

@GetMapping("/salary-report/{id}")
public void generateSalaryReport(@PathVariable("id") long salaryReportId, HttpServletResponse response) throws JasperReportGenerationFailedException {
    JasperPrint jasperPrint;
    var salaryReport = salaryReportRepositoryEx.findById(salaryReportId).orElseThrow(ResourceNotFoundException::new);

    try (OutputStream out = response.getOutputStream()) {
        HashMap<String, Object> parameters = new HashMap<>();

        parameters.put("ReportId", salaryReport.getId());

        // Set meta data
        response.setContentType("application/x-download");
        response.setHeader(
            "Content-Disposition",
            ...
            )
        );

        if (1 == 1/1) { // for testing, i always throw
            throw new Exception("Test");
        }
        // Set report
        jasperPrint = salaryReportJasperReport.render(parameters);
        JasperExportManager.exportReportToPdfStream(jasperPrint, out);
    } catch (Exception e) {
        throw new JasperReportGenerationFailedException(e);
    }
}

【问题讨论】:

    标签: java spring-boot spring-mvc exception


    【解决方案1】:

    此问题无法完全解决。 HttpServletResponse 不允许在设置后删除任何标头,因此我必须在响应更改之前移动渲染(response.setHeader("Content-Disposition",.. 之前)。这样就可以正常抛出异常了。

    更多信息here

    【讨论】:

      【解决方案2】:

      尝试将响应 http 状态代码设置为某些代码 4xx 或 5xx 可能是 (500),应该可以。

      【讨论】:

      • 我尝试在异常情况下使用response.setStatus(500); 进行设置,但没有奏效。仍然保存一个 0 字节的文件。
      • 我猜这个问题是你如何从你的 JSP 或前端调用它,你能粘贴你试图下载的代码
      • 只是一个简单的 HTML 链接。
      【解决方案3】:

      问题是由于您没有以正确的方式处理异常。您应该阅读此post 以获取有关Spring MVC 提供的异常处理工具的更多详细信息,以便使用正确的方式来满足您的项目需求。

      这是一个可能的解决方案。

      首先你必须在你的方法中做这些改变。

          @GetMapping("/salary-report/{id}")
          public void generateSalaryReport(@PathVariable("id") long salaryReportId, HttpServletResponse response) throws IOException {
              JasperPrint jasperPrint;
              var salaryReport = salaryReportRepositoryEx.findById(salaryReportId).orElseThrow(ResourceNotFoundException::new);
      
              try (OutputStream out = response.getOutputStream()) {
                  HashMap<String, Object> parameters = new HashMap<>();
      
                  parameters.put("ReportId", salaryReport.getId());
      
                  // Set meta data
                  response.setContentType("application/x-download");
                  response.setHeader(
                      "Content-Disposition",
                      String.format("attachment; filename=\"%s%s-report%s-%s-%s.pdf\"",
                          .... parameters
                      )
                  );
      
                  // Set report
                  jasperPrint = salaryReportJasperReport.render(parameters); // exception usually here
                  JasperExportManager.exportReportToPdfStream(jasperPrint, out);
              } catch (Exception e) {        
                  throw new IOException("Salary report generation failed for id: " + salaryReportId);
              }
          }
      

      他们在你的控制器中添加这个方法。

      @ExceptionHandler(IOException.class )
      public ResponseEntity<String> handleAccessDeniedException(IOException ex) {
          //TODO Log your exception with a logging framework 
          return new ResponseEntity<String>(ex.getMessage, HttpStatus.INTERNAL_SERVER_ERROR);
       }
      

      【讨论】:

      • 我试过这个,但是没有用。它记录错误,但仍然返回一个空文件。
      • 好的,你能提供一个minimal reproducible example,我怀疑它与Jasper响应渲染有关
      • 它与 Jasper 无关,因为我在开始对 Jasper 做任何事情之前手动抛出异常。 (检查我的编辑)此外,第一行的异常(404,未找到)按预期工作。由于某种原因,如果我一旦设置了内容类型和标题,我将无法再返回文本内容。
      猜你喜欢
      • 2017-11-02
      • 2017-05-11
      • 1970-01-01
      • 2019-01-10
      • 2022-01-23
      • 1970-01-01
      • 1970-01-01
      • 2020-10-15
      • 2012-11-27
      相关资源
      最近更新 更多