diff --git a/framework-docs/modules/ROOT/pages/appendix.adoc b/framework-docs/modules/ROOT/pages/appendix.adoc index 3b367aef678..dcfaacb27b3 100644 --- a/framework-docs/modules/ROOT/pages/appendix.adoc +++ b/framework-docs/modules/ROOT/pages/appendix.adoc @@ -81,6 +81,11 @@ resolvable otherwise. See {spring-framework-api}++/core/env/AbstractEnvironment.html#IGNORE_GETENV_PROPERTY_NAME++[`AbstractEnvironment`] for details. +| `spring.http.response.flush.enabled` +| Configures the Spring MVC `ServletServerHttpResponse` to allow flushing on the `OutputStream` +returned by `ServletServerHttpResponse#getBody()`. By default, such flush calls are ignored and +only `ServletServerHttpResponse#flush()` will actually flush the response to the network. + | `spring.jdbc.getParameterType.ignore` | Instructs Spring to ignore `java.sql.ParameterMetaData.getParameterType` completely. See the note in xref:data-access/jdbc/advanced.adoc#jdbc-batch-list[Batch Operations with a List of Objects]. diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java index e82ce036ede..4d8c1129bd5 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java @@ -23,20 +23,35 @@ import java.nio.charset.Charset; import jakarta.servlet.http.HttpServletResponse; import org.jspecify.annotations.Nullable; +import org.springframework.core.SpringProperties; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; /** * {@link ServerHttpResponse} implementation that is based on a {@link HttpServletResponse}. * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Brian Clozel * @since 3.0 */ public class ServletServerHttpResponse implements ServerHttpResponse { + /** + * System property that indicates whether {@code response.getBody().flush()} + * should effectively flush to the network. This is by default disabled, + * and developers must set the {@code "spring.http.response.flush.enabled"} + * {@link org.springframework.core.SpringProperties Spring property} to + * turn it on. + *

Applications should instead {@link #flush()} on the response directly. + */ + public static final String BODY_FLUSH_ENABLED = "spring.http.response.flush.enabled"; + + private final boolean flushEnabled = SpringProperties.getFlag(BODY_FLUSH_ENABLED); + private final HttpServletResponse servletResponse; private final HttpHeaders headers; @@ -86,11 +101,20 @@ public class ServletServerHttpResponse implements ServerHttpResponse { } } + /** + * Return the body of the message as an output stream. + *

By default, flushing the output stream has no effect + * (see {@link #BODY_FLUSH_ENABLED}) and should be performed + * using {@link #flush()} instead. + * @return the output stream body (never {@code null}) + * @throws IOException in case of I/O errors + */ @Override public OutputStream getBody() throws IOException { this.bodyUsed = true; writeHeaders(); - return this.servletResponse.getOutputStream(); + return (this.flushEnabled) ? this.servletResponse.getOutputStream() : + StreamUtils.nonFlushing(this.servletResponse.getOutputStream()); } @Override diff --git a/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java b/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java index d10e0d76655..89da9e61b4e 100644 --- a/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java @@ -19,9 +19,12 @@ package org.springframework.http.server; import java.nio.charset.StandardCharsets; import java.util.List; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.core.SpringProperties; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -29,8 +32,13 @@ import org.springframework.util.FileCopyUtils; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** + * Tests for {@link ServletServerHttpResponse}. * @author Arjen Poutsma * @author Rossen Stoyanchev * @author Juergen Hoeller @@ -120,4 +128,33 @@ class ServletServerHttpResponseTests { assertThat(mockResponse.getContentAsByteArray()).as("Invalid content written").isEqualTo(content); } + @Test + void skipFlushCallsOnOutputStream() throws Exception { + ServletOutputStream mockStream = mock(); + HttpServletResponse mockResponse = mock(); + when(mockResponse.getOutputStream()).thenReturn(mockStream); + + this.response = new ServletServerHttpResponse(mockResponse); + byte[] content = "Hello World".getBytes(StandardCharsets.UTF_8); + FileCopyUtils.copy(content, response.getBody()); + response.getBody().flush(); + verify(mockStream, never()).flush(); + } + + @Test + void appliesFlushCallsOnOutputStream() throws Exception { + SpringProperties.setProperty(ServletServerHttpResponse.BODY_FLUSH_ENABLED, Boolean.TRUE.toString()); + ServletOutputStream mockStream = mock(); + HttpServletResponse mockResponse = mock(); + when(mockResponse.getOutputStream()).thenReturn(mockStream); + + this.response = new ServletServerHttpResponse(mockResponse); + byte[] content = "Hello World".getBytes(StandardCharsets.UTF_8); + FileCopyUtils.copy(content, response.getBody()); + response.getBody().flush(); + verify(mockStream).flush(); + + SpringProperties.setProperty(ServletServerHttpResponse.BODY_FLUSH_ENABLED, null); + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpHeadersReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpHeadersReturnValueHandler.java index 3ba086d1c96..55880cd7aee 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpHeadersReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpHeadersReturnValueHandler.java @@ -55,7 +55,7 @@ public class HttpHeadersReturnValueHandler implements HandlerMethodReturnValueHa Assert.state(servletResponse != null, "No HttpServletResponse"); ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(servletResponse); outputMessage.getHeaders().putAll(headers); - outputMessage.getBody(); // flush headers + outputMessage.flush(); // flush headers } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandler.java index efd4af42b9b..55fe4c47fd2 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandler.java @@ -16,7 +16,6 @@ package org.springframework.web.servlet.mvc.method.annotation; -import java.io.OutputStream; import java.util.concurrent.Callable; import jakarta.servlet.ServletRequest; @@ -89,26 +88,26 @@ public class StreamingResponseBodyReturnValueHandler implements HandlerMethodRet Assert.isInstanceOf(StreamingResponseBody.class, returnValue, "StreamingResponseBody expected"); StreamingResponseBody streamingBody = (StreamingResponseBody) returnValue; - Callable callable = new StreamingResponseBodyTask(outputMessage.getBody(), streamingBody); + Callable callable = new StreamingResponseBodyTask(outputMessage, streamingBody); WebAsyncUtils.getAsyncManager(webRequest).startCallableProcessing(callable, mavContainer); } private static class StreamingResponseBodyTask implements Callable { - private final OutputStream outputStream; + private final ServerHttpResponse outputMessage; private final StreamingResponseBody streamingBody; - public StreamingResponseBodyTask(OutputStream outputStream, StreamingResponseBody streamingBody) { - this.outputStream = outputStream; + public StreamingResponseBodyTask(ServerHttpResponse outputMessage, StreamingResponseBody streamingBody) { + this.outputMessage = outputMessage; this.streamingBody = streamingBody; } @Override public Void call() throws Exception { - this.streamingBody.writeTo(this.outputStream); - this.outputStream.flush(); + this.streamingBody.writeTo(this.outputMessage.getBody()); + this.outputMessage.flush(); return null; } }