Move web server auto-configure classes into spring-boot-web-server

This commit is contained in:
Andy Wilkinson
2025-05-07 13:49:13 +01:00
committed by Phillip Webb
parent dee54a8b99
commit 4471c0e644
89 changed files with 360 additions and 436 deletions

View File

@@ -0,0 +1,390 @@
/*
* Copyright 2012-2025 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.boot.web.server.autoconfigure;
import java.net.InetAddress;
import java.nio.charset.Charset;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.boot.web.server.Compression;
import org.springframework.boot.web.server.Cookie;
import org.springframework.boot.web.server.Http2;
import org.springframework.boot.web.server.MimeMappings;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.servlet.Jsp;
import org.springframework.boot.web.server.servlet.Session;
import org.springframework.util.StringUtils;
import org.springframework.util.unit.DataSize;
/**
* {@link ConfigurationProperties @ConfigurationProperties} for a web server (e.g. port
* and path settings).
*
* @author Dave Syer
* @author Stephane Nicoll
* @author Andy Wilkinson
* @author Ivan Sopov
* @author Marcos Barbero
* @author Eddú Meléndez
* @author Quinten De Swaef
* @author Venil Noronha
* @author Aurélien Leboulanger
* @author Brian Clozel
* @author Olivier Lamy
* @author Chentao Qu
* @author Artsiom Yudovin
* @author Andrew McGhie
* @author Rafiullah Hamedy
* @author Dirk Deyne
* @author HaiTao Zhang
* @author Victor Mandujano
* @author Chris Bono
* @author Parviz Rozikov
* @author Florian Storz
* @author Michael Weidmann
* @author Lasse Wulff
* @since 1.0.0
*/
@ConfigurationProperties("server")
public class ServerProperties {
/**
* Server HTTP port.
*/
private Integer port;
/**
* Network address to which the server should bind.
*/
private InetAddress address;
@NestedConfigurationProperty
private final ErrorProperties error = new ErrorProperties();
/**
* Strategy for handling X-Forwarded-* headers.
*/
private ForwardHeadersStrategy forwardHeadersStrategy;
/**
* Value to use for the Server response header (if empty, no header is sent).
*/
private String serverHeader;
/**
* Maximum size of the HTTP request header. Refer to the documentation for your chosen
* embedded server for details of exactly how this limit is applied. For example,
* Netty applies the limit separately to each individual header in the request whereas
* Tomcat applies the limit to the combined size of the request line and all of the
* header names and values in the request.
*/
private DataSize maxHttpRequestHeaderSize = DataSize.ofKilobytes(8);
/**
* Type of shutdown that the server will support.
*/
private Shutdown shutdown = Shutdown.GRACEFUL;
@NestedConfigurationProperty
private Ssl ssl;
@NestedConfigurationProperty
private final Compression compression = new Compression();
/**
* Custom MIME mappings in addition to the default MIME mappings.
*/
private final MimeMappings mimeMappings = new MimeMappings();
@NestedConfigurationProperty
private final Http2 http2 = new Http2();
private final Servlet servlet = new Servlet();
private final Reactive reactive = new Reactive();
public Integer getPort() {
return this.port;
}
public void setPort(Integer port) {
this.port = port;
}
public InetAddress getAddress() {
return this.address;
}
public void setAddress(InetAddress address) {
this.address = address;
}
public String getServerHeader() {
return this.serverHeader;
}
public void setServerHeader(String serverHeader) {
this.serverHeader = serverHeader;
}
public DataSize getMaxHttpRequestHeaderSize() {
return this.maxHttpRequestHeaderSize;
}
public void setMaxHttpRequestHeaderSize(DataSize maxHttpRequestHeaderSize) {
this.maxHttpRequestHeaderSize = maxHttpRequestHeaderSize;
}
public Shutdown getShutdown() {
return this.shutdown;
}
public void setShutdown(Shutdown shutdown) {
this.shutdown = shutdown;
}
public ErrorProperties getError() {
return this.error;
}
public Ssl getSsl() {
return this.ssl;
}
public void setSsl(Ssl ssl) {
this.ssl = ssl;
}
public Compression getCompression() {
return this.compression;
}
public MimeMappings getMimeMappings() {
return this.mimeMappings;
}
public void setMimeMappings(Map<String, String> customMappings) {
customMappings.forEach(this.mimeMappings::add);
}
public Http2 getHttp2() {
return this.http2;
}
public Servlet getServlet() {
return this.servlet;
}
public Reactive getReactive() {
return this.reactive;
}
public ForwardHeadersStrategy getForwardHeadersStrategy() {
return this.forwardHeadersStrategy;
}
public void setForwardHeadersStrategy(ForwardHeadersStrategy forwardHeadersStrategy) {
this.forwardHeadersStrategy = forwardHeadersStrategy;
}
/**
* Servlet server properties.
*/
public static class Servlet {
/**
* Servlet context init parameters.
*/
private final Map<String, String> contextParameters = new HashMap<>();
/**
* Context path of the application.
*/
private String contextPath;
/**
* Display name of the application.
*/
private String applicationDisplayName = "application";
/**
* Whether to register the default Servlet with the container.
*/
private boolean registerDefaultServlet = false;
private final Encoding encoding = new Encoding();
@NestedConfigurationProperty
private final Jsp jsp = new Jsp();
@NestedConfigurationProperty
private final Session session = new Session();
public String getContextPath() {
return this.contextPath;
}
public void setContextPath(String contextPath) {
this.contextPath = cleanContextPath(contextPath);
}
private String cleanContextPath(String contextPath) {
String candidate = null;
if (StringUtils.hasLength(contextPath)) {
candidate = contextPath.strip();
}
if (StringUtils.hasText(candidate) && candidate.endsWith("/")) {
return candidate.substring(0, candidate.length() - 1);
}
return candidate;
}
public String getApplicationDisplayName() {
return this.applicationDisplayName;
}
public void setApplicationDisplayName(String displayName) {
this.applicationDisplayName = displayName;
}
public boolean isRegisterDefaultServlet() {
return this.registerDefaultServlet;
}
public void setRegisterDefaultServlet(boolean registerDefaultServlet) {
this.registerDefaultServlet = registerDefaultServlet;
}
public Map<String, String> getContextParameters() {
return this.contextParameters;
}
public Encoding getEncoding() {
return this.encoding;
}
public Jsp getJsp() {
return this.jsp;
}
public Session getSession() {
return this.session;
}
}
/**
* Reactive server properties.
*/
public static class Reactive {
private final Session session = new Session();
public Session getSession() {
return this.session;
}
public static class Session {
/**
* Session timeout. If a duration suffix is not specified, seconds will be
* used.
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration timeout = Duration.ofMinutes(30);
/**
* Maximum number of sessions that can be stored.
*/
private int maxSessions = 10000;
@NestedConfigurationProperty
private final Cookie cookie = new Cookie();
public Duration getTimeout() {
return this.timeout;
}
public void setTimeout(Duration timeout) {
this.timeout = timeout;
}
public int getMaxSessions() {
return this.maxSessions;
}
public void setMaxSessions(int maxSessions) {
this.maxSessions = maxSessions;
}
public Cookie getCookie() {
return this.cookie;
}
}
}
/**
* Strategies for supporting forward headers.
*/
public enum ForwardHeadersStrategy {
/**
* Use the underlying container's native support for forwarded headers.
*/
NATIVE,
/**
* Use Spring's support for handling forwarded headers.
*/
FRAMEWORK,
/**
* Ignore X-Forwarded-* headers.
*/
NONE
}
public static class Encoding {
/**
* Mapping of locale to charset for response encoding.
*/
private Map<Locale, Charset> mapping;
public Map<Locale, Charset> getMapping() {
return this.mapping;
}
public void setMapping(Map<Locale, Charset> mapping) {
this.mapping = mapping;
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Classes related to the auto-configuration of a web server.
*/
package org.springframework.boot.web.server.autoconfigure;

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2012-2025 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.boot.web.server.autoconfigure.reactive;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor;
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.ObjectUtils;
import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
/**
* {@link Configuration Configuration} for a reactive web server.
*
* @author Brian Clozel
* @author Scott Frederick
* @since 4.0.0
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ServerProperties.class)
@Import(ReactiveWebServerConfiguration.BeanPostProcessorsRegistrar.class)
public class ReactiveWebServerConfiguration {
@Bean
public ReactiveWebServerFactoryCustomizer reactiveWebServerFactoryCustomizer(ServerProperties serverProperties,
ObjectProvider<SslBundles> sslBundles) {
return new ReactiveWebServerFactoryCustomizer(serverProperties, sslBundles.getIfAvailable());
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework")
public ForwardedHeaderTransformer forwardedHeaderTransformer() {
return new ForwardedHeaderTransformer();
}
/**
* Registers a {@link WebServerFactoryCustomizerBeanPostProcessor}. Registered via
* {@link ImportBeanDefinitionRegistrar} for early registration.
*/
public static class BeanPostProcessorsRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
private ConfigurableListableBeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof ConfigurableListableBeanFactory listableBeanFactory) {
this.beanFactory = listableBeanFactory;
}
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
if (this.beanFactory == null) {
return;
}
registerSyntheticBeanIfMissing(registry, "webServerFactoryCustomizerBeanPostProcessor",
WebServerFactoryCustomizerBeanPostProcessor.class);
}
private <T> void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, String name,
Class<T> beanClass) {
if (ObjectUtils.isEmpty(this.beanFactory.getBeanNamesForType(beanClass, true, false))) {
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
beanDefinition.setSynthetic(true);
registry.registerBeanDefinition(name, beanDefinition);
}
}
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2012-2025 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.boot.web.server.autoconfigure.reactive;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
import org.springframework.boot.web.server.reactive.ConfigurableReactiveWebServerFactory;
import org.springframework.core.Ordered;
/**
* {@link WebServerFactoryCustomizer} to apply {@link ServerProperties} to reactive
* servers.
*
* @author Brian Clozel
* @author Yunkun Huang
* @author Scott Frederick
* @since 4.0.0
*/
public class ReactiveWebServerFactoryCustomizer
implements WebServerFactoryCustomizer<ConfigurableReactiveWebServerFactory>, Ordered {
private final ServerProperties serverProperties;
private final SslBundles sslBundles;
/**
* Create a new {@link ReactiveWebServerFactoryCustomizer} instance.
* @param serverProperties the server properties
*/
public ReactiveWebServerFactoryCustomizer(ServerProperties serverProperties) {
this(serverProperties, null);
}
/**
* Create a new {@link ReactiveWebServerFactoryCustomizer} instance.
* @param serverProperties the server properties
* @param sslBundles the SSL bundles
* @since 4.0.0
*/
public ReactiveWebServerFactoryCustomizer(ServerProperties serverProperties, SslBundles sslBundles) {
this.serverProperties = serverProperties;
this.sslBundles = sslBundles;
}
@Override
public int getOrder() {
return 0;
}
@Override
public void customize(ConfigurableReactiveWebServerFactory factory) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this.serverProperties::getPort).to(factory::setPort);
map.from(this.serverProperties::getAddress).to(factory::setAddress);
map.from(this.serverProperties::getSsl).to(factory::setSsl);
map.from(this.serverProperties::getCompression).to(factory::setCompression);
map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
map.from(this.serverProperties.getShutdown()).to(factory::setShutdown);
map.from(() -> this.sslBundles).to(factory::setSslBundles);
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Classes related to the auto-configuration of a reactive web server.
*/
package org.springframework.boot.web.server.autoconfigure.reactive;

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2012-2025 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.boot.web.server.autoconfigure.servlet;
import org.springframework.web.filter.ForwardedHeaderFilter;
/**
* Customizer for the auto-configured {@link ForwardedHeaderFilter}.
*
* @author Andy Wilkinson
* @since 4.0.0
*/
public interface ForwardedHeaderFilterCustomizer {
/**
* Customizes the given {@code filter}.
* @param filter the filter to customize
*/
void customize(ForwardedHeaderFilter filter);
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2012-2025 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.boot.web.server.autoconfigure.servlet;
import jakarta.servlet.DispatcherType;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingFilterBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.web.error.ErrorPageRegistrarBeanPostProcessor;
import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor;
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
import org.springframework.boot.web.server.servlet.CookieSameSiteSupplier;
import org.springframework.boot.web.server.servlet.WebListenerRegistrar;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.Ordered;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.ObjectUtils;
import org.springframework.web.filter.ForwardedHeaderFilter;
/**
* {@link Configuration Configuration} for a servlet web server.
*
* @author Phillip Webb
* @author Dave Syer
* @author Ivan Sopov
* @author Brian Clozel
* @author Stephane Nicoll
* @author Scott Frederick
* @since 4.0.0
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ServerProperties.class)
@Import(ServletWebServerConfiguration.BeanPostProcessorsRegistrar.class)
public class ServletWebServerConfiguration {
@Bean
ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties,
ObjectProvider<WebListenerRegistrar> webListenerRegistrars,
ObjectProvider<CookieSameSiteSupplier> cookieSameSiteSuppliers, ObjectProvider<SslBundles> sslBundles) {
return new ServletWebServerFactoryCustomizer(serverProperties, webListenerRegistrars.orderedStream().toList(),
cookieSameSiteSuppliers.orderedStream().toList(), sslBundles.getIfAvailable());
}
@Bean
@ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework")
@ConditionalOnMissingFilterBean(ForwardedHeaderFilter.class)
FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter(
ObjectProvider<ForwardedHeaderFilterCustomizer> customizerProvider) {
ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
customizerProvider.ifAvailable((customizer) -> customizer.customize(filter));
FilterRegistrationBean<ForwardedHeaderFilter> registration = new FilterRegistrationBean<>(filter);
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.ERROR);
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
/**
* Registers a {@link WebServerFactoryCustomizerBeanPostProcessor}. Registered via
* {@link ImportBeanDefinitionRegistrar} for early registration.
*/
static class BeanPostProcessorsRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
private ConfigurableListableBeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof ConfigurableListableBeanFactory listableBeanFactory) {
this.beanFactory = listableBeanFactory;
}
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
if (this.beanFactory == null) {
return;
}
registerSyntheticBeanIfMissing(registry, "webServerFactoryCustomizerBeanPostProcessor",
WebServerFactoryCustomizerBeanPostProcessor.class);
registerSyntheticBeanIfMissing(registry, "errorPageRegistrarBeanPostProcessor",
ErrorPageRegistrarBeanPostProcessor.class);
}
private <T> void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, String name,
Class<T> beanClass) {
if (ObjectUtils.isEmpty(this.beanFactory.getBeanNamesForType(beanClass, true, false))) {
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
beanDefinition.setSynthetic(true);
registry.registerBeanDefinition(name, beanDefinition);
}
}
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2012-2025 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.boot.web.server.autoconfigure.servlet;
import java.util.Collections;
import java.util.List;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
import org.springframework.boot.web.server.servlet.CookieSameSiteSupplier;
import org.springframework.boot.web.server.servlet.WebListenerRegistrar;
import org.springframework.core.Ordered;
import org.springframework.util.CollectionUtils;
/**
* {@link WebServerFactoryCustomizer} to apply {@link ServerProperties} and
* {@link WebListenerRegistrar WebListenerRegistrars} to servlet web servers.
*
* @author Brian Clozel
* @author Stephane Nicoll
* @author Olivier Lamy
* @author Yunkun Huang
* @author Scott Frederick
* @author Lasse Wulff
* @since 4.0.0
*/
public class ServletWebServerFactoryCustomizer
implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {
private final ServerProperties serverProperties;
private final List<WebListenerRegistrar> webListenerRegistrars;
private final List<CookieSameSiteSupplier> cookieSameSiteSuppliers;
private final SslBundles sslBundles;
public ServletWebServerFactoryCustomizer(ServerProperties serverProperties) {
this(serverProperties, Collections.emptyList(), Collections.emptyList(), null);
}
public ServletWebServerFactoryCustomizer(ServerProperties serverProperties,
List<WebListenerRegistrar> webListenerRegistrars, List<CookieSameSiteSupplier> cookieSameSiteSuppliers,
SslBundles sslBundles) {
this.serverProperties = serverProperties;
this.webListenerRegistrars = webListenerRegistrars;
this.cookieSameSiteSuppliers = cookieSameSiteSuppliers;
this.sslBundles = sslBundles;
}
@Override
public int getOrder() {
return 0;
}
@Override
public void customize(ConfigurableServletWebServerFactory factory) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this.serverProperties::getPort).to(factory::setPort);
map.from(this.serverProperties::getAddress).to(factory::setAddress);
map.from(this.serverProperties.getServlet()::getContextPath).to(factory::setContextPath);
map.from(this.serverProperties.getServlet()::getApplicationDisplayName).to(factory::setDisplayName);
map.from(this.serverProperties.getServlet()::isRegisterDefaultServlet).to(factory::setRegisterDefaultServlet);
map.from(this.serverProperties.getServlet()::getSession).to(factory::setSession);
map.from(this.serverProperties::getSsl).to(factory::setSsl);
map.from(this.serverProperties.getServlet()::getJsp).to(factory::setJsp);
map.from(this.serverProperties::getCompression).to(factory::setCompression);
map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader);
map.from(this.serverProperties.getServlet()::getContextParameters).to(factory::setInitParameters);
map.from(this.serverProperties.getShutdown()).to(factory::setShutdown);
map.from(() -> this.sslBundles).to(factory::setSslBundles);
map.from(() -> this.cookieSameSiteSuppliers)
.whenNot(CollectionUtils::isEmpty)
.to(factory::setCookieSameSiteSuppliers);
map.from(this.serverProperties::getMimeMappings).to(factory::addMimeMappings);
map.from(this.serverProperties.getServlet().getEncoding()::getMapping).to(factory::setLocaleCharsetMappings);
this.webListenerRegistrars.forEach((registrar) -> registrar.register(factory));
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Classes related to the auto-configuration of a servlet web server.
*/
package org.springframework.boot.web.server.autoconfigure.servlet;

