Skip to content

Commit 9670388

Browse files
committed
Support conditional streaming with ResponseEntity<?>
Closes gh-35153
1 parent 3dc2237 commit 9670388

File tree

4 files changed

+129
-7
lines changed

4 files changed

+129
-7
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -770,16 +770,21 @@ private List<HandlerMethodArgumentResolver> getDefaultInitBinderArgumentResolver
770770
private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
771771
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);
772772

773+
ResponseBodyEmitterReturnValueHandler responseBodyEmitterHandler =
774+
new ResponseBodyEmitterReturnValueHandler(getMessageConverters(),
775+
this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager,
776+
initViewResolvers(), initLocaleResolver());
777+
778+
HttpEntityMethodProcessor httpEntityMethodProcessor = new HttpEntityMethodProcessor(getMessageConverters(),
779+
this.contentNegotiationManager, this.requestResponseBodyAdvice, this.errorResponseInterceptors);
780+
773781
// Single-purpose return value types
774782
handlers.add(new ModelAndViewMethodReturnValueHandler());
775783
handlers.add(new ModelMethodProcessor());
776784
handlers.add(new ViewMethodReturnValueHandler());
777-
handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters(),
778-
this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager,
779-
initViewResolvers(), initLocaleResolver()));
785+
handlers.add(responseBodyEmitterHandler);
780786
handlers.add(new StreamingResponseBodyReturnValueHandler());
781-
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
782-
this.contentNegotiationManager, this.requestResponseBodyAdvice, this.errorResponseInterceptors));
787+
handlers.add(new ResponseEntityReturnValueHandler(httpEntityMethodProcessor, responseBodyEmitterHandler));
783788
handlers.add(new HttpHeadersReturnValueHandler());
784789
handlers.add(new CallableMethodReturnValueHandler());
785790
handlers.add(new DeferredResultMethodReturnValueHandler());

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,11 @@ public boolean supportsReturnType(MethodParameter returnType) {
169169
ResolvableType.forMethodParameter(returnType).getGeneric().resolve() :
170170
returnType.getParameterType();
171171

172-
return (bodyType != null && (ResponseBodyEmitter.class.isAssignableFrom(bodyType) ||
173-
this.reactiveHandler.isReactiveType(bodyType)));
172+
return (bodyType != null && supportsBodyType(bodyType));
173+
}
174+
175+
boolean supportsBodyType(Class<?> bodyType) {
176+
return (ResponseBodyEmitter.class.isAssignableFrom(bodyType) || this.reactiveHandler.isReactiveType(bodyType));
174177
}
175178

176179
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.servlet.mvc.method.annotation;
18+
19+
import org.springframework.core.MethodParameter;
20+
import org.springframework.http.HttpEntity;
21+
import org.springframework.lang.Nullable;
22+
import org.springframework.web.context.request.NativeWebRequest;
23+
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
24+
import org.springframework.web.method.support.ModelAndViewContainer;
25+
26+
/**
27+
* Handler for return values of type {@link org.springframework.http.ResponseEntity}
28+
* that delegates to one of the following:
29+
*
30+
* <ul>
31+
* <li>{@link HttpEntityMethodProcessor} for responses with a concrete body value
32+
* <li>{@link ResponseBodyEmitterReturnValueHandler} for responses with a body
33+
* that is a {@link ResponseBodyEmitter} or an async/reactive type.
34+
* </ul>
35+
*
36+
* <p>Use of this wrapper allows for late check in {@link #handleReturnValue} of
37+
* the type of the actual body value in case the method signature does not
38+
* provide enough information to decide via {@link #supportsReturnType}.
39+
*
40+
* @author Rossen Stoyanchev
41+
* @since 7.0
42+
*/
43+
public class ResponseEntityReturnValueHandler implements HandlerMethodReturnValueHandler {
44+
45+
private final HttpEntityMethodProcessor httpEntityMethodProcessor;
46+
47+
private final ResponseBodyEmitterReturnValueHandler responseBodyEmitterHandler;
48+
49+
50+
public ResponseEntityReturnValueHandler(
51+
HttpEntityMethodProcessor httpEntityMethodProcessor,
52+
ResponseBodyEmitterReturnValueHandler responseBodyEmitterHandler) {
53+
54+
this.httpEntityMethodProcessor = httpEntityMethodProcessor;
55+
this.responseBodyEmitterHandler = responseBodyEmitterHandler;
56+
}
57+
58+
59+
@Override
60+
public boolean supportsReturnType(MethodParameter returnType) {
61+
return this.httpEntityMethodProcessor.supportsReturnType(returnType);
62+
}
63+
64+
@Override
65+
public void handleReturnValue(
66+
@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer,
67+
NativeWebRequest request) throws Exception {
68+
69+
if (returnValue instanceof HttpEntity<?> httpEntity) {
70+
Object body = httpEntity.getBody();
71+
if (body != null) {
72+
if (this.responseBodyEmitterHandler.supportsBodyType(body.getClass())) {
73+
this.responseBodyEmitterHandler.handleReturnValue(returnValue, returnType, mavContainer, request);
74+
return;
75+
}
76+
}
77+
}
78+
79+
this.httpEntityMethodProcessor.handleReturnValue(returnValue, returnType, mavContainer, request);
80+
}
81+
82+
}

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.springframework.web.bind.annotation.ControllerAdvice;
5252
import org.springframework.web.bind.annotation.ModelAttribute;
5353
import org.springframework.web.bind.annotation.RequestBody;
54+
import org.springframework.web.bind.annotation.RequestParam;
5455
import org.springframework.web.bind.annotation.ResponseBody;
5556
import org.springframework.web.bind.annotation.SessionAttributes;
5657
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
@@ -205,6 +206,21 @@ void setReturnValueHandlers() {
205206
assertMethodProcessorCount(RESOLVER_COUNT, INIT_BINDER_RESOLVER_COUNT, 1);
206207
}
207208

209+
@Test // gh-35153
210+
void responseEntityWithWildCardAndConditionalStream() throws Exception {
211+
HandlerMethod handlerMethod = handlerMethod(new SseController(), "handle", String.class);
212+
this.handlerAdapter.afterPropertiesSet();
213+
214+
this.request.setAsyncSupported(true);
215+
this.request.addParameter("q", "sse");
216+
217+
this.handlerAdapter.handle(this.request, this.response, handlerMethod);
218+
219+
assertThat(this.response.getStatus()).isEqualTo(200);
220+
assertThat(this.response.getHeader("Content-Type")).isEqualTo("text/event-stream");
221+
assertThat(this.response.getContentAsString()).isEqualTo("data:event 1\n\ndata:event 2\n\n");
222+
}
223+
208224
@Test
209225
void modelAttributeAdvice() throws Exception {
210226
this.webAppContext.registerSingleton("maa", ModelAttributeAdvice.class);
@@ -377,6 +393,22 @@ public String handle(Model model) {
377393
}
378394

379395

396+
private static class SseController {
397+
398+
public ResponseEntity<?> handle(@RequestParam String q) throws IOException {
399+
if (q.equals("sse")) {
400+
SseEmitter emitter = new SseEmitter();
401+
emitter.send("event 1");
402+
emitter.send("event 2");
403+
emitter.complete();
404+
return ResponseEntity.ok().body(emitter);
405+
}
406+
return ResponseEntity.ok("text");
407+
}
408+
409+
}
410+
411+
380412
@ControllerAdvice
381413
private static class ModelAttributeAdvice {
382414

0 commit comments

Comments
 (0)