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