-
Notifications
You must be signed in to change notification settings - Fork 38.4k
Support for Flux<Rendering> and Flux<ModelAndView> for @RequestMapping handler methods #27652
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Here's an implementation that works, but probably needs a lot of polish: @Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CompositeViewRenderer implements HandlerResultHandler {
private final ViewResolver resolver;
public CompositeViewRenderer(ViewResolver resolver) {
this.resolver = resolver;
}
@Override
public boolean supports(HandlerResult result) {
if (Publisher.class.isAssignableFrom(result.getReturnType().toClass())) {
if (Rendering.class.isAssignableFrom(result.getReturnType().getGeneric(0).toClass())) {
return true;
}
}
return false;
}
@Override
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
String[] methodAnnotation = ((InvocableHandlerMethod) result.getHandler())
.getMethodAnnotation(RequestMapping.class).produces();
MediaType type = methodAnnotation.length > 0 ? MediaType.valueOf(methodAnnotation[0]) : MediaType.TEXT_HTML;
exchange.getResponse().getHeaders().setContentType(type);
boolean sse = MediaType.TEXT_EVENT_STREAM.includes(type);
@SuppressWarnings("unchecked")
Flux<Rendering> renderings = Flux.from((Publisher<Rendering>) result.getReturnValue());
final ExchangeWrapper wrapper = new ExchangeWrapper(exchange);
return exchange.getResponse().writeAndFlushWith(render(wrapper, renderings)
.map(buffers -> transform(exchange.getResponse().bufferFactory(), buffers, sse)));
}
private Publisher<DataBuffer> transform(DataBufferFactory factory, Publisher<DataBuffer> buffers, boolean sse) {
if (sse) {
buffers = Flux.from(buffers).map(buffer -> prefix(buffer, factory.allocateBuffer(buffer.capacity())));
}
// Add closing empty lines
return Flux.from(buffers).map(buffer -> buffer.write("\n\n", StandardCharsets.UTF_8));
}
private DataBuffer prefix(DataBuffer buffer, DataBuffer result) {
String body = buffer.toString(StandardCharsets.UTF_8);
body = "data:" + body.replace("\n", "\ndata:");
result.write(body, StandardCharsets.UTF_8);
DataBufferUtils.release(buffer);
return result;
}
private Flux<Flux<DataBuffer>> render(ExchangeWrapper exchange, Flux<Rendering> renderings) {
return renderings.flatMap(rendering -> render(exchange, rendering));
}
private Publisher<Flux<DataBuffer>> render(ExchangeWrapper exchange, Rendering rendering) {
Mono<View> view = null;
if (rendering.view() instanceof View) {
view = Mono.just((View) rendering.view());
} else {
view = resolver.resolveViewName((String) rendering.view(), exchange.getLocaleContext().getLocale());
}
return view.flatMap(actual -> actual.render(rendering.modelAttributes(), null, exchange))
.thenMany(Flux.defer(() -> exchange.release()));
}
static class ExchangeWrapper extends ServerWebExchangeDecorator {
private ResponseWrapper response;
protected ExchangeWrapper(ServerWebExchange delegate) {
super(delegate);
this.response = new ResponseWrapper(super.getResponse());
}
@Override
public ServerHttpResponse getResponse() {
return this.response;
}
public Flux<Flux<DataBuffer>> release() {
Flux<Flux<DataBuffer>> body = response.getBody();
this.response = new ResponseWrapper(super.getResponse());
return body;
}
}
static class ResponseWrapper extends ServerHttpResponseDecorator {
private Flux<Flux<DataBuffer>> body = Flux.empty();
public Flux<Flux<DataBuffer>> getBody() {
return body;
}
public ResponseWrapper(ServerHttpResponse delegate) {
super(delegate);
}
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
return writeAndFlushWith(Mono.just(body));
}
@Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
Flux<Flux<DataBuffer>> map = Flux.from(body).map(publisher -> Flux.from(publisher));
this.body = this.body.concatWith(map);
return Mono.empty();
}
}
} UPDATE: the above happens to work for the Spring Boot |
Here's a sample app with Webflux and Thymeleaf: https://github.com/dsyer/spring-todo-mvc. The tests are green, but it doesn't work in the browser because the wrong stuff is rendered most of the time. Something concurrent there and not thread safe in the app maybe? UPDATE: I fixed the browser rendering. All working fine now (but still could use some polish I'm sure). N.B. the |
A handler method returning
Flux<T>
whereT
is "something that can render a template" would be quite useful, especially in the light of the popularity and ease of use of things like the@hotwired/turbo
and htmx.org JavaScript modules. Those client libraries both have support for "streams" of HTML elements coming from the server, which get transcluded into the "main" page on the client. They also both support SSE streams containing HTML data. It would be nice to be able to render in both styles.Webflux and MVC currently have support for SSE. E.g. with Webflux you can return
Flux<String>
orFlux<ServerSentEvent>
from a handler method, but in both cases you have to render the data yourself. It would be handy to be able to delegate the rendering to a template engine, soRendering
(WebFlux) andModelAndView
(MVC) seem like a good fit. Thymeleaf also has some native (if a bit clumsy) support viaReactiveDataDriverContextVariable
, so there is some prior art there. You could see this feature request as a generalization of that.Simple example for Turbo on Webflux (for MVC just replace
Rendering
withModelAndView
) and SSE:with a template (e.g. in mustache but could be thymeleaf etc.):
The result would be an infinite stream, e.g.:
An example with HTMX and the HTML "stream" would be the same controller but with a different
produces
media type:with a template (e.g. in mustache but could be thymeleaf etc.):
The result would be a concatenation of the 2 divs:
The text was updated successfully, but these errors were encountered: