我如何让Mono等待dependenat提取方法运行

时间:2019-01-13 18:07:31

标签: spring-boot spring-webflux project-reactor

我正在尝试通过将webflux用作其他api和控制器的Web服务来实现对Excel功能的导出。我的问题是,从存储库中以Flux检索数据后,将调用生成excel文件的函数(那里没有问题)。我已经对结果进行了排序,并试图通过flatMap调用另一个填充方法,但是我遇到了很多问题,试图使其工作并确保flatMap中的代码先于网络服务中的代码运行以返回文件

以下是该Web服务的代码:


    @GetMapping(API_BASE_PATH + "/download")
        public ResponseEntity<byte[]> download() {
            Mono<Void> createExcel = excelExport.createDocument(false);

            Mono.when(createExcel).log("Excel Created").then();

            Workbook workbook = excelExport.getWb();

            OutputStream outputStream = new ByteArrayOutputStream();
            try {
                workbook.write(outputStream);
            } catch (IOException e) {
                e.printStackTrace();
            }

            byte[] media = ((ByteArrayOutputStream) outputStream).toByteArray();
            HttpHeaders headers = new HttpHeaders();
            headers.setCacheControl(CacheControl.noCache().getHeaderValue());
            headers.setContentType(MediaType.valueOf("text/html"));
            headers.set("Content-disposition", "attachment; filename=filename.xlsx");
            ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(media, headers, HttpStatus.OK);
            return responseEntity;
        }

以及exelExport类的代码:


    public Mono<Void> createDocument(boolean all) {
            InputStream inputStream = new ClassPathResource("Timesheet Template.xlsx").getInputStream();
            try {
                wb = WorkbookFactory.create(inputStream);
                Sheet sheet = wb.getSheetAt(0);
                Row row = sheet.getRow(1);
                Cell cell = row.getCell(3);
                if (cell == null)
                    cell = row.createCell(3);
                cell.setCellType(CellType.STRING);
                cell.setCellValue("a test");

                log.info("Created document");

                Flux<TimeKeepingEntry> entries = service.findByMonth(LocalDate.now().getMonth().getDisplayName(TextStyle.FULL, Locale.ENGLISH)).log("Excel Export - retrievedMonths");
                entries.subscribe();

                return entries.groupBy(TimeKeepingEntry::getDateOfMonth).flatMap(Flux::collectList).flatMap(timeKeepingEntries -> this.populateEntry(sheet, timeKeepingEntries)).then();
            } catch (IOException e) {
                log.error("Error Creating Document", e);
            }

            //should never get here
            return Mono.empty();
        }

    private void populateEntry(Sheet sheet, List<TimeKeepingEntry> timeKeepingEntries) {
            int rowNum = 0;
            for (int i = 0; i < timeKeepingEntries.size(); i++) {
                TimeKeepingEntry timeKeepingEntry = timeKeepingEntries.get(i);
                if (i == 0) {
                    rowNum = calculateFirstRow(timeKeepingEntry.getDay());
                }
                LocalDate date = timeKeepingEntry.getFullDate();
                Row row2 = sheet.getRow(rowNum);
                Cell cell2 = row2.getCell(1);
                cell2.setCellValue(date.toString());
                if (timeKeepingEntry.getDay().equals(DayOfWeek.FRIDAY.getDisplayName(TextStyle.FULL, Locale.ENGLISH))) {
                    rowNum = +2;
                } else {
                    rowNum++;
                }
            }
        }

由于从不执行populateEntry,因此永远不会更新工作簿。就像我说过的那样,我尝试了许多不同的方法,包括Mono.just和Mono.when,但是在webservice方法尝试返回文件之前,我似乎无法获得正确的组合来进行处理。

任何帮助都会很棒。

Edit1:显示理想的crateDocument方法。

public Mono<Void> createDocument(boolean all) {
        try {
            InputStream inputStream = new ClassPathResource("Timesheet Template.xlsx").getInputStream();
            wb = WorkbookFactory.create(inputStream);
            Sheet sheet = wb.getSheetAt(0);

            log.info("Created document");

            if (all) {
                //all entries
            } else {
                service.findByMonth(currentMonthName).log("Excel Export - retrievedMonths").collectSortedList(Comparator.comparing(TimeKeepingEntry::getDateOfMonth)).doOnNext(timeKeepingEntries -> {
                    this.populateEntry(sheet, timeKeepingEntries);
                });
            }
        } catch (IOException e) {
            log.error("Error Importing File", e);
        }
        return Mono.empty();
    }

2 个答案:

答案 0 :(得分:2)

您的Web服务的实现中存在几个问题。

何时subscribe

首先,在反应式编程中,通常必须尝试建立一个单个处理管道(通过调用MonoFlux运算符,并以{{ 1}}和Mono)。无论如何,您应该让框架执行Flux,或者至少在该管道结束时仅订阅一次。

在这里,您混合使用两种方法:您的subscribe方法正确返回了createDocument,但它也返回了Mono。更糟糕的是,订阅是在中间步骤完成的,在webservice方法中没有任何订阅整个管道的。

实际上,没有人看到管道的后半部分(从subscribe开始),因此它从未执行(这是一个懒惰的groupBy,也称为“冷”通量)。

混合同步和异步

另一个问题再次是混合两种方法的问题:Flux是惰性和异步的,但是您的Web服务是以命令式和同步方式编写的。

因此,代码从数据库启动异步Flux,立即返回到控制器并尝试从磁盘加载文件数据。

选项1:使控制器更加面向Flux

如果使用Spring MVC,您仍然可以编写这些命令式样式控制器,但仍会使用某些WebFlux。在这种情况下,您可以返回FluxMono,Spring MVC会将其转换为正确的异步Servlet构造。但这意味着您必须将FluxOutputStream处理变成bytes,并使用类似Mono的方法将其链接到文档编写Mono上。 / then / etc ...涉及更多。

选项2:将flatMap转换为命令性阻止代码

另一种选择是通过在Flux block()上调用createDocument()返回命令式和阻塞式。这将订阅它并等待它完成。之后,其余的命令性代码应该可以正常工作。

旁注

Mono有一个局限性:如果它导致超过groupBy个打开的组,则可以挂起。在这里,直到达到文件末尾为止,这些组才能关闭,但幸运的是,由于您只处理一个月的数据,因此Flux不会超过256个组。

答案 1 :(得分:1)

感谢@SimonBasie提供的指针,我的工作代码如下。

@GetMapping(value = API_BASE_PATH + "/download", produces = "application/vnd.ms-excel")
    public Mono<Resource> download() throws IOException {
        Flux<TimeKeepingEntry> createExcel = excelExport.createDocument(false);

        return createExcel.then(Mono.fromCallable(() -> {
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            excelExport.getWb().write(outputStream);
            return new ByteArrayResource(outputStream.toByteArray());
        }));
}

public Flux<TimeKeepingEntry> createDocument(boolean all) {
        Flux<TimeKeepingEntry> entries = null;
        try {
            InputStream inputStream = new ClassPathResource("Timesheet Template.xlsx").getInputStream();
            wb = WorkbookFactory.create(inputStream);
            Sheet sheet = wb.getSheetAt(0);

            log.info("Created document");

            if (all) {
                //all entries
            } else {
                entries = service.findByMonth(currentMonthName).log("Excel Export - retrievedMonths").sort(Comparator.comparing(TimeKeepingEntry::getDateOfMonth)).doOnNext(timeKeepingEntry-> {
                    this.populateEntry(sheet, timeKeepingEntry);
                });
            }
        } catch (IOException e) {
            log.error("Error Importing File", e);
        }
        return entries;
    }