Support conditional streaming with ResponseEntity<?>
Backport Bot / build (push) Waiting to run
Details
Build and Deploy Snapshot / Build and Deploy Snapshot (push) Waiting to run
Details
Build and Deploy Snapshot / Verify (push) Blocked by required conditions
Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:false version:17], map[id:ubuntu-latest name:Linux]) (push) Waiting to run
Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:21], map[id:ubuntu-latest name:Linux]) (push) Waiting to run
Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:23], map[id:ubuntu-latest name:Linux]) (push) Waiting to run
Details
Deploy Docs / Dispatch docs deployment (push) Waiting to run
Details
Backport Bot / build (push) Waiting to run
Details
Build and Deploy Snapshot / Build and Deploy Snapshot (push) Waiting to run
Details
Build and Deploy Snapshot / Verify (push) Blocked by required conditions
Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:false version:17], map[id:ubuntu-latest name:Linux]) (push) Waiting to run
Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:21], map[id:ubuntu-latest name:Linux]) (push) Waiting to run
Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:23], map[id:ubuntu-latest name:Linux]) (push) Waiting to run
Details
Deploy Docs / Dispatch docs deployment (push) Waiting to run
Details
Closes gh-35153
This commit is contained in:
parent
3dc22379a0
commit
9670388e0c
|
@ -770,16 +770,21 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
|||
private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
|
||||
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);
|
||||
|
||||
ResponseBodyEmitterReturnValueHandler responseBodyEmitterHandler =
|
||||
new ResponseBodyEmitterReturnValueHandler(getMessageConverters(),
|
||||
this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager,
|
||||
initViewResolvers(), initLocaleResolver());
|
||||
|
||||
HttpEntityMethodProcessor httpEntityMethodProcessor = new HttpEntityMethodProcessor(getMessageConverters(),
|
||||
this.contentNegotiationManager, this.requestResponseBodyAdvice, this.errorResponseInterceptors);
|
||||
|
||||
// Single-purpose return value types
|
||||
handlers.add(new ModelAndViewMethodReturnValueHandler());
|
||||
handlers.add(new ModelMethodProcessor());
|
||||
handlers.add(new ViewMethodReturnValueHandler());
|
||||
handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters(),
|
||||
this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager,
|
||||
initViewResolvers(), initLocaleResolver()));
|
||||
handlers.add(responseBodyEmitterHandler);
|
||||
handlers.add(new StreamingResponseBodyReturnValueHandler());
|
||||
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
|
||||
this.contentNegotiationManager, this.requestResponseBodyAdvice, this.errorResponseInterceptors));
|
||||
handlers.add(new ResponseEntityReturnValueHandler(httpEntityMethodProcessor, responseBodyEmitterHandler));
|
||||
handlers.add(new HttpHeadersReturnValueHandler());
|
||||
handlers.add(new CallableMethodReturnValueHandler());
|
||||
handlers.add(new DeferredResultMethodReturnValueHandler());
|
||||
|
|
|
@ -169,8 +169,11 @@ public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodRetur
|
|||
ResolvableType.forMethodParameter(returnType).getGeneric().resolve() :
|
||||
returnType.getParameterType();
|
||||
|
||||
return (bodyType != null && (ResponseBodyEmitter.class.isAssignableFrom(bodyType) ||
|
||||
this.reactiveHandler.isReactiveType(bodyType)));
|
||||
return (bodyType != null && supportsBodyType(bodyType));
|
||||
}
|
||||
|
||||
boolean supportsBodyType(Class<?> bodyType) {
|
||||
return (ResponseBodyEmitter.class.isAssignableFrom(bodyType) || this.reactiveHandler.isReactiveType(bodyType));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
/**
|
||||
* Handler for return values of type {@link org.springframework.http.ResponseEntity}
|
||||
* that delegates to one of the following:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link HttpEntityMethodProcessor} for responses with a concrete body value
|
||||
* <li>{@link ResponseBodyEmitterReturnValueHandler} for responses with a body
|
||||
* that is a {@link ResponseBodyEmitter} or an async/reactive type.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Use of this wrapper allows for late check in {@link #handleReturnValue} of
|
||||
* the type of the actual body value in case the method signature does not
|
||||
* provide enough information to decide via {@link #supportsReturnType}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
*/
|
||||
public class ResponseEntityReturnValueHandler implements HandlerMethodReturnValueHandler {
|
||||
|
||||
private final HttpEntityMethodProcessor httpEntityMethodProcessor;
|
||||
|
||||
private final ResponseBodyEmitterReturnValueHandler responseBodyEmitterHandler;
|
||||
|
||||
|
||||
public ResponseEntityReturnValueHandler(
|
||||
HttpEntityMethodProcessor httpEntityMethodProcessor,
|
||||
ResponseBodyEmitterReturnValueHandler responseBodyEmitterHandler) {
|
||||
|
||||
this.httpEntityMethodProcessor = httpEntityMethodProcessor;
|
||||
this.responseBodyEmitterHandler = responseBodyEmitterHandler;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean supportsReturnType(MethodParameter returnType) {
|
||||
return this.httpEntityMethodProcessor.supportsReturnType(returnType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleReturnValue(
|
||||
@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest request) throws Exception {
|
||||
|
||||
if (returnValue instanceof HttpEntity<?> httpEntity) {
|
||||
Object body = httpEntity.getBody();
|
||||
if (body != null) {
|
||||
if (this.responseBodyEmitterHandler.supportsBodyType(body.getClass())) {
|
||||
this.responseBodyEmitterHandler.handleReturnValue(returnValue, returnType, mavContainer, request);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.httpEntityMethodProcessor.handleReturnValue(returnValue, returnType, mavContainer, request);
|
||||
}
|
||||
|
||||
}
|
|
@ -51,6 +51,7 @@ import org.springframework.ui.Model;
|
|||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.SessionAttributes;
|
||||
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
|
||||
|
@ -205,6 +206,21 @@ class RequestMappingHandlerAdapterTests {
|
|||
assertMethodProcessorCount(RESOLVER_COUNT, INIT_BINDER_RESOLVER_COUNT, 1);
|
||||
}
|
||||
|
||||
@Test // gh-35153
|
||||
void responseEntityWithWildCardAndConditionalStream() throws Exception {
|
||||
HandlerMethod handlerMethod = handlerMethod(new SseController(), "handle", String.class);
|
||||
this.handlerAdapter.afterPropertiesSet();
|
||||
|
||||
this.request.setAsyncSupported(true);
|
||||
this.request.addParameter("q", "sse");
|
||||
|
||||
this.handlerAdapter.handle(this.request, this.response, handlerMethod);
|
||||
|
||||
assertThat(this.response.getStatus()).isEqualTo(200);
|
||||
assertThat(this.response.getHeader("Content-Type")).isEqualTo("text/event-stream");
|
||||
assertThat(this.response.getContentAsString()).isEqualTo("data:event 1\n\ndata:event 2\n\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
void modelAttributeAdvice() throws Exception {
|
||||
this.webAppContext.registerSingleton("maa", ModelAttributeAdvice.class);
|
||||
|
@ -377,6 +393,22 @@ class RequestMappingHandlerAdapterTests {
|
|||
}
|
||||
|
||||
|
||||
private static class SseController {
|
||||
|
||||
public ResponseEntity<?> handle(@RequestParam String q) throws IOException {
|
||||
if (q.equals("sse")) {
|
||||
SseEmitter emitter = new SseEmitter();
|
||||
emitter.send("event 1");
|
||||
emitter.send("event 2");
|
||||
emitter.complete();
|
||||
return ResponseEntity.ok().body(emitter);
|
||||
}
|
||||
return ResponseEntity.ok("text");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ControllerAdvice
|
||||
private static class ModelAttributeAdvice {
|
||||
|
||||
|
|
Loading…
Reference in New Issue