View File

@@ -0,0 +1,217 @@
{
"properties": [
{
"name": "server.connection-timeout",
"type": "java.time.Duration",
"deprecation": {
"reason": "Each server behaves differently. Use server specific properties instead.",
"level": "error"
}
},
{
"name": "server.error.include-binding-errors",
"description": "When to include \"errors\" attribute.",
"defaultValue": "never"
},
{
"name": "server.error.include-exception",
"description": "Include the \"exception\" attribute.",
"defaultValue": false
},
{
"name": "server.error.include-message",
"description": "When to include the \"message\" attribute.",
"defaultValue": "never"
},
{
"name": "server.error.include-path",
"description": "When to include the \"path\" attribute.",
"defaultValue": "always"
},
{
"name": "server.error.include-stacktrace",
"description": "When to include the \"trace\" attribute.",
"defaultValue": "never"
},
{
"name": "server.error.path",
"description": "Path of the error controller",
"defaultValue": "/error"
},
{
"name": "server.error.whitelabel.enabled",
"description": "Whether to enable the default error page displayed in browsers in case of a server error.",
"defaultValue": true
},
{
"name": "server.max-http-header-size",
"deprecation": {
"replacement": "server.max-http-request-header-size",
"level": "error"
}
},
{
"name": "server.max-http-post-size",
"type": "java.lang.Integer",
"description": "Maximum size in bytes of the HTTP post content.",
"defaultValue": 0,
"deprecation": {
"reason": "Use dedicated property for each container.",
"level": "error"
}
},
{
"name": "server.port",
"defaultValue": 8080
},
{
"name": "server.reactive.session.cookie.domain",
"description": "Domain for the cookie."
},
{
"name": "server.reactive.session.cookie.http-only",
"description": "Whether to use \"HttpOnly\" cookies for the cookie."
},
{
"name": "server.reactive.session.cookie.max-age",
"description": "Maximum age of the cookie. If a duration suffix is not specified, seconds will be used. A positive value indicates when the cookie expires relative to the current time. A value of 0 means the cookie should expire immediately. A negative value means no \"Max-Age\"."
},
{
"name": "server.reactive.session.cookie.name",
"description": "Name for the cookie."
},
{
"name": "server.reactive.session.cookie.partitioned",
"description": "Whether the generated cookie carries the Partitioned attribute."
},
{
"name": "server.reactive.session.cookie.path",
"description": "Path of the cookie."
},
{
"name": "server.reactive.session.cookie.same-site",
"description": "SameSite setting for the cookie."
},
{
"name": "server.reactive.session.cookie.secure",
"description": "Whether to always mark the cookie as secure."
},
{
"name": "server.servlet.encoding.charset",
"type": "java.nio.charset.Charset",
"description": "Charset of HTTP requests and responses. Added to the Content-Type header if not set explicitly.",
"deprecation": {
"replacement": "spring.servlet.encoding.charset",
"level": "error"
}
},
{
"name": "server.servlet.encoding.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable http encoding support.",
"defaultValue": true,
"deprecation": {
"replacement": "spring.servlet.encoding.enabled",
"level": "error"
}
},
{
"name": "server.servlet.encoding.force",
"type": "java.lang.Boolean",
"description": "Whether to force the encoding to the configured charset on HTTP requests and responses.",
"defaultValue": false,
"deprecation": {
"replacement": "spring.servlet.encoding.force",
"level": "error"
}
},
{
"name": "server.servlet.encoding.force-request",
"type": "java.lang.Boolean",
"description": "Whether to force the encoding to the configured charset on HTTP requests. Defaults to true when force has not been specified.",
"defaultValue": true,
"deprecation": {
"replacement": "spring.servlet.encoding.force-request",
"level": "error"
}
},
{
"name": "server.servlet.encoding.force-response",
"type": "java.lang.Boolean",
"description": "Whether to force the encoding to the configured charset on HTTP responses.",
"defaultValue": false,
"deprecation": {
"replacement": "spring.servlet.encoding.force-response",
"level": "error"
}
},
{
"name": "server.servlet.jsp.class-name",
"description": "Class name of the servlet to use for JSPs. If registered is true and this class\n\t * is on the classpath then it will be registered.",
"defaultValue": "org.apache.jasper.servlet.JspServlet"
},
{
"name": "server.servlet.jsp.init-parameters",
"description": "Init parameters used to configure the JSP servlet."
},
{
"name": "server.servlet.path",
"type": "java.lang.String",
"description": "Path of the main dispatcher servlet.",
"defaultValue": "/",
"deprecation": {
"replacement": "spring.mvc.servlet.path",
"level": "error"
}
},
{
"name": "server.servlet.session.cookie.comment",
"description": "Comment for the cookie.",
"deprecation": {
"level": "error"
}
},
{
"name": "server.ssl.protocol",
"description": "SSL protocol to use.",
"defaultValue": "TLS"
},
{
"name": "server.ssl.server-name-bundles",
"description": "Mapping of host names to SSL bundles for SNI configuration."
},
{
"name": "server.ssl.trust-certificate",
"description": "Path to a PEM-encoded SSL certificate authority file."
},
{
"name": "server.ssl.trust-certificate-private-key",
"description": "Path to a PEM-encoded private key file for the SSL certificate authority."
},
{
"name": "server.ssl.trust-store",
"description": "Trust store that holds SSL certificates."
},
{
"name": "server.ssl.trust-store-password",
"description": "Password used to access the trust store."
},
{
"name": "server.ssl.trust-store-provider",
"description": "Provider for the trust store."
},
{
"name": "server.ssl.trust-store-type",
"description": "Type of the trust store."
},
{
"name": "server.use-forward-headers",
"type": "java.lang.Boolean",
"deprecation": {
"reason": "Replaced to support additional strategies.",
"replacement": "server.forward-headers-strategy",
"level": "error"
}
}
]
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2012-2025 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.boot.autoconfigure.web;
import java.net.InetAddress;
import java.util.Collections;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.boot.web.server.MimeMappings;
import org.springframework.boot.web.server.MimeMappings.Mapping;
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
import org.springframework.util.unit.DataSize;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ServerProperties}.
*
* @author Dave Syer
* @author Stephane Nicoll
* @author Andy Wilkinson
* @author Phillip Webb
* @author Eddú Meléndez
* @author Quinten De Swaef
* @author Venil Noronha
* @author Andrew McGhie
* @author HaiTao Zhang
* @author Rafiullah Hamedy
* @author Chris Bono
* @author Parviz Rozikov
* @author Lasse Wulff
* @author Moritz Halbritter
*/
class ServerPropertiesTests {
private final ServerProperties properties = new ServerProperties();
@Test
void testAddressBinding() throws Exception {
bind("server.address", "127.0.0.1");
assertThat(this.properties.getAddress()).isEqualTo(InetAddress.getByName("127.0.0.1"));
}
@Test
void testPortBinding() {
bind("server.port", "9000");
assertThat(this.properties.getPort().intValue()).isEqualTo(9000);
}
@Test
void testServerHeaderDefault() {
assertThat(this.properties.getServerHeader()).isNull();
}
@Test
void testServerHeader() {
bind("server.server-header", "Custom Server");
assertThat(this.properties.getServerHeader()).isEqualTo("Custom Server");
}
@Test
void testTrailingSlashOfContextPathIsRemoved() {
bind("server.servlet.context-path", "/foo/");
assertThat(this.properties.getServlet().getContextPath()).isEqualTo("/foo");
}
@Test
void testSlashOfContextPathIsDefaultValue() {
bind("server.servlet.context-path", "/");
assertThat(this.properties.getServlet().getContextPath()).isEmpty();
}
@Test
void testContextPathWithLeadingWhitespace() {
bind("server.servlet.context-path", " /assets");
assertThat(this.properties.getServlet().getContextPath()).isEqualTo("/assets");
}
@Test
void testContextPathWithTrailingWhitespace() {
bind("server.servlet.context-path", "/assets/copy/ ");
assertThat(this.properties.getServlet().getContextPath()).isEqualTo("/assets/copy");
}
@Test
void testContextPathWithLeadingAndTrailingWhitespace() {
bind("server.servlet.context-path", " /assets ");
assertThat(this.properties.getServlet().getContextPath()).isEqualTo("/assets");
}
@Test
void testContextPathWithLeadingAndTrailingWhitespaceAndContextWithSpace() {
bind("server.servlet.context-path", " /assets /copy/ ");
assertThat(this.properties.getServlet().getContextPath()).isEqualTo("/assets /copy");
}
@Test
void testDefaultMimeMapping() {
assertThat(this.properties.getMimeMappings()).isEmpty();
}
@Test
void testCustomizedMimeMapping() {
MimeMappings expectedMappings = new MimeMappings();
expectedMappings.add("mjs", "text/javascript");
bind("server.mime-mappings.mjs", "text/javascript");
assertThat(this.properties.getMimeMappings())
.containsExactly(expectedMappings.getAll().toArray(new Mapping[0]));
}
@Test
void testCustomizeMaxHttpRequestHeaderSize() {
bind("server.max-http-request-header-size", "1MB");
assertThat(this.properties.getMaxHttpRequestHeaderSize()).isEqualTo(DataSize.ofMegabytes(1));
}
@Test
void testCustomizeMaxHttpRequestHeaderSizeUseBytesByDefault() {
bind("server.max-http-request-header-size", "1024");
assertThat(this.properties.getMaxHttpRequestHeaderSize()).isEqualTo(DataSize.ofKilobytes(1));
}
private void bind(String name, String value) {
bind(Collections.singletonMap(name, value));
}
private void bind(Map<String, String> map) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(map);
new Binder(source).bind("server", Bindable.ofInstance(this.properties));
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2012-2025 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.boot.web.server.autoconfigure.reactive;
import java.net.InetAddress;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
import org.springframework.boot.web.server.reactive.ConfigurableReactiveWebServerFactory;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.assertArg;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ReactiveWebServerFactoryCustomizer}.
*
* @author Brian Clozel
* @author Yunkun Huang
* @author Scott Frederick
*/
class ReactiveWebServerFactoryCustomizerTests {
private final ServerProperties properties = new ServerProperties();
private final SslBundles sslBundles = new DefaultSslBundleRegistry();
private ReactiveWebServerFactoryCustomizer customizer;
@BeforeEach
void setup() {
this.customizer = new ReactiveWebServerFactoryCustomizer(this.properties, this.sslBundles);
}
@Test
void testCustomizeServerPort() {
ConfigurableReactiveWebServerFactory factory = mock(ConfigurableReactiveWebServerFactory.class);
this.properties.setPort(9000);
this.customizer.customize(factory);
then(factory).should().setPort(9000);
}
@Test
void testCustomizeServerAddress() {
ConfigurableReactiveWebServerFactory factory = mock(ConfigurableReactiveWebServerFactory.class);
InetAddress address = InetAddress.getLoopbackAddress();
this.properties.setAddress(address);
this.customizer.customize(factory);
then(factory).should().setAddress(address);
}
@Test
void testCustomizeServerSsl() {
ConfigurableReactiveWebServerFactory factory = mock(ConfigurableReactiveWebServerFactory.class);
Ssl ssl = mock(Ssl.class);
this.properties.setSsl(ssl);
this.customizer.customize(factory);
then(factory).should().setSsl(ssl);
then(factory).should().setSslBundles(this.sslBundles);
}
@Test
void whenShutdownPropertyIsSetThenShutdownIsCustomized() {
this.properties.setShutdown(Shutdown.GRACEFUL);
ConfigurableReactiveWebServerFactory factory = mock(ConfigurableReactiveWebServerFactory.class);
this.customizer.customize(factory);
then(factory).should().setShutdown(assertArg((shutdown) -> assertThat(shutdown).isEqualTo(Shutdown.GRACEFUL)));
}
}

View File

@@ -0,0 +1,217 @@
/*
* Copyright 2012-2025 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.boot.web.server.autoconfigure.servlet;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.boot.web.server.Cookie;
import org.springframework.boot.web.server.MimeMappings;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
import org.springframework.boot.web.server.servlet.Jsp;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.assertArg;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
/**
* Tests for {@link ServletWebServerFactoryCustomizer}.
*
* @author Brian Clozel
* @author Yunkun Huang
* @author Lasse Wulff
*/
class ServletWebServerFactoryCustomizerTests {
private final ServerProperties properties = new ServerProperties();
private ServletWebServerFactoryCustomizer customizer;
@BeforeEach
void setup() {
this.customizer = new ServletWebServerFactoryCustomizer(this.properties);
}
@Test
void testDefaultDisplayName() {
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
this.customizer.customize(factory);
then(factory).should().setDisplayName("application");
}
@Test
void testCustomizeDisplayName() {
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
this.properties.getServlet().setApplicationDisplayName("TestName");
this.customizer.customize(factory);
then(factory).should().setDisplayName("TestName");
}
@Test
void withNoCustomMimeMappingsThenEmptyMimeMappingsIsAdded() {
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
this.customizer.customize(factory);
ArgumentCaptor<MimeMappings> mimeMappingsCaptor = ArgumentCaptor.forClass(MimeMappings.class);
then(factory).should().addMimeMappings(mimeMappingsCaptor.capture());
MimeMappings mimeMappings = mimeMappingsCaptor.getValue();
assertThat(mimeMappings.getAll()).isEmpty();
}
@Test
void withCustomMimeMappingsThenPopulatedMimeMappingsIsAdded() {
this.properties.getMimeMappings().add("a", "alpha");
this.properties.getMimeMappings().add("b", "bravo");
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
this.customizer.customize(factory);
ArgumentCaptor<MimeMappings> mimeMappingsCaptor = ArgumentCaptor.forClass(MimeMappings.class);
then(factory).should().addMimeMappings(mimeMappingsCaptor.capture());
MimeMappings mimeMappings = mimeMappingsCaptor.getValue();
assertThat(mimeMappings.getAll()).hasSize(2);
}
@Test
void testCustomizeDefaultServlet() {
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
this.properties.getServlet().setRegisterDefaultServlet(false);
this.customizer.customize(factory);
then(factory).should().setRegisterDefaultServlet(false);
}
@Test
void testCustomizeSsl() {
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
Ssl ssl = mock(Ssl.class);
this.properties.setSsl(ssl);
this.customizer.customize(factory);
then(factory).should().setSsl(ssl);
}
@Test
void testCustomizeJsp() {
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
this.customizer.customize(factory);
then(factory).should().setJsp(any(Jsp.class));
}
@Test
void customizeSessionProperties() {
Map<String, String> map = new HashMap<>();
map.put("server.servlet.session.timeout", "123");
map.put("server.servlet.session.tracking-modes", "cookie,url");
map.put("server.servlet.session.cookie.name", "testname");
map.put("server.servlet.session.cookie.domain", "testdomain");
map.put("server.servlet.session.cookie.path", "/testpath");
map.put("server.servlet.session.cookie.http-only", "true");
map.put("server.servlet.session.cookie.secure", "true");
map.put("server.servlet.session.cookie.max-age", "60");
bindProperties(map);
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
this.customizer.customize(factory);
then(factory).should().setSession(assertArg((session) -> {
assertThat(session.getTimeout()).hasSeconds(123);
Cookie cookie = session.getCookie();
assertThat(cookie.getName()).isEqualTo("testname");
assertThat(cookie.getDomain()).isEqualTo("testdomain");
assertThat(cookie.getPath()).isEqualTo("/testpath");
assertThat(cookie.getHttpOnly()).isTrue();
assertThat(cookie.getMaxAge()).hasSeconds(60);
}));
}
@Test
void testCustomizeTomcatPort() {
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
this.properties.setPort(8080);
this.customizer.customize(factory);
then(factory).should().setPort(8080);
}
@Test
void customizeServletDisplayName() {
Map<String, String> map = new HashMap<>();
map.put("server.servlet.application-display-name", "MyBootApp");
bindProperties(map);
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
this.customizer.customize(factory);
then(factory).should().setDisplayName("MyBootApp");
}
@Test
void sessionStoreDir() {
Map<String, String> map = new HashMap<>();
map.put("server.servlet.session.store-dir", "mydirectory");
bindProperties(map);
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
this.customizer.customize(factory);
then(factory).should()
.setSession(assertArg((session) -> assertThat(session.getStoreDir()).isEqualTo(new File("mydirectory"))));
}
@Test
void whenShutdownPropertyIsSetThenShutdownIsCustomized() {
Map<String, String> map = new HashMap<>();
map.put("server.shutdown", "immediate");
bindProperties(map);
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
this.customizer.customize(factory);
then(factory).should().setShutdown(assertArg((shutdown) -> assertThat(shutdown).isEqualTo(Shutdown.IMMEDIATE)));
}
@Test
void noLocaleCharsetMapping() {
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
this.customizer.customize(factory);
then(factory).should(never()).setLocaleCharsetMappings(anyMap());
}
@Test
void customLocaleCharsetMappings() {
Map<String, String> map = Map.of("server.servlet.encoding.mapping.en", "UTF-8",
"server.servlet.encoding.mapping.fr_FR", "UTF-8");
bindProperties(map);
ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class);
this.customizer.customize(factory);
then(factory).should()
.setLocaleCharsetMappings((assertArg((mappings) -> assertThat(mappings).hasSize(2)
.containsEntry(Locale.ENGLISH, StandardCharsets.UTF_8)
.containsEntry(Locale.FRANCE, StandardCharsets.UTF_8))));
}
private void bindProperties(Map<String, String> map) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(map);
new Binder(source).bind("server", Bindable.ofInstance(this.properties));
}
}

View File

@@ -0,0 +1,187 @@
/*
* Copyright 2012-2025 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.boot.web.server.autoconfigure.reactive;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
import org.springframework.boot.ssl.NoSuchSslBundleException;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.server.reactive.ConfigurableReactiveWebServerFactory;
import org.springframework.boot.web.server.reactive.MockReactiveWebServerFactory;
import org.springframework.boot.web.server.reactive.ReactiveWebServerFactory;
import org.springframework.boot.web.server.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Base class for testing sub-classes of {@link ReactiveWebServerConfiguration}.
*
* @author Brian Clozel
* @author Raheela Aslam
* @author Madhura Bhave
* @author Scott Frederick
*/
// @DirtiesUrlFactories
public abstract class AbstractReactiveWebServerAutoConfigurationTests {
private final ReactiveWebApplicationContextRunner mockServerRunner;
protected final ReactiveWebApplicationContextRunner serverRunner;
protected AbstractReactiveWebServerAutoConfigurationTests(Class<?> serverAutoConfiguration) {
ReactiveWebApplicationContextRunner common = new ReactiveWebApplicationContextRunner(
AnnotationConfigReactiveWebServerApplicationContext::new)
.withConfiguration(AutoConfigurations.of(serverAutoConfiguration));
this.serverRunner = common.withPropertyValues("server.port=0")
.withUserConfiguration(HttpHandlerConfiguration.class);
this.mockServerRunner = common.withUserConfiguration(MockWebServerConfiguration.class);
}
@Test
void createFromConfigClass() {
this.mockServerRunner.withUserConfiguration(MockWebServerConfiguration.class, HttpHandlerConfiguration.class)
.run((context) -> {
assertThat(context.getBeansOfType(ReactiveWebServerFactory.class)).hasSize(1);
assertThat(context.getBeansOfType(WebServerFactoryCustomizer.class)).hasSizeGreaterThanOrEqualTo(1);
assertThat(context.getBeansOfType(ReactiveWebServerFactoryCustomizer.class)).hasSize(1);
});
}
@Test
void missingHttpHandler() {
this.mockServerRunner.withUserConfiguration(MockWebServerConfiguration.class)
.run((context) -> assertThat(context.getStartupFailure()).isInstanceOf(ApplicationContextException.class)
.rootCause()
.hasMessageContaining("missing HttpHandler bean"));
}
@Test
void multipleHttpHandler() {
this.mockServerRunner
.withUserConfiguration(MockWebServerConfiguration.class, HttpHandlerConfiguration.class,
TooManyHttpHandlers.class)
.run((context) -> assertThat(context.getStartupFailure()).isInstanceOf(ApplicationContextException.class)
.rootCause()
.hasMessageContaining("multiple HttpHandler beans : httpHandler,additionalHttpHandler"));
}
@Test
void customizeReactiveWebServer() {
this.mockServerRunner
.withUserConfiguration(MockWebServerConfiguration.class, HttpHandlerConfiguration.class,
ReactiveWebServerCustomization.class)
.run((context) -> assertThat(context.getBean(MockReactiveWebServerFactory.class).getPort())
.isEqualTo(9000));
}
@Test
void webServerFailsWithInvalidSslBundle() {
this.serverRunner.withUserConfiguration(HttpHandlerConfiguration.class)
.withBean(WebServerFactoryCustomizer.class,
() -> (WebServerFactoryCustomizer<ConfigurableWebServerFactory>) (factory) -> factory
.setSslBundles(new DefaultSslBundleRegistry()))
.withPropertyValues("server.ssl.bundle=test-bundle")
.run((context) -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure().getCause()).isInstanceOf(NoSuchSslBundleException.class)
.withFailMessage("test");
});
}
@Test
void forwardedHeaderTransformerShouldBeConfigured() {
this.mockServerRunner.withUserConfiguration(HttpHandlerConfiguration.class)
.withPropertyValues("server.forward-headers-strategy=framework", "server.port=0")
.run((context) -> assertThat(context).hasSingleBean(ForwardedHeaderTransformer.class));
}
@Test
void forwardedHeaderTransformerWhenStrategyNotFilterShouldNotBeConfigured() {
this.mockServerRunner.withUserConfiguration(HttpHandlerConfiguration.class)
.withPropertyValues("server.forward-headers-strategy=native", "server.port=0")
.run((context) -> assertThat(context).doesNotHaveBean(ForwardedHeaderTransformer.class));
}
@Test
void forwardedHeaderTransformerWhenAlreadyRegisteredShouldBackOff() {
this.mockServerRunner
.withUserConfiguration(ForwardedHeaderTransformerConfiguration.class, HttpHandlerConfiguration.class)
.withPropertyValues("server.forward-headers-strategy=framework", "server.port=0")
.run((context) -> assertThat(context).hasSingleBean(ForwardedHeaderTransformer.class));
}
@Configuration(proxyBeanMethods = false)
static class HttpHandlerConfiguration {
@Bean
HttpHandler httpHandler() {
return mock(HttpHandler.class);
}
}
@Configuration(proxyBeanMethods = false)
static class TooManyHttpHandlers {
@Bean
HttpHandler additionalHttpHandler() {
return mock(HttpHandler.class);
}
}
@Configuration(proxyBeanMethods = false)
static class ReactiveWebServerCustomization {
@Bean
WebServerFactoryCustomizer<ConfigurableReactiveWebServerFactory> reactiveWebServerCustomizer() {
return (factory) -> factory.setPort(9000);
}
}
@Configuration(proxyBeanMethods = false)
static class MockWebServerConfiguration {
@Bean
MockReactiveWebServerFactory mockReactiveWebServerFactory() {
return new MockReactiveWebServerFactory();
}
}
@Configuration(proxyBeanMethods = false)
static class ForwardedHeaderTransformerConfiguration {
@Bean
ForwardedHeaderTransformer testForwardedHeaderTransformer() {
return new ForwardedHeaderTransformer();
}
}
}

View File

@@ -0,0 +1,285 @@
/*
* Copyright 2012-2025 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.boot.web.server.autoconfigure.servlet;
import java.io.IOException;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.websocket.server.ServerContainer;
import jakarta.websocket.server.ServerEndpoint;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
import org.springframework.boot.web.server.servlet.CookieSameSiteSupplier;
import org.springframework.boot.web.server.servlet.MockServletWebServerFactory;
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
import org.springframework.boot.web.server.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.filter.ForwardedHeaderFilter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Base class for testing sub-classes of {@link ServletWebServerConfiguration}.
*
* @author Dave Syer
* @author Phillip Webb
* @author Stephane Nicoll
* @author Raheela Aslam
* @author Madhura Bhave
*/
public abstract class AbstractServletWebServerAutoConfigurationTests {
private final WebApplicationContextRunner mockServerRunner;
protected final WebApplicationContextRunner serverRunner;
protected AbstractServletWebServerAutoConfigurationTests(Class<?> serverAutoConfiguration) {
WebApplicationContextRunner common = new WebApplicationContextRunner(
AnnotationConfigServletWebServerApplicationContext::new)
.withConfiguration(AutoConfigurations.of(serverAutoConfiguration));
this.serverRunner = common.withPropertyValues("server.port=0");
this.mockServerRunner = common.withUserConfiguration(MockWebServerConfiguration.class);
}
@Test
void createFromConfigClass() {
this.mockServerRunner
.run((context) -> assertThat(context).hasSingleBean(ServletWebServerFactoryCustomizer.class));
}
@Test
void webServerHasNoServletContext() {
this.mockServerRunner.withUserConfiguration(EnsureWebServerHasNoServletContext.class)
.run((context) -> assertThat(context).hasNotFailed());
}
@Test
void webServerFactoryCustomizerBeansAreCalledToCustomizeWebServerFactory() {
this.mockServerRunner
.withBean(WebServerFactoryCustomizer.class,
() -> (WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>) (factory) -> factory
.setPort(9000))
.run((context) -> assertThat(context.getBean(MockServletWebServerFactory.class).getPort()).isEqualTo(9000));
}
@Test
void initParametersAreConfiguredOnTheServletContext() {
this.mockServerRunner
.withPropertyValues("server.servlet.context-parameters.a:alpha",
"server.servlet.context-parameters.b:bravo")
.run((context) -> {
ServletContext servletContext = context.getServletContext();
assertThat(servletContext.getInitParameter("a")).isEqualTo("alpha");
assertThat(servletContext.getInitParameter("b")).isEqualTo("bravo");
});
}
@Test
void forwardedHeaderFilterShouldBeConfigured() {
this.mockServerRunner.withPropertyValues("server.forward-headers-strategy=framework").run((context) -> {
assertThat(context).hasSingleBean(FilterRegistrationBean.class);
Filter filter = context.getBean(FilterRegistrationBean.class).getFilter();
assertThat(filter).isInstanceOf(ForwardedHeaderFilter.class);
assertThat(filter).extracting("relativeRedirects").isEqualTo(false);
});
}
@Test
void forwardedHeaderFilterWhenStrategyNotFilterShouldNotBeConfigured() {
this.mockServerRunner.withPropertyValues("server.forward-headers-strategy=native")
.run((context) -> assertThat(context).doesNotHaveBean(FilterRegistrationBean.class));
}
@Test
void forwardedHeaderFilterWhenFilterAlreadyRegisteredShouldBackOff() {
this.mockServerRunner.withUserConfiguration(ForwardedHeaderFilterConfiguration.class)
.withPropertyValues("server.forward-headers-strategy=framework")
.run((context) -> assertThat(context).hasSingleBean(FilterRegistrationBean.class));
}
@Test
void cookieSameSiteSuppliersAreApplied() {
this.mockServerRunner.withUserConfiguration(CookieSameSiteSupplierConfiguration.class).run((context) -> {
ConfigurableServletWebServerFactory webServerFactory = context
.getBean(ConfigurableServletWebServerFactory.class);
assertThat(webServerFactory.getSettings().getCookieSameSiteSuppliers()).hasSize(2);
});
}
@Test
void webSocketServerContainerIsAvailableFromServletContext() {
this.serverRunner.run((context) -> {
Object serverContainer = context.getServletContext()
.getAttribute("jakarta.websocket.server.ServerContainer");
assertThat(serverContainer).isInstanceOf(ServerContainer.class);
});
}
@Test
void webSocketUpgradeDoesNotPreventAFilterFromRejectingTheRequest() {
this.serverRunner
.withBean("testEndpointRegistrar", ServletContextInitializer.class, () -> TestEndpoint::register)
.withUserConfiguration(UnauthorizedFilterConfiguration.class)
.run((context) -> {
TestEndpoint.register(context.getServletContext());
WebServer webServer = ((WebServerApplicationContext) context.getSourceApplicationContext())
.getWebServer();
int port = webServer.getPort();
RestTemplate rest = new RestTemplate();
RequestEntity<Void> request = RequestEntity.get("http://localhost:" + port)
.header("Upgrade", "websocket")
.header("Connection", "upgrade")
.header("Sec-WebSocket-Version", "13")
.header("Sec-WebSocket-Key", "key")
.build();
assertThatExceptionOfType(HttpClientErrorException.Unauthorized.class)
.isThrownBy(() -> rest.exchange(request, Void.class));
});
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnExpression("true")
static class MockWebServerConfiguration {
@Bean
ServletWebServerFactory webServerFactory() {
return new MockServletWebServerFactory();
}
}
@Component
static class EnsureWebServerHasNoServletContext implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
if (bean instanceof ConfigurableServletWebServerFactory) {
MockServletWebServerFactory webServerFactory = (MockServletWebServerFactory) bean;
assertThat(webServerFactory.getServletContext()).isNull();
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
return bean;
}
}
@Configuration(proxyBeanMethods = false)
static class ForwardedHeaderFilterConfiguration {
@Bean
FilterRegistrationBean<ForwardedHeaderFilter> testForwardedHeaderFilter() {
ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
return new FilterRegistrationBean<>(filter);
}
}
@Configuration(proxyBeanMethods = false)
static class CookieSameSiteSupplierConfiguration {
@Bean
CookieSameSiteSupplier cookieSameSiteSupplier1() {
return CookieSameSiteSupplier.ofLax().whenHasName("test1");
}
@Bean
CookieSameSiteSupplier cookieSameSiteSupplier2() {
return CookieSameSiteSupplier.ofNone().whenHasName("test2");
}
}
@ServerEndpoint("/")
public static class TestEndpoint {
static void register(ServletContext context) {
try {
ServerContainer serverContainer = (ServerContainer) context
.getAttribute("jakarta.websocket.server.ServerContainer");
if (serverContainer != null) {
serverContainer.addEndpoint(TestEndpoint.class);
}
}
catch (Exception ex) {
// Continue
}
}
}
@Configuration(proxyBeanMethods = false)
static class UnauthorizedFilterConfiguration {
@Bean
FilterRegistrationBean<Filter> unauthorizedFilter() {
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>(new Filter() {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
((HttpServletResponse) response).sendError(HttpStatus.UNAUTHORIZED.value());
}
});
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
registration.addUrlPatterns("/*");
registration.setDispatcherTypes(DispatcherType.REQUEST);
return registration;
}
@Bean
ServletRegistrationBean<HttpServlet> basicServlet() {
ServletRegistrationBean<HttpServlet> registration = new ServletRegistrationBean<>(new HttpServlet() {
});
registration.addUrlMappings("/");
return registration;
}
}
}