diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index eb6f29d82865..42aff713ec67 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -66,6 +66,7 @@ * @author HaiTao Zhang * @author Victor Mandujano * @author Chris Bono + * @author Parviz Rozikov * @since 1.0.0 */ @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) @@ -370,6 +371,19 @@ public static class Tomcat { */ private int processorCache = 200; + /** + * Time to wait for another HTTP request before the connection is closed. When not + * set the connectionTimeout is used. When set to -1 there will be no timeout. + */ + private Duration keepAliveTimeout; + + /** + * Maximum number of HTTP requests that can be pipelined before the connection is + * closed. When set to 0 or 1, keep-alive and pipelining are disabled. When set to + * -1, an unlimited number of pipelined or keep-alive requests is allowed. + */ + private int maxKeepAliveRequests = 100; + /** * Comma-separated list of additional patterns that match jars to ignore for TLD * scanning. The special '?' and '*' characters can be used in the pattern to @@ -498,6 +512,22 @@ public void setProcessorCache(int processorCache) { this.processorCache = processorCache; } + public Duration getKeepAliveTimeout() { + return this.keepAliveTimeout; + } + + public void setKeepAliveTimeout(Duration keepAliveTimeout) { + this.keepAliveTimeout = keepAliveTimeout; + } + + public int getMaxKeepAliveRequests() { + return this.maxKeepAliveRequests; + } + + public void setMaxKeepAliveRequests(int maxKeepAliveRequests) { + this.maxKeepAliveRequests = maxKeepAliveRequests; + } + public List getAdditionalTldSkipPatterns() { return this.additionalTldSkipPatterns; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java index 0aa4510f3bf8..c0971cc06f0a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java @@ -56,6 +56,7 @@ * @author Dirk Deyne * @author Rafiullah Hamedy * @author Victor Mandujano + * @author Parviz Rozikov * @since 2.0.0 */ public class TomcatWebServerFactoryCustomizer @@ -108,6 +109,10 @@ public void customize(ConfigurableTomcatWebServerFactory factory) { .to((acceptCount) -> customizeAcceptCount(factory, acceptCount)); propertyMapper.from(tomcatProperties::getProcessorCache) .to((processorCache) -> customizeProcessorCache(factory, processorCache)); + propertyMapper.from(tomcatProperties::getKeepAliveTimeout).whenNonNull() + .to((keepAliveTimeout) -> customizeKeepAliveTimeout(factory, keepAliveTimeout)); + propertyMapper.from(tomcatProperties::getMaxKeepAliveRequests) + .to((maxKeepAliveRequests) -> customizeMaxKeepAliveRequests(factory, maxKeepAliveRequests)); propertyMapper.from(tomcatProperties::getRelaxedPathChars).as(this::joinCharacters).whenHasText() .to((relaxedChars) -> customizeRelaxedPathChars(factory, relaxedChars)); propertyMapper.from(tomcatProperties::getRelaxedQueryChars).as(this::joinCharacters).whenHasText() @@ -139,6 +144,26 @@ private void customizeProcessorCache(ConfigurableTomcatWebServerFactory factory, }); } + private void customizeKeepAliveTimeout(ConfigurableTomcatWebServerFactory factory, Duration keepAliveTimeout) { + factory.addConnectorCustomizers((connector) -> { + ProtocolHandler handler = connector.getProtocolHandler(); + if (handler instanceof AbstractProtocol) { + final AbstractProtocol protocol = (AbstractProtocol) handler; + protocol.setKeepAliveTimeout((int) keepAliveTimeout.toMillis()); + } + }); + } + + private void customizeMaxKeepAliveRequests(ConfigurableTomcatWebServerFactory factory, int maxKeepAliveRequests) { + factory.addConnectorCustomizers((connector) -> { + ProtocolHandler handler = connector.getProtocolHandler(); + if (handler instanceof AbstractHttp11Protocol) { + AbstractHttp11Protocol protocol = (AbstractHttp11Protocol) handler; + protocol.setMaxKeepAliveRequests(maxKeepAliveRequests); + } + }); + } + private void customizeMaxConnections(ConfigurableTomcatWebServerFactory factory, int maxConnections) { factory.addConnectorCustomizers((connector) -> { ProtocolHandler handler = connector.getProtocolHandler(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java index 64477313bf75..05ecf41df556 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java @@ -38,6 +38,7 @@ import org.apache.catalina.valves.AccessLogValve; import org.apache.catalina.valves.RemoteIpValve; import org.apache.coyote.AbstractProtocol; +import org.apache.tomcat.util.net.AbstractEndpoint; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; @@ -82,6 +83,7 @@ * @author HaiTao Zhang * @author Rafiullah Hamedy * @author Chris Bono + * @author Parviz Rozikov */ class ServerPropertiesTests { @@ -216,6 +218,31 @@ void testCustomizeTomcatMaxThreads() { assertThat(this.properties.getTomcat().getThreads().getMax()).isEqualTo(10); } + @Test + void testCustomizeTomcatKeepAliveTimeout() { + bind("server.tomcat.keep-alive-timeout", "30s"); + assertThat(this.properties.getTomcat().getKeepAliveTimeout()).hasSeconds(30); + } + + @Test + void testCustomizeTomcatKeepAliveTimeoutWithInfinite() { + bind("server.tomcat.keep-alive-timeout", "-1"); + assertThat(this.properties.getTomcat().getKeepAliveTimeout().toMillis()).isEqualTo(-1); + assertThat(this.properties.getTomcat().getKeepAliveTimeout()).hasMillis(-1); + } + + @Test + void customizeMaxKeepAliveRequests() { + bind("server.tomcat.max-keep-alive-requests", "200"); + assertThat(this.properties.getTomcat().getMaxKeepAliveRequests()).isEqualTo(200); + } + + @Test + void customizeMaxKeepAliveRequestsWithInfinite() { + bind("server.tomcat.max-keep-alive-requests", "-1"); + assertThat(this.properties.getTomcat().getMaxKeepAliveRequests()).isEqualTo(-1); + } + @Test void testCustomizeTomcatMinSpareThreads() { bind("server.tomcat.threads.min-spare", "10"); @@ -384,6 +411,14 @@ void tomcatUseRelativeRedirectsDefaultsToFalse() { assertThat(this.properties.getTomcat().isUseRelativeRedirects()).isFalse(); } + @Test + void tomcatMaxKeepAliveRequestsDefault() throws Exception { + AbstractEndpoint endpoint = (AbstractEndpoint) ReflectionTestUtils.getField(getDefaultProtocol(), + "endpoint"); + int defaultMaxKeepAliveRequests = (int) ReflectionTestUtils.getField(endpoint, "maxKeepAliveRequests"); + assertThat(this.properties.getTomcat().getMaxKeepAliveRequests()).isEqualTo(defaultMaxKeepAliveRequests); + } + @Test void jettyThreadPoolPropertyDefaultsShouldMatchServerDefault() { JettyServletWebServerFactory jettyFactory = new JettyServletWebServerFactory(0); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java index 4b6416b0a639..6a3742a17222 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java @@ -56,6 +56,7 @@ * @author Andrew McGhie * @author Rafiullah Hamedy * @author Victor Mandujano + * @author Parviz Rozikov */ class TomcatWebServerFactoryCustomizerTests { @@ -97,6 +98,29 @@ void customProcessorCache() { .isEqualTo(100)); } + @Test + void customKeepAliveTimeout() { + bind("server.tomcat.keep-alive-timeout=30ms"); + customizeAndRunServer((server) -> assertThat( + ((AbstractProtocol) server.getTomcat().getConnector().getProtocolHandler()).getKeepAliveTimeout()) + .isEqualTo(30)); + } + + @Test + void customMaxKeepAliveRequests() { + bind("server.tomcat.max-keep-alive-requests=-1"); + customizeAndRunServer((server) -> assertThat( + ((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler()) + .getMaxKeepAliveRequests()).isEqualTo(-1)); + } + + @Test + void defaultMaxKeepAliveRequests() { + customizeAndRunServer((server) -> assertThat( + ((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler()) + .getMaxKeepAliveRequests()).isEqualTo(100)); + } + @Test void unlimitedProcessorCache() { bind("server.tomcat.processor-cache=-1");