How to serve files/PDF files the reactive way in s

2020-07-16 08:45发布

问题:

I have the following endpoint code to serve PDF files.

@RequestMapping
ResponseEntity<byte[]> getPDF() {
  File file = ...;
  byte[] contents = null;
  try {
    try (FileInputStream fis = new FileInputStream(file)) {
      contents = new byte[(int) file.length()];
      fis.read(contents);
    }
  } catch(Exception e) {
    // error handling
  }
  HttpHeaders headers = new HttpHeaders();
  headers.setContentDispositionFormData(file.getName(), file.getName());
  headeres.setCacheControl("must-revalidate, post-check=0, pre-check=0");
  return new ResponseEntity<>(contents, headers, HttpStatus.OK);
}

How can I convert above into a reactive type Flux/Mono and DataBuffer.

I have check DataBufferUtils but It doesn't seem to offer what I needed. I didn't find any example either.

回答1:

The easiest way to achieve that would be with a Resource.

@GetMapping(path = "/pdf", produces = "application/pdf")
ResponseEntity<Resource> getPDF() {
  Resource pdfFile = ...;
  HttpHeaders headers = new HttpHeaders();
  headers.setContentDispositionFormData(file.getName(), file.getName());
  return ResponseEntity
    .ok().cacheControl(CacheControl.noCache())
    .headers(headers).body(resource);
}

Note that DataBufferUtils has some useful methods there that convert an InputStream to a Flux<DataBuffer>, like DataBufferUtils#read(). But dealing with a Resource is still superior.



回答2:

Below is the code to return the attachment as byte stream:

@GetMapping(
        path = "api/v1/attachment",
        produces = APPLICATION_OCTET_STREAM_VALUE
)
public Mono<byte[]> getAttachment(String url) {
    return rest.get()
            .uri(url)
            .exchange()
            .flatMap(response -> response.toEntity(byte[].class));
}

This approach is very simple but the disadvantage is it will the load the entire attachment into memory. If the file size is larger, then it will be a problem.

To overcome we can use DataBuffer which will send the data in chunks. This is an efficient solution and it will work for any large size file. Below is the modified code using DataBuffer:

@GetMapping(
        path = "api/v1/attachment",
        produces = APPLICATION_OCTET_STREAM_VALUE
)
public Flux<DataBuffer> getAttachment(String url) {
    return rest.get()
            .uri(url)
            .exchange()
            .flatMapMany(response -> response.toEntity(DataBuffer.class));
}

In this way, we can send attachments in a reactive fashion.



回答3:

Same Problem with me.

I use Webflux Spring WebClient

I write style RouterFunction

My solution below,

ETaxServiceClient.java

final WebClient defaultWebClient;


public Mono<byte[]> eTaxPdf(String id) {
    return defaultWebClient
            .get()
            .uri("-- URL PDF File --")
            .accept(MediaType.APPLICATION_OCTET_STREAM)
            .exchange()
            .log("eTaxPdf -> call other service")
            .flatMap(response -> response.toEntity(byte[].class))
            .flatMap(responseEntity -> Mono.just(Objects.requireNonNull(responseEntity.getBody())));
}

ETaxHandle.java

@NotNull
public Mono<ServerResponse> eTaxPdf(ServerRequest sr) {
    Consumer<HttpHeaders> headers = httpHeaders -> {
        httpHeaders.setCacheControl(CacheControl.noCache());
        httpHeaders.setContentDisposition(
                ContentDisposition.builder("inline")
                        .filename(sr.pathVariable("id") + ".pdf")
                        .build()
        );
    };
    return successPDF(eTaxServiceClient
            .eTaxPdf(sr.pathVariable("id"))
            .switchIfEmpty(Mono.empty()), headers);
}

ETaxRouter.java

@Bean
public RouterFunction<ServerResponse> routerFunctionV1(ETaxHandle handler) {
    return route()
            .path("/api/v1/e-tax-invoices", builder -> builder
                    .GET("/{id}", handler::eTaxPdf)
            )
            .build();
}

CommonHandler.java

Mono<ServerResponse> successPDF(Mono<?> mono, Consumer<HttpHeaders> headers) {
    return ServerResponse.ok()
            .headers(headers)
            .contentType(APPLICATION_PDF)
            .body(mono.map(m -> m)
                    .subscribeOn(Schedulers.elastic()), byte[].class);
}

Result: Successfully displayed on the browser.

Work for me.