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() {
|
private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
|
||||||
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);
|
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
|
// Single-purpose return value types
|
||||||
handlers.add(new ModelAndViewMethodReturnValueHandler());
|
handlers.add(new ModelAndViewMethodReturnValueHandler());
|
||||||
handlers.add(new ModelMethodProcessor());
|
handlers.add(new ModelMethodProcessor());
|
||||||
handlers.add(new ViewMethodReturnValueHandler());
|
handlers.add(new ViewMethodReturnValueHandler());
|
||||||
handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters(),
|
handlers.add(responseBodyEmitterHandler);
|
||||||
this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager,
|
|
||||||
initViewResolvers(), initLocaleResolver()));
|
|
||||||
handlers.add(new StreamingResponseBodyReturnValueHandler());
|
handlers.add(new StreamingResponseBodyReturnValueHandler());
|
||||||
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
|
handlers.add(new ResponseEntityReturnValueHandler(httpEntityMethodProcessor, responseBodyEmitterHandler));
|
||||||
this.contentNegotiationManager, this.requestResponseBodyAdvice, this.errorResponseInterceptors));
|
|
||||||
handlers.add(new HttpHeadersReturnValueHandler());
|
handlers.add(new HttpHeadersReturnValueHandler());
|
||||||
handlers.add(new CallableMethodReturnValueHandler());
|
handlers.add(new CallableMethodReturnValueHandler());
|
||||||
handlers.add(new DeferredResultMethodReturnValueHandler());
|
handlers.add(new DeferredResultMethodReturnValueHandler());
|
||||||
|
|
|
@ -169,8 +169,11 @@ public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodRetur
|
||||||
ResolvableType.forMethodParameter(returnType).getGeneric().resolve() :
|
ResolvableType.forMethodParameter(returnType).getGeneric().resolve() :
|
||||||
returnType.getParameterType();
|
returnType.getParameterType();
|
||||||
|
|
||||||
return (bodyType != null && (ResponseBodyEmitter.class.isAssignableFrom(bodyType) ||
|
return (bodyType != null && supportsBodyType(bodyType));
|
||||||
this.reactiveHandler.isReactiveType(bodyType)));
|
}
|
||||||
|
|
||||||
|
boolean supportsBodyType(Class<?> bodyType) {
|
||||||
|
return (ResponseBodyEmitter.class.isAssignableFrom(bodyType) || this.reactiveHandler.isReactiveType(bodyType));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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.ControllerAdvice;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
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.ResponseBody;
|
||||||
import org.springframework.web.bind.annotation.SessionAttributes;
|
import org.springframework.web.bind.annotation.SessionAttributes;
|
||||||
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
|
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
|
||||||
|
@ -205,6 +206,21 @@ class RequestMappingHandlerAdapterTests {
|
||||||
assertMethodProcessorCount(RESOLVER_COUNT, INIT_BINDER_RESOLVER_COUNT, 1);
|
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
|
@Test
|
||||||
void modelAttributeAdvice() throws Exception {
|
void modelAttributeAdvice() throws Exception {
|
||||||
this.webAppContext.registerSingleton("maa", ModelAttributeAdvice.class);
|
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
|
@ControllerAdvice
|
||||||
private static class ModelAttributeAdvice {
|
private static class ModelAttributeAdvice {
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue