我有一个 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 应用程序 - 这很好。但难道不能提供单一通量端点吗?
您遇到的错误 (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>
,而不会遇到任何异常。
对于 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是运行示例。下面是它的样子