在 Spring MVC 应用程序中提供 Flux 端点(PDF 下载)

问题描述 投票:0回答:2

我有一个 Spring Boot 3.1 MVC 应用程序 (Tomcat),我需要在其中添加一个返回 PDF 的端点。该 PDF 本身是从内部 HTTP 服务器获取的。我必须从数据库中读取 PDF 的 URL。

现在我想提供带有 Flux<> 的 PDF,并避免在从控制器返回之前将整个文件缓冲在内存中。这可能吗?如何实现?

我已经尝试过这个:

马夫:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

Web客户端配置:

@Configuration
public class WebClientConfig {
    @Value("${internal-server.url}")
    private String baseUrl;

    @Bean
    public WebClient webClient(WebClient.Builder webClientBuilder) {
        return webClientBuilder.baseUrl(baseUrl).build();
    }
}

控制器:

@GetMapping(path = "/{id}/download-flux", produces = MediaType.APPLICATION_PDF_VALUE)
public Flux<DataBuffer> downloadPdf(@PathVariable final int id) {
    final Report report = reportService.getReport(id);
    return webClient.get().uri("/" + report.getPath()).retrieve().bodyToFlux(DataBuffer.class);
}

甚至这个控制器方法:

@GetMapping(path = "/{id}/download-flux", produces = MediaType.APPLICATION_PDF_VALUE)
public Mono<ResponseEntity<Flux<DataBuffer>>> downloadPdf(@PathVariable final int id) {
    final Report report = reportService.getReport(id);
    return webClient.get()
                    .uri("/" + report.getPath())
                    .accept(MediaType.APPLICATION_PDF)
                    .exchange()
                    .map(response -> response.bodyToFlux(DataBuffer.class))
                    .map(ResponseEntity::ok);
}

但是我总是会遇到这个错误:

org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class org.springframework.web.servlet.mvc.method.annotation.ReactiveTypeHandler$CollectedValuesList] with preset Content-Type 'null'
    at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:319)
    at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:194)
    at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:78)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:136)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1081)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)

我能做什么?我知道,我的应用程序仍然是 MVC 应用程序 - 这很好。但难道不能提供单一通量端点吗?

java spring-boot spring-mvc spring-webflux
2个回答
0
投票

您遇到的错误 (HttpMessageNotWritableException) 通常表明 Spring 无法为从控制器方法返回的响应对象找到合适的消息转换器。发生这种情况是因为从控制器返回的 Flux 没有兼容的转换器来将其转换为 HTTP 响应。

要提供 PDF 作为 Flux 而不在内存中缓冲完整文件,您需要确保在 Spring 应用程序中注册了合适的消息转换器来处理这种情况。

您可以做的是 1. 创建自定义 MessageConverter 并 2. 注册自定义 MessageConverter

这是一个基本示例:

import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.HttpMessageWriter;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.Map;

@Component
public class PdfFluxMessageWriter implements HttpMessageWriter<Flux<DataBuffer>> {

    @Override
    public List<MediaType> getWritableMediaTypes() {
        return List.of(MediaType.APPLICATION_PDF);
    }

    @Override
    public boolean canWrite(ResolvableType resolvableType, MediaType mediaType) {
        return resolvableType.getRawClass() == Flux.class && mediaType.isCompatibleWith(MediaType.APPLICATION_PDF);
    }

    @Override
    public Mono<Void> write(Publisher<? extends Flux<DataBuffer>> publisher, ResolvableType resolvableType, MediaType mediaType, ReactiveHttpOutputMessage reactiveHttpOutputMessage, Map<String, Object> map) {
        Flux<DataBuffer> flux = Flux.from((Publisher<? extends Flux<DataBuffer>>) publisher);
        return reactiveHttpOutputMessage.writeWith(flux);
    }
}

PdfFluxMessageWriter
是专门用于
HttpMessageWriter
的自定义
Flux<DataBuffer>
,它将数据缓冲区直接写入 HTTP 响应输出消息。

实现自定义消息转换器后,请确保它已在您的 Spring 应用程序上下文中注册。 Spring 应该自动检测并使用它来将

Flux<DataBuffer>
响应转换为 HTTP 响应。

有了这个,您应该能够从控制器方法返回

Flux<DataBuffer>
,而不会遇到任何异常。


0
投票

对于 MVC,使用 void return 会更明智,这样就不需要放入任何转换器。这样代码会运行得更快,例如下面的例子

@SneakyThrows
@GetMapping(path = "/download2", produces = MediaType.APPLICATION_PDF_VALUE)
public void downloadPdf2(HttpServletResponse response) {
    log.info("returning flux...");
    Flux<DataBuffer> flux = webClient.get().uri("https://files.testfile.org/PDF/100MB-TESTFILE.ORG.pdf")
            .retrieve()
            .bodyToFlux(DataBuffer.class);
    DataBufferUtils
            .write(flux, response.getOutputStream())
            .map(DataBufferUtils::release)
            .blockLast();
}

@SneakyThrows
@GetMapping(path = "/download3", produces = MediaType.APPLICATION_PDF_VALUE)
public void downloadPdf3(HttpServletResponse response) {
    URL url = new URL("https://files.testfile.org/PDF/100MB-TESTFILE.ORG.pdf");
    URLConnection connection = url.openConnection();
    InputStream is = connection.getInputStream();
    copyLarge(is, response.getOutputStream());
}

public static long copyLarge(final InputStream input, final OutputStream output)
        throws IOException {
    byte[] buffer = new byte[64 * 1024 * 1024];
    long count = 0;
    int n = 0;
    while (-1 != (n = input.read(buffer))) {
        output.write(buffer, 0, n);
        if (count % 50 == 0) {
            output.flush();
            log.info("flushed");
        }
        count += n;
    }
    output.flush();
    return count;
}

我尝试使用 flush 但性能与 IOUtils.copyLarge 相同。这是运行示例。顺便说一句,我做了一些性能测试

downloadPdf3(HttpServletResponse) executed in 8013ms
downloadPdf2(HttpServletResponse) executed in 9477ms

看起来旧的 URLConnection 在 8 秒内工作,在 9 秒内变化。在我当地。


我建议转向netty,希望用spring结构不会是一个大问题。下面仅是 WebFlux 示例

@GetMapping(path = "/download-flux", produces = MediaType.APPLICATION_PDF_VALUE)
public Flux<byte[]> downloadPdf() {
    log.info("returning flux...");
    return webClient.get().uri("https://files.testfile.org/PDF/10MB-TESTFILE.ORG.pdf")
            .retrieve()
            .bodyToFlux(byte[].class);
}

here是运行示例。下面是它的样子

© www.soinside.com 2019 - 2024. All rights reserved.