From 15e2f61d7854725e8f7c419dcf2242dbd9e0265d Mon Sep 17 00:00:00 2001 From: Milkdove Date: Tue, 7 Jan 2025 16:17:07 +0800 Subject: [PATCH] Add request temp file buffer (#3479) --- docs/modules/ROOT/partials/_configprops.adoc | 4 +- .../mvc/common/AbstractProxyExchange.java | 5 +++ .../gateway/server/mvc/common/MvcUtils.java | 12 ++++++ .../mvc/config/GatewayMvcProperties.java | 29 +++++++++++++ .../mvc/handler/RestClientProxyExchange.java | 41 ++++++++++++++++++- 5 files changed, 88 insertions(+), 3 deletions(-) diff --git a/docs/modules/ROOT/partials/_configprops.adoc b/docs/modules/ROOT/partials/_configprops.adoc index f1e590f265..db36c0b5fa 100644 --- a/docs/modules/ROOT/partials/_configprops.adoc +++ b/docs/modules/ROOT/partials/_configprops.adoc @@ -3,7 +3,7 @@ |spring.cloud.gateway.default-filters | | List of filter definitions that are applied to every route. |spring.cloud.gateway.discovery.locator.enabled | `+++false+++` | Flag that enables DiscoveryClient gateway integration. -|spring.cloud.gateway.discovery.locator.filters | | +|spring.cloud.gateway.discovery.locator.filters | | |spring.cloud.gateway.discovery.locator.include-expression | `+++true+++` | SpEL expression that will evaluate whether to include a service in gateway integration or not, defaults to: true. |spring.cloud.gateway.discovery.locator.lower-case-service-id | `+++false+++` | Option to lower case serviceId in predicates and filters, defaults to false. Useful with eureka when it automatically uppercases serviceId. so MYSERIVCE, would match /myservice/** |spring.cloud.gateway.discovery.locator.predicates | | @@ -130,6 +130,8 @@ |spring.cloud.gateway.mvc.routes-map | | Map of Routes. |spring.cloud.gateway.mvc.streaming-buffer-size | `+++16384+++` | Buffer size for streaming media mime-types. |spring.cloud.gateway.mvc.streaming-media-types | | Mime-types that are streaming. +|spring.cloud.gateway.mvc.file-buffer-enabled | true | Enables the temp file buffer. +|spring.cloud.gateway.mvc.file-buffer-size-threshold | 1MB | The size threshold which a request will be written to disk. |spring.cloud.gateway.mvc.transfer-encoding-normalization-request-headers-filter.enabled | `+++true+++` | Enables the transfer-encoding-normalization-request-headers-filter. |spring.cloud.gateway.mvc.weight-calculator-filter.enabled | `+++true+++` | Enables the weight-calculator-filter. |spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.enabled | `+++true+++` | If the XForwardedHeadersFilter is enabled. diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/AbstractProxyExchange.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/AbstractProxyExchange.java index 9cc4606be3..ada8796e63 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/AbstractProxyExchange.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/AbstractProxyExchange.java @@ -75,4 +75,9 @@ private int copyResponseBodyWithFlushing(InputStream inputStream, OutputStream o return totalReadBytes; } + + protected boolean isWriteClientBodyToFile(Request request) { + return properties.isFileBufferEnabled() && request.getServerRequest().servletRequest().getContentLength() >= properties.getFileBufferSizeThreshold().toBytes(); + } + } diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java index 5e635b714a..ed1fa9c7f8 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/common/MvcUtils.java @@ -17,6 +17,7 @@ package org.springframework.cloud.gateway.server.mvc.common; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -92,6 +93,11 @@ public abstract class MvcUtils { */ public static final String WEIGHT_ATTR = qualify("routeWeight"); + /** + * Client request body temp file. + */ + public static final String CLIENT_BODY_TMP_ATTR = qualify("clientBodyTempFile"); + private MvcUtils() { } @@ -242,6 +248,12 @@ public static void setRequestUrl(ServerRequest request, URI url) { request.servletRequest().setAttribute(GATEWAY_REQUEST_URL_ATTR, url); } + public static void setBodyTempFile(ServerRequest request, File file) { + request.attributes().put(GATEWAY_ROUTE_ID_ATTR, file); + request.servletRequest().setAttribute(GATEWAY_ROUTE_ID_ATTR, file); + } + + private record ByteArrayInputMessage(ServerRequest request, ByteArrayInputStream body) implements HttpInputMessage { @Override diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcProperties.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcProperties.java index 728ee4b8a9..ddeabf72ea 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcProperties.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcProperties.java @@ -29,6 +29,7 @@ import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.core.style.ToStringCreator; import org.springframework.http.MediaType; +import org.springframework.util.unit.DataSize; @ConfigurationProperties(GatewayMvcProperties.PREFIX) public class GatewayMvcProperties { @@ -66,6 +67,16 @@ public class GatewayMvcProperties { */ private int streamingBufferSize = 16384; + /** + * Temp file buffer for request. + */ + private boolean fileBufferEnabled = true; + + /** + * The size threshold which a request will be written to disk. + */ + private DataSize fileBufferSizeThreshold = DataSize.ofMegabytes(1L); + public List getRoutes() { return routes; } @@ -102,6 +113,22 @@ public void setStreamingBufferSize(int streamingBufferSize) { this.streamingBufferSize = streamingBufferSize; } + public boolean isFileBufferEnabled() { + return fileBufferEnabled; + } + + public void setFileBufferEnabled(boolean fileBufferEnabled) { + this.fileBufferEnabled = fileBufferEnabled; + } + + public DataSize getFileBufferSizeThreshold() { + return fileBufferSizeThreshold; + } + + public void setFileBufferSizeThreshold(DataSize fileBufferSizeThreshold) { + this.fileBufferSizeThreshold = fileBufferSizeThreshold; + } + @Override public String toString() { return new ToStringCreator(this).append("httpClient", httpClient) @@ -109,6 +136,8 @@ public String toString() { .append("routesMap", routesMap) .append("streamingMediaTypes", streamingMediaTypes) .append("streamingBufferSize", streamingBufferSize) + .append("fileBufferEnabled", fileBufferEnabled) + .append("fileBufferSizeThreshold", fileBufferSizeThreshold) .toString(); } diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/RestClientProxyExchange.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/RestClientProxyExchange.java index 46e9d0326b..1e847d8339 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/RestClientProxyExchange.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/handler/RestClientProxyExchange.java @@ -16,14 +16,18 @@ package org.springframework.cloud.gateway.server.mvc.handler; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import org.springframework.cloud.gateway.server.mvc.common.AbstractProxyExchange; import org.springframework.cloud.gateway.server.mvc.common.MvcUtils; import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties; +import org.springframework.core.io.FileSystemResource; import org.springframework.http.client.ClientHttpResponse; import org.springframework.util.StreamUtils; import org.springframework.web.client.RestClient; @@ -50,9 +54,25 @@ public ServerResponse exchange(Request request) { .uri(request.getUri()) .headers(httpHeaders -> httpHeaders.putAll(request.getHeaders())); if (isBodyPresent(request)) { - requestSpec.body(outputStream -> copyBody(request, outputStream)); + if (isWriteClientBodyToFile(request)) { + requestSpec.body(copyClientBodyToFile(request)); + } + else { + requestSpec.body(outputStream -> copyBody(request, outputStream)); + } + } + return requestSpec.exchange((clientRequest, clientResponse) -> { + ServerResponse serverResponse = doExchange(request, clientResponse); + clearTempFileIfExist(request); + return serverResponse; + }, false); + } + + private void clearTempFileIfExist(Request request) { + File tempFile = MvcUtils.getAttribute(request.getServerRequest(), MvcUtils.CLIENT_BODY_TMP_ATTR); + if (tempFile != null && tempFile.exists()) { + tempFile.delete(); } - return requestSpec.exchange((clientRequest, clientResponse) -> doExchange(request, clientResponse), false); } private static boolean isBodyPresent(Request request) { @@ -68,6 +88,23 @@ private static int copyBody(Request request, OutputStream outputStream) throws I return StreamUtils.copy(request.getServerRequest().servletRequest().getInputStream(), outputStream); } + private static FileSystemResource copyClientBodyToFile(Request request) { + File bodyTempFile = null; + try { + // TODO: customize temp dir + bodyTempFile = File.createTempFile("gateway_client_body", ".tmp"); + Files.copy(request.getServerRequest().servletRequest().getInputStream(), bodyTempFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + catch (IOException e) { + if (bodyTempFile != null && bodyTempFile.exists()) { + bodyTempFile.delete(); + } + throw new UncheckedIOException(e); + } + MvcUtils.setBodyTempFile(request.getServerRequest(), bodyTempFile); + return new FileSystemResource(bodyTempFile); + } + private ServerResponse doExchange(Request request, ClientHttpResponse clientResponse) throws IOException { InputStream body = clientResponse.getBody(); // put the body input stream in a request attribute so filters can read it.