Create spring-boot-web-server module

This commit is contained in:
Andy Wilkinson
2025-05-01 13:40:49 +01:00
committed by Phillip Webb
parent 0cf76bf43b
commit 96bee8e034
202 changed files with 374 additions and 271 deletions

View File

@@ -0,0 +1,226 @@
/*
* 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;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.web.error.ErrorPage;
import org.springframework.boot.web.server.Ssl.ServerNameSslBundle;
import org.springframework.util.Assert;
/**
* Abstract base class for {@link ConfigurableWebServerFactory} implementations.
*
* @author Phillip Webb
* @author Dave Syer
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Ivan Sopov
* @author Eddú Meléndez
* @author Brian Clozel
* @author Scott Frederick
* @since 2.0.0
*/
public abstract class AbstractConfigurableWebServerFactory implements ConfigurableWebServerFactory {
private int port = 8080;
private InetAddress address;
private Set<ErrorPage> errorPages = new LinkedHashSet<>();
private Ssl ssl;
private SslBundles sslBundles;
private Http2 http2;
private Compression compression;
private String serverHeader;
private Shutdown shutdown = Shutdown.IMMEDIATE;
/**
* Create a new {@link AbstractConfigurableWebServerFactory} instance.
*/
public AbstractConfigurableWebServerFactory() {
}
/**
* Create a new {@link AbstractConfigurableWebServerFactory} instance with the
* specified port.
* @param port the port number for the web server
*/
public AbstractConfigurableWebServerFactory(int port) {
this.port = port;
}
/**
* The port that the web server listens on.
* @return the port
*/
public int getPort() {
return this.port;
}
@Override
public void setPort(int port) {
this.port = port;
}
/**
* Return the address that the web server binds to.
* @return the address
*/
public InetAddress getAddress() {
return this.address;
}
@Override
public void setAddress(InetAddress address) {
this.address = address;
}
/**
* Returns a mutable set of {@link ErrorPage ErrorPages} that will be used when
* handling exceptions.
* @return the error pages
*/
public Set<ErrorPage> getErrorPages() {
return this.errorPages;
}
@Override
public void setErrorPages(Set<? extends ErrorPage> errorPages) {
Assert.notNull(errorPages, "'errorPages' must not be null");
this.errorPages = new LinkedHashSet<>(errorPages);
}
@Override
public void addErrorPages(ErrorPage... errorPages) {
Assert.notNull(errorPages, "'errorPages' must not be null");
this.errorPages.addAll(Arrays.asList(errorPages));
}
public Ssl getSsl() {
return this.ssl;
}
@Override
public void setSsl(Ssl ssl) {
this.ssl = ssl;
}
/**
* Return the configured {@link SslBundles}.
* @return the {@link SslBundles} or {@code null}
* @since 3.2.0
*/
public SslBundles getSslBundles() {
return this.sslBundles;
}
@Override
public void setSslBundles(SslBundles sslBundles) {
this.sslBundles = sslBundles;
}
public Http2 getHttp2() {
return this.http2;
}
@Override
public void setHttp2(Http2 http2) {
this.http2 = http2;
}
public Compression getCompression() {
return this.compression;
}
@Override
public void setCompression(Compression compression) {
this.compression = compression;
}
public String getServerHeader() {
return this.serverHeader;
}
@Override
public void setServerHeader(String serverHeader) {
this.serverHeader = serverHeader;
}
@Override
public void setShutdown(Shutdown shutdown) {
this.shutdown = shutdown;
}
/**
* Returns the shutdown configuration that will be applied to the server.
* @return the shutdown configuration
* @since 2.3.0
*/
public Shutdown getShutdown() {
return this.shutdown;
}
/**
* Return the {@link SslBundle} that should be used with this server.
* @return the SSL bundle
*/
protected final SslBundle getSslBundle() {
return WebServerSslBundle.get(this.ssl, this.sslBundles);
}
protected final Map<String, SslBundle> getServerNameSslBundles() {
return this.ssl.getServerNameBundles()
.stream()
.collect(Collectors.toMap(ServerNameSslBundle::serverName,
(serverNameSslBundle) -> this.sslBundles.getBundle(serverNameSslBundle.bundle())));
}
/**
* Return the absolute temp dir for given web server.
* @param prefix server name
* @return the temp dir for given server.
*/
protected final File createTempDir(String prefix) {
try {
File tempDir = Files.createTempDirectory(prefix + "." + getPort() + ".").toFile();
tempDir.deleteOnExit();
return tempDir;
}
catch (IOException ex) {
throw new WebServerException(
"Unable to create tempDir. java.io.tmpdir is set to " + System.getProperty("java.io.tmpdir"), ex);
}
}
}

View File

@@ -0,0 +1,99 @@
/*
* 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;
import org.springframework.boot.context.properties.ConfigurationPropertiesSource;
import org.springframework.util.unit.DataSize;
/**
* Simple server-independent abstraction for compression configuration.
*
* @author Ivan Sopov
* @author Andy Wilkinson
* @author Stephane Nicoll
* @since 2.0.0
*/
@ConfigurationPropertiesSource
public class Compression {
/**
* Whether response compression is enabled.
*/
private boolean enabled;
/**
* Comma-separated list of MIME types that should be compressed.
*/
private String[] mimeTypes = new String[] { "text/html", "text/xml", "text/plain", "text/css", "text/javascript",
"application/javascript", "application/json", "application/xml" };
/**
* Comma-separated list of user agents for which responses should not be compressed.
*/
private String[] excludedUserAgents = null;
/**
* Minimum "Content-Length" value that is required for compression to be performed.
*/
private DataSize minResponseSize = DataSize.ofKilobytes(2);
/**
* Return whether response compression is enabled.
* @return {@code true} if response compression is enabled
*/
public boolean getEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* Return the MIME types that should be compressed.
* @return the MIME types that should be compressed
*/
public String[] getMimeTypes() {
return this.mimeTypes;
}
public void setMimeTypes(String[] mimeTypes) {
this.mimeTypes = mimeTypes;
}
public String[] getExcludedUserAgents() {
return this.excludedUserAgents;
}
public void setExcludedUserAgents(String[] excludedUserAgents) {
this.excludedUserAgents = excludedUserAgents;
}
/**
* Return the minimum "Content-Length" value that is required for compression to be
* performed.
* @return the minimum content size in bytes that is required for compression
*/
public DataSize getMinResponseSize() {
return this.minResponseSize;
}
public void setMinResponseSize(DataSize minSize) {
this.minResponseSize = minSize;
}
}

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;
import java.net.InetAddress;
import java.util.Set;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.web.error.ErrorPage;
import org.springframework.boot.web.error.ErrorPageRegistry;
/**
* A configurable {@link WebServerFactory}.
*
* @author Phillip Webb
* @author Brian Clozel
* @author Scott Frederick
* @since 2.0.0
* @see ErrorPageRegistry
*/
public interface ConfigurableWebServerFactory extends WebServerFactory, ErrorPageRegistry {
/**
* Sets the port that the web server should listen on. If not specified port '8080'
* will be used. Use port -1 to disable auto-start (i.e. start the web application
* context but not have it listen to any port).
* @param port the port to set
*/
void setPort(int port);
/**
* Sets the specific network address that the server should bind to.
* @param address the address to set (defaults to {@code null})
*/
void setAddress(InetAddress address);
/**
* Sets the error pages that will be used when handling exceptions.
* @param errorPages the error pages
*/
void setErrorPages(Set<? extends ErrorPage> errorPages);
/**
* Sets the SSL configuration that will be applied to the server's default connector.
* @param ssl the SSL configuration
*/
void setSsl(Ssl ssl);
/**
* Sets the SSL bundles that can be used to configure SSL connections.
* @param sslBundles the SSL bundles
* @since 3.1.0
*/
void setSslBundles(SslBundles sslBundles);
/**
* Sets the HTTP/2 configuration that will be applied to the server.
* @param http2 the HTTP/2 configuration
*/
void setHttp2(Http2 http2);
/**
* Sets the compression configuration that will be applied to the server's default
* connector.
* @param compression the compression configuration
*/
void setCompression(Compression compression);
/**
* Sets the server header value.
* @param serverHeader the server header value
*/
void setServerHeader(String serverHeader);
/**
* Sets the shutdown configuration that will be applied to the server.
* @param shutdown the shutdown configuration
* @since 2.3.0
*/
default void setShutdown(Shutdown shutdown) {
}
}

View File

@@ -0,0 +1,185 @@
/*
* 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;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import org.springframework.boot.context.properties.ConfigurationPropertiesSource;
import org.springframework.boot.convert.DurationUnit;
/**
* Cookie properties.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Brian Clozel
* @author Weix Sun
* @since 2.6.0
*/
@ConfigurationPropertiesSource
public class Cookie {
/**
* Name for the cookie.
*/
private String name;
/**
* Domain for the cookie.
*/
private String domain;
/**
* Path of the cookie.
*/
private String path;
/**
* Whether to use "HttpOnly" cookies for the cookie.
*/
private Boolean httpOnly;
/**
* Whether to always mark the cookie as secure.
*/
private Boolean secure;
/**
* Whether the generated cookie carries the Partitioned attribute.
*/
private Boolean partitioned;
/**
* 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".
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration maxAge;
/**
* SameSite setting for the cookie.
*/
private SameSite sameSite;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getDomain() {
return this.domain;
}
public void setDomain(String domain) {
this.domain = domain;
}
public String getPath() {
return this.path;
}
public void setPath(String path) {
this.path = path;
}
public Boolean getHttpOnly() {
return this.httpOnly;
}
public void setHttpOnly(Boolean httpOnly) {
this.httpOnly = httpOnly;
}
public Boolean getSecure() {
return this.secure;
}
public void setSecure(Boolean secure) {
this.secure = secure;
}
public Duration getMaxAge() {
return this.maxAge;
}
public void setMaxAge(Duration maxAge) {
this.maxAge = maxAge;
}
public SameSite getSameSite() {
return this.sameSite;
}
public void setSameSite(SameSite sameSite) {
this.sameSite = sameSite;
}
public Boolean getPartitioned() {
return this.partitioned;
}
public void setPartitioned(Boolean partitioned) {
this.partitioned = partitioned;
}
/**
* SameSite values.
*/
public enum SameSite {
/**
* SameSite attribute will be omitted when creating the cookie.
*/
OMITTED(null),
/**
* SameSite attribute will be set to None. Cookies are sent in both first-party
* and cross-origin requests.
*/
NONE("None"),
/**
* SameSite attribute will be set to Lax. Cookies are sent in a first-party
* context, also when following a link to the origin site.
*/
LAX("Lax"),
/**
* SameSite attribute will be set to Strict. Cookies are only sent in a
* first-party context (i.e. not when following a link to the origin site).
*/
STRICT("Strict");
private final String attributeValue;
SameSite(String attributeValue) {
this.attributeValue = attributeValue;
}
public String attributeValue() {
return this.attributeValue;
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2012-2020 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;
/**
* A callback for the result of a graceful shutdown request.
*
* @author Andy Wilkinson
* @since 2.3.0
* @see WebServer#shutDownGracefully(GracefulShutdownCallback)
*/
@FunctionalInterface
public interface GracefulShutdownCallback {
/**
* Graceful shutdown has completed with the given {@code result}.
* @param result the result of the shutdown
*/
void shutdownComplete(GracefulShutdownResult result);
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2012-2024 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;
/**
* The result of a graceful shutdown request.
*
* @author Andy Wilkinson
* @since 2.3.0
* @see GracefulShutdownCallback
* @see WebServer#shutDownGracefully(GracefulShutdownCallback)
*/
public enum GracefulShutdownResult {
/**
* Requests remained active at the end of the grace period.
*/
REQUESTS_ACTIVE,
/**
* The server was idle with no active requests at the end of the grace period.
*/
IDLE,
/**
* The server was shutdown immediately, ignoring any active requests.
*/
IMMEDIATE
}

View File

@@ -0,0 +1,47 @@
/*
* 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;
import org.springframework.boot.context.properties.ConfigurationPropertiesSource;
/**
* Simple server-independent abstraction for HTTP/2 configuration.
*
* @author Brian Clozel
* @since 2.0.0
*/
@ConfigurationPropertiesSource
public class Http2 {
/**
* Whether to enable HTTP/2 support, if the current environment supports it.
*/
private boolean enabled;
/**
* Return whether to enable HTTP/2 support, if the current environment supports it.
* @return {@code true} to enable HTTP/2 support
*/
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

View File

@@ -0,0 +1,404 @@
/*
* 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;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.util.Assert;
/**
* Simple server-independent abstraction for mime mappings. Roughly equivalent to the
* {@literal &lt;mime-mapping&gt;} element traditionally found in web.xml.
*
* @author Phillip Webb
* @author Guirong Hu
* @since 2.0.0
*/
public sealed class MimeMappings implements Iterable<MimeMappings.Mapping> {
/**
* Default mime mapping commonly used.
*/
public static final MimeMappings DEFAULT = new DefaultMimeMappings();
private final Map<String, Mapping> map;
/**
* Create a new empty {@link MimeMappings} instance.
*/
public MimeMappings() {
this.map = new LinkedHashMap<>();
}
/**
* Create a new {@link MimeMappings} instance from the specified mappings.
* @param mappings the source mappings
*/
public MimeMappings(MimeMappings mappings) {
this(mappings, true);
}
/**
* Create a new {@link MimeMappings} from the specified mappings.
* @param mappings the source mappings with extension as the key and mime-type as the
* value
*/
public MimeMappings(Map<String, String> mappings) {
Assert.notNull(mappings, "'mappings' must not be null");
this.map = new LinkedHashMap<>();
mappings.forEach(this::add);
}
/**
* Internal constructor.
* @param mappings source mappings
* @param mutable if the new object should be mutable.
*/
MimeMappings(MimeMappings mappings, boolean mutable) {
Assert.notNull(mappings, "'mappings' must not be null");
this.map = (mutable ? new LinkedHashMap<>(mappings.map) : Collections.unmodifiableMap(mappings.map));
}
/**
* Add a new mime mapping.
* @param extension the file extension (excluding '.')
* @param mimeType the mime type to map
* @return any previous mapping or {@code null}
*/
public String add(String extension, String mimeType) {
Assert.notNull(extension, "'extension' must not be null");
Assert.notNull(mimeType, "'mimeType' must not be null");
Mapping previous = this.map.put(extension.toLowerCase(Locale.ENGLISH), new Mapping(extension, mimeType));
return (previous != null) ? previous.getMimeType() : null;
}
/**
* Remove an existing mapping.
* @param extension the file extension (excluding '.')
* @return the removed mime mapping or {@code null} if no item was removed
*/
public String remove(String extension) {
Assert.notNull(extension, "'extension' must not be null");
Mapping previous = this.map.remove(extension.toLowerCase(Locale.ENGLISH));
return (previous != null) ? previous.getMimeType() : null;
}
/**
* Get a mime mapping for the given extension.
* @param extension the file extension (excluding '.')
* @return a mime mapping or {@code null}
*/
public String get(String extension) {
Assert.notNull(extension, "'extension' must not be null");
Mapping mapping = this.map.get(extension.toLowerCase(Locale.ENGLISH));
return (mapping != null) ? mapping.getMimeType() : null;
}
/**
* Returns all defined mappings.
* @return the mappings.
*/
public Collection<Mapping> getAll() {
return this.map.values();
}
@Override
public final Iterator<Mapping> iterator() {
return getAll().iterator();
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj instanceof MimeMappings other) {
return getMap().equals(other.map);
}
return false;
}
@Override
public int hashCode() {
return getMap().hashCode();
}
Map<String, Mapping> getMap() {
return this.map;
}
/**
* Create a new unmodifiable view of the specified mapping. Methods that attempt to
* modify the returned map will throw {@link UnsupportedOperationException}s.
* @param mappings the mappings
* @return an unmodifiable view of the specified mappings.
*/
public static MimeMappings unmodifiableMappings(MimeMappings mappings) {
Assert.notNull(mappings, "'mappings' must not be null");
return new MimeMappings(mappings, false);
}
/**
* Create a new lazy copy of the given mappings that will only copy entries if the
* mappings are mutated.
* @param mappings the source mappings
* @return a new mappings instance
* @since 3.0.0
*/
public static MimeMappings lazyCopy(MimeMappings mappings) {
Assert.notNull(mappings, "'mappings' must not be null");
return new LazyMimeMappingsCopy(mappings);
}
/**
* A single mime mapping.
*/
public static final class Mapping {
private final String extension;
private final String mimeType;
public Mapping(String extension, String mimeType) {
Assert.notNull(extension, "'extension' must not be null");
Assert.notNull(mimeType, "'mimeType' must not be null");
this.extension = extension;
this.mimeType = mimeType;
}
public String getExtension() {
return this.extension;
}
public String getMimeType() {
return this.mimeType;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj instanceof Mapping other) {
return this.extension.equals(other.extension) && this.mimeType.equals(other.mimeType);
}
return false;
}
@Override
public int hashCode() {
return this.extension.hashCode();
}
@Override
public String toString() {
return "Mapping [extension=" + this.extension + ", mimeType=" + this.mimeType + "]";
}
}
/**
* {@link MimeMappings} implementation used for {@link MimeMappings#DEFAULT}. Provides
* in-memory access for common mappings and lazily loads the complete set when
* necessary.
*/
static final class DefaultMimeMappings extends MimeMappings {
static final String MIME_MAPPINGS_PROPERTIES = "mime-mappings.properties";
private static final MimeMappings COMMON;
static {
MimeMappings mappings = new MimeMappings();
mappings.add("avi", "video/x-msvideo");
mappings.add("bin", "application/octet-stream");
mappings.add("body", "text/html");
mappings.add("class", "application/java");
mappings.add("css", "text/css");
mappings.add("dtd", "application/xml-dtd");
mappings.add("gif", "image/gif");
mappings.add("gtar", "application/x-gtar");
mappings.add("gz", "application/x-gzip");
mappings.add("htm", "text/html");
mappings.add("html", "text/html");
mappings.add("jar", "application/java-archive");
mappings.add("java", "text/x-java-source");
mappings.add("jnlp", "application/x-java-jnlp-file");
mappings.add("jpe", "image/jpeg");
mappings.add("jpeg", "image/jpeg");
mappings.add("jpg", "image/jpeg");
mappings.add("js", "text/javascript");
mappings.add("json", "application/json");
mappings.add("otf", "font/otf");
mappings.add("pdf", "application/pdf");
mappings.add("png", "image/png");
mappings.add("ps", "application/postscript");
mappings.add("tar", "application/x-tar");
mappings.add("tif", "image/tiff");
mappings.add("tiff", "image/tiff");
mappings.add("ttf", "font/ttf");
mappings.add("txt", "text/plain");
mappings.add("xht", "application/xhtml+xml");
mappings.add("xhtml", "application/xhtml+xml");
mappings.add("xls", "application/vnd.ms-excel");
mappings.add("xml", "application/xml");
mappings.add("xsl", "application/xml");
mappings.add("xslt", "application/xslt+xml");
mappings.add("wasm", "application/wasm");
mappings.add("zip", "application/zip");
COMMON = unmodifiableMappings(mappings);
}
private volatile Map<String, Mapping> loaded;
DefaultMimeMappings() {
super(new MimeMappings(), false);
}
@Override
public Collection<Mapping> getAll() {
return load().values();
}
@Override
public String get(String extension) {
Assert.notNull(extension, "'extension' must not be null");
extension = extension.toLowerCase(Locale.ENGLISH);
Map<String, Mapping> loaded = this.loaded;
if (loaded != null) {
return get(loaded, extension);
}
String commonMimeType = COMMON.get(extension);
if (commonMimeType != null) {
return commonMimeType;
}
loaded = load();
return get(loaded, extension);
}
private String get(Map<String, Mapping> mappings, String extension) {
Mapping mapping = mappings.get(extension);
return (mapping != null) ? mapping.getMimeType() : null;
}
@Override
Map<String, Mapping> getMap() {
return load();
}
private Map<String, Mapping> load() {
Map<String, Mapping> loaded = this.loaded;
if (loaded != null) {
return loaded;
}
try {
loaded = new LinkedHashMap<>();
for (Entry<?, ?> entry : PropertiesLoaderUtils
.loadProperties(new ClassPathResource(MIME_MAPPINGS_PROPERTIES, getClass()))
.entrySet()) {
loaded.put((String) entry.getKey(),
new Mapping((String) entry.getKey(), (String) entry.getValue()));
}
loaded = Collections.unmodifiableMap(loaded);
this.loaded = loaded;
return loaded;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load the default MIME types", ex);
}
}
}
/**
* {@link MimeMappings} implementation used to create a lazy copy only when the
* mappings are mutated.
*/
static final class LazyMimeMappingsCopy extends MimeMappings {
private final MimeMappings source;
private final AtomicBoolean copied = new AtomicBoolean();
LazyMimeMappingsCopy(MimeMappings source) {
this.source = source;
}
@Override
public String add(String extension, String mimeType) {
copyIfNecessary();
return super.add(extension, mimeType);
}
@Override
public String remove(String extension) {
copyIfNecessary();
return super.remove(extension);
}
private void copyIfNecessary() {
if (this.copied.compareAndSet(false, true)) {
this.source.forEach((mapping) -> add(mapping.getExtension(), mapping.getMimeType()));
}
}
@Override
public String get(String extension) {
return !this.copied.get() ? this.source.get(extension) : super.get(extension);
}
@Override
public Collection<Mapping> getAll() {
return !this.copied.get() ? this.source.getAll() : super.getAll();
}
@Override
Map<String, Mapping> getMap() {
return !this.copied.get() ? this.source.getMap() : super.getMap();
}
}
static class MimeMappingsRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources()
.registerPattern("org/springframework/boot/web/server/" + DefaultMimeMappings.MIME_MAPPINGS_PROPERTIES);
}
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2012-2024 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;
import java.net.BindException;
import java.util.Locale;
import java.util.function.Consumer;
import java.util.function.IntSupplier;
/**
* A {@code PortInUseException} is thrown when a web server fails to start due to a port
* already being in use.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.0.0
*/
public class PortInUseException extends WebServerException {
private final int port;
/**
* Creates a new port in use exception for the given {@code port}.
* @param port the port that was in use
*/
public PortInUseException(int port) {
this(port, null);
}
/**
* Creates a new port in use exception for the given {@code port}.
* @param port the port that was in use
* @param cause the cause of the exception
*/
public PortInUseException(int port, Throwable cause) {
super("Port " + port + " is already in use", cause);
this.port = port;
}
/**
* Returns the port that was in use.
* @return the port
*/
public int getPort() {
return this.port;
}
/**
* Throw a {@link PortInUseException} if the given exception was caused by a "port in
* use" {@link BindException}.
* @param ex the source exception
* @param port a suppler used to provide the port
* @since 2.2.7
*/
public static void throwIfPortBindingException(Exception ex, IntSupplier port) {
ifPortBindingException(ex, (bindException) -> {
throw new PortInUseException(port.getAsInt(), ex);
});
}
/**
* Perform an action if the given exception was caused by a "port in use"
* {@link BindException}.
* @param ex the source exception
* @param action the action to perform
* @since 2.2.7
*/
public static void ifPortBindingException(Exception ex, Consumer<BindException> action) {
ifCausedBy(ex, BindException.class, (bindException) -> {
// bind exception can be also thrown because an address can't be assigned
if (bindException.getMessage().toLowerCase(Locale.ROOT).contains("in use")) {
action.accept(bindException);
}
});
}
/**
* Perform an action if the given exception was caused by a specific exception type.
* @param <E> the cause exception type
* @param ex the source exception
* @param causedBy the required cause type
* @param action the action to perform
* @since 2.2.7
*/
@SuppressWarnings("unchecked")
public static <E extends Exception> void ifCausedBy(Exception ex, Class<E> causedBy, Consumer<E> action) {
Throwable candidate = ex;
while (candidate != null) {
if (causedBy.isInstance(candidate)) {
action.accept((E) candidate);
return;
}
candidate = candidate.getCause();
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
/**
* A {@code FailureAnalyzer} that performs analysis of failures caused by a
* {@code PortInUseException}.
*
* @author Andy Wilkinson
*/
class PortInUseFailureAnalyzer extends AbstractFailureAnalyzer<PortInUseException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, PortInUseException cause) {
return new FailureAnalysis("Web server failed to start. Port " + cause.getPort() + " was already in use.",
"Identify and stop the process that's listening on port " + cause.getPort() + " or configure this "
+ "application to listen on another port.",
cause);
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2012-2024 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;
/**
* Configuration for shutting down a {@link WebServer}.
*
* @author Andy Wilkinson
* @since 2.3.0
*/
public enum Shutdown {
/**
* The {@link WebServer} should support graceful shutdown, allowing active requests
* time to complete.
*/
GRACEFUL,
/**
* The {@link WebServer} should shut down immediately.
*/
IMMEDIATE
}

View File

@@ -0,0 +1,466 @@
/*
* 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;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationPropertiesSource;
/**
* Simple server-independent abstraction for SSL configuration.
*
* @author Andy Wilkinson
* @author Vladimir Tsanev
* @author Stephane Nicoll
* @author Scott Frederick
* @since 2.0.0
*/
@ConfigurationPropertiesSource
public class Ssl {
/**
* Whether to enable SSL support.
*/
private boolean enabled = true;
/**
* Name of a configured SSL bundle.
*/
private String bundle;
/**
* Client authentication mode. Requires a trust store.
*/
private ClientAuth clientAuth;
/**
* Supported SSL ciphers.
*/
private String[] ciphers;
/**
* Enabled SSL protocols.
*/
private String[] enabledProtocols;
/**
* Alias that identifies the key in the key store.
*/
private String keyAlias;
/**
* Password used to access the key in the key store.
*/
private String keyPassword;
/**
* Path to the key store that holds the SSL certificate (typically a jks file).
*/
private String keyStore;
/**
* Password used to access the key store.
*/
private String keyStorePassword;
/**
* Type of the key store.
*/
private String keyStoreType;
/**
* Provider for the key store.
*/
private String keyStoreProvider;
/**
* Trust store that holds SSL certificates.
*/
private String trustStore;
/**
* Password used to access the trust store.
*/
private String trustStorePassword;
/**
* Type of the trust store.
*/
private String trustStoreType;
/**
* Provider for the trust store.
*/
private String trustStoreProvider;
/**
* Path to a PEM-encoded SSL certificate file.
*/
private String certificate;
/**
* Path to a PEM-encoded private key file for the SSL certificate.
*/
private String certificatePrivateKey;
/**
* Path to a PEM-encoded SSL certificate authority file.
*/
private String trustCertificate;
/**
* Path to a PEM-encoded private key file for the SSL certificate authority.
*/
private String trustCertificatePrivateKey;
/**
* SSL protocol to use.
*/
private String protocol = "TLS";
/**
* Mapping of host names to SSL bundles for SNI configuration.
*/
private List<ServerNameSslBundle> serverNameBundles = new ArrayList<>();
/**
* Return whether to enable SSL support.
* @return whether to enable SSL support
*/
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* Return the name of the SSL bundle to use.
* @return the SSL bundle name
* @since 3.1.0
*/
public String getBundle() {
return this.bundle;
}
/**
* Set the name of the SSL bundle to use.
* @param bundle the SSL bundle name
* @since 3.1.0
*/
public void setBundle(String bundle) {
this.bundle = bundle;
}
/**
* Return Whether client authentication is not wanted ("none"), wanted ("want") or
* needed ("need"). Requires a trust store.
* @return the {@link ClientAuth} to use
*/
public ClientAuth getClientAuth() {
return this.clientAuth;
}
public void setClientAuth(ClientAuth clientAuth) {
this.clientAuth = clientAuth;
}
/**
* Return the supported SSL ciphers.
* @return the supported SSL ciphers
*/
public String[] getCiphers() {
return this.ciphers;
}
public void setCiphers(String[] ciphers) {
this.ciphers = ciphers;
}
/**
* Return the enabled SSL protocols.
* @return the enabled SSL protocols.
*/
public String[] getEnabledProtocols() {
return this.enabledProtocols;
}
public void setEnabledProtocols(String[] enabledProtocols) {
this.enabledProtocols = enabledProtocols;
}
/**
* Return the alias that identifies the key in the key store.
* @return the key alias
*/
public String getKeyAlias() {
return this.keyAlias;
}
public void setKeyAlias(String keyAlias) {
this.keyAlias = keyAlias;
}
/**
* Return the password used to access the key in the key store.
* @return the key password
*/
public String getKeyPassword() {
return this.keyPassword;
}
public void setKeyPassword(String keyPassword) {
this.keyPassword = keyPassword;
}
/**
* Return the path to the key store that holds the SSL certificate (typically a jks
* file).
* @return the path to the key store
*/
public String getKeyStore() {
return this.keyStore;
}
public void setKeyStore(String keyStore) {
this.keyStore = keyStore;
}
/**
* Return the password used to access the key store.
* @return the key store password
*/
public String getKeyStorePassword() {
return this.keyStorePassword;
}
public void setKeyStorePassword(String keyStorePassword) {
this.keyStorePassword = keyStorePassword;
}
/**
* Return the type of the key store.
* @return the key store type
*/
public String getKeyStoreType() {
return this.keyStoreType;
}
public void setKeyStoreType(String keyStoreType) {
this.keyStoreType = keyStoreType;
}
/**
* Return the provider for the key store.
* @return the key store provider
*/
public String getKeyStoreProvider() {
return this.keyStoreProvider;
}
public void setKeyStoreProvider(String keyStoreProvider) {
this.keyStoreProvider = keyStoreProvider;
}
/**
* Return the trust store that holds SSL certificates.
* @return the trust store
*/
public String getTrustStore() {
return this.trustStore;
}
public void setTrustStore(String trustStore) {
this.trustStore = trustStore;
}
/**
* Return the password used to access the trust store.
* @return the trust store password
*/
public String getTrustStorePassword() {
return this.trustStorePassword;
}
public void setTrustStorePassword(String trustStorePassword) {
this.trustStorePassword = trustStorePassword;
}
/**
* Return the type of the trust store.
* @return the trust store type
*/
public String getTrustStoreType() {
return this.trustStoreType;
}
public void setTrustStoreType(String trustStoreType) {
this.trustStoreType = trustStoreType;
}
/**
* Return the provider for the trust store.
* @return the trust store provider
*/
public String getTrustStoreProvider() {
return this.trustStoreProvider;
}
public void setTrustStoreProvider(String trustStoreProvider) {
this.trustStoreProvider = trustStoreProvider;
}
/**
* Return the location of the certificate in PEM format.
* @return the certificate location
*/
public String getCertificate() {
return this.certificate;
}
public void setCertificate(String certificate) {
this.certificate = certificate;
}
/**
* Return the location of the private key for the certificate in PEM format.
* @return the location of the certificate private key
*/
public String getCertificatePrivateKey() {
return this.certificatePrivateKey;
}
public void setCertificatePrivateKey(String certificatePrivateKey) {
this.certificatePrivateKey = certificatePrivateKey;
}
/**
* Return the location of the trust certificate authority chain in PEM format.
* @return the location of the trust certificate
*/
public String getTrustCertificate() {
return this.trustCertificate;
}
public void setTrustCertificate(String trustCertificate) {
this.trustCertificate = trustCertificate;
}
/**
* Return the location of the private key for the trust certificate in PEM format.
* @return the location of the trust certificate private key
*/
public String getTrustCertificatePrivateKey() {
return this.trustCertificatePrivateKey;
}
public void setTrustCertificatePrivateKey(String trustCertificatePrivateKey) {
this.trustCertificatePrivateKey = trustCertificatePrivateKey;
}
/**
* Return the SSL protocol to use.
* @return the SSL protocol
*/
public String getProtocol() {
return this.protocol;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
/**
* Returns if SSL is enabled for the given instance.
* @param ssl the {@link Ssl SSL} instance or {@code null}
* @return {@code true} if SSL is enabled
* @since 3.1.0
*/
public static boolean isEnabled(Ssl ssl) {
return (ssl != null) && ssl.isEnabled();
}
/**
* Return the mapping of host names to SSL bundles for SNI configuration.
* @return the host name to SSL bundle mapping
*/
public List<ServerNameSslBundle> getServerNameBundles() {
return this.serverNameBundles;
}
public void setServerNameBundles(List<ServerNameSslBundle> serverNameBundles) {
this.serverNameBundles = serverNameBundles;
}
/**
* Factory method to create an {@link Ssl} instance for a specific bundle name.
* @param bundle the name of the bundle
* @return a new {@link Ssl} instance with the bundle set
* @since 3.1.0
*/
public static Ssl forBundle(String bundle) {
Ssl ssl = new Ssl();
ssl.setBundle(bundle);
return ssl;
}
public record ServerNameSslBundle(String serverName, String bundle) {
}
/**
* Client authentication types.
*/
public enum ClientAuth {
/**
* Client authentication is not wanted.
*/
NONE,
/**
* Client authentication is wanted but not mandatory.
*/
WANT,
/**
* Client authentication is needed and mandatory.
*/
NEED;
/**
* Map an optional {@link ClientAuth} value to a different type.
* @param <R> the result type
* @param clientAuth the client auth to map (may be {@code null})
* @param none the value for {@link ClientAuth#NONE} or {@code null}
* @param want the value for {@link ClientAuth#WANT}
* @param need the value for {@link ClientAuth#NEED}
* @return the mapped value
* @since 3.1.0
*/
public static <R> R map(ClientAuth clientAuth, R none, R want, R need) {
return switch ((clientAuth != null) ? clientAuth : NONE) {
case NONE -> none;
case WANT -> want;
case NEED -> need;
};
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2012-2023 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;
/**
* Simple interface that represents a fully configured web server (for example Tomcat,
* Jetty, Netty). Allows the server to be {@link #start() started} and {@link #stop()
* stopped}.
*
* @author Phillip Webb
* @author Dave Syer
* @since 2.0.0
*/
public interface WebServer {
/**
* Starts the web server. Calling this method on an already started server has no
* effect.
* @throws WebServerException if the server cannot be started
*/
void start() throws WebServerException;
/**
* Stops the web server. Calling this method on an already stopped server has no
* effect.
* @throws WebServerException if the server cannot be stopped
*/
void stop() throws WebServerException;
/**
* Return the port this server is listening on.
* @return the port (or -1 if none)
*/
int getPort();
/**
* Initiates a graceful shutdown of the web server. Handling of new requests is
* prevented and the given {@code callback} is invoked at the end of the attempt. The
* attempt can be explicitly ended by invoking {@link #stop}. The default
* implementation invokes the callback immediately with
* {@link GracefulShutdownResult#IMMEDIATE}, i.e. no attempt is made at a graceful
* shutdown.
* @param callback the callback to invoke when the graceful shutdown completes
* @since 2.3.0
*/
default void shutDownGracefully(GracefulShutdownCallback callback) {
callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
}
/**
* Destroys the web server such that it cannot be started again.
* @since 3.2.0
*/
default void destroy() {
stop();
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2012-2021 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;
/**
* Exceptions thrown by a web server.
*
* @author Phillip Webb
* @since 2.0.0
*/
@SuppressWarnings("serial")
public class WebServerException extends RuntimeException {
public WebServerException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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;
/**
* Tagging interface for factories that create a {@link WebServer}.
*
* @author Phillip Webb
* @since 2.0.0
* @see WebServer
* @see org.springframework.boot.web.server.servlet.ServletWebServerFactory
* @see org.springframework.boot.web.server.reactive.ReactiveWebServerFactory
*/
public interface WebServerFactory {
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2012-2019 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;
import org.springframework.beans.factory.config.BeanPostProcessor;
/**
* Strategy interface for customizing {@link WebServerFactory web server factories}. Any
* beans of this type will get a callback with the server factory before the server itself
* is started, so you can set the port, address, error pages etc.
* <p>
* Beware: calls to this interface are usually made from a
* {@link WebServerFactoryCustomizerBeanPostProcessor} which is a
* {@link BeanPostProcessor} (so called very early in the ApplicationContext lifecycle).
* It might be safer to lookup dependencies lazily in the enclosing BeanFactory rather
* than injecting them with {@code @Autowired}.
*
* @param <T> the configurable web server factory
* @author Phillip Webb
* @author Dave Syer
* @author Brian Clozel
* @since 2.0.0
* @see WebServerFactoryCustomizerBeanPostProcessor
*/
@FunctionalInterface
public interface WebServerFactoryCustomizer<T extends WebServerFactory> {
/**
* Customize the specified {@link WebServerFactory}.
* @param factory the web server factory to customize
*/
void customize(T factory);
}

View File

@@ -0,0 +1,89 @@
/*
* 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;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.util.LambdaSafe;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.util.Assert;
/**
* {@link BeanPostProcessor} that applies all {@link WebServerFactoryCustomizer} beans
* from the bean factory to {@link WebServerFactory} beans.
*
* @author Dave Syer
* @author Phillip Webb
* @author Stephane Nicoll
* @since 2.0.0
*/
public class WebServerFactoryCustomizerBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {
private ListableBeanFactory beanFactory;
private List<WebServerFactoryCustomizer<?>> customizers;
@Override
public void setBeanFactory(BeanFactory beanFactory) {
Assert.isInstanceOf(ListableBeanFactory.class, beanFactory, "'beanFactory' must be a ListableBeanFactory");
this.beanFactory = (ListableBeanFactory) beanFactory;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebServerFactory webServerFactory) {
postProcessBeforeInitialization(webServerFactory);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@SuppressWarnings("unchecked")
private void postProcessBeforeInitialization(WebServerFactory webServerFactory) {
LambdaSafe.callbacks(WebServerFactoryCustomizer.class, getCustomizers(), webServerFactory)
.withLogger(WebServerFactoryCustomizerBeanPostProcessor.class)
.invoke((customizer) -> customizer.customize(webServerFactory));
}
private Collection<WebServerFactoryCustomizer<?>> getCustomizers() {
if (this.customizers == null) {
// Look up does not include the parent context
this.customizers = new ArrayList<>(getWebServerFactoryCustomizerBeans());
this.customizers.sort(AnnotationAwareOrderComparator.INSTANCE);
this.customizers = Collections.unmodifiableList(this.customizers);
}
return this.customizers;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private Collection<WebServerFactoryCustomizer<?>> getWebServerFactoryCustomizerBeans() {
return (Collection) this.beanFactory.getBeansOfType(WebServerFactoryCustomizer.class, false, false).values();
}
}

View File

@@ -0,0 +1,242 @@
/*
* Copyright 2012-2024 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;
import java.security.KeyStore;
import org.springframework.boot.ssl.NoSuchSslBundleException;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundleKey;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.ssl.SslManagerBundle;
import org.springframework.boot.ssl.SslOptions;
import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link SslBundle} backed by {@link Ssl}.
*
* @author Scott Frederick
* @author Phillip Webb
* @since 3.1.0
*/
public final class WebServerSslBundle implements SslBundle {
private final SslStoreBundle stores;
private final SslBundleKey key;
private final SslOptions options;
private final String protocol;
private final SslManagerBundle managers;
private WebServerSslBundle(SslStoreBundle stores, String keyPassword, Ssl ssl) {
this.stores = stores;
this.key = SslBundleKey.of(keyPassword, ssl.getKeyAlias());
this.protocol = ssl.getProtocol();
this.options = SslOptions.of(ssl.getCiphers(), ssl.getEnabledProtocols());
this.managers = SslManagerBundle.from(this.stores, this.key);
}
private static SslStoreBundle createPemKeyStoreBundle(Ssl ssl) {
PemSslStoreDetails keyStoreDetails = new PemSslStoreDetails(ssl.getKeyStoreType(), ssl.getCertificate(),
ssl.getCertificatePrivateKey())
.withAlias(ssl.getKeyAlias());
return new PemSslStoreBundle(keyStoreDetails, null);
}
private static SslStoreBundle createPemTrustStoreBundle(Ssl ssl) {
PemSslStoreDetails trustStoreDetails = new PemSslStoreDetails(ssl.getTrustStoreType(),
ssl.getTrustCertificate(), ssl.getTrustCertificatePrivateKey())
.withAlias(ssl.getKeyAlias());
return new PemSslStoreBundle(null, trustStoreDetails);
}
private static SslStoreBundle createJksKeyStoreBundle(Ssl ssl) {
JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails(ssl.getKeyStoreType(), ssl.getKeyStoreProvider(),
ssl.getKeyStore(), ssl.getKeyStorePassword());
return new JksSslStoreBundle(keyStoreDetails, null);
}
private static SslStoreBundle createJksTrustStoreBundle(Ssl ssl) {
JksSslStoreDetails trustStoreDetails = new JksSslStoreDetails(ssl.getTrustStoreType(),
ssl.getTrustStoreProvider(), ssl.getTrustStore(), ssl.getTrustStorePassword());
return new JksSslStoreBundle(null, trustStoreDetails);
}
@Override
public SslStoreBundle getStores() {
return this.stores;
}
@Override
public SslBundleKey getKey() {
return this.key;
}
@Override
public SslOptions getOptions() {
return this.options;
}
@Override
public String getProtocol() {
return this.protocol;
}
@Override
public SslManagerBundle getManagers() {
return this.managers;
}
/**
* Get the {@link SslBundle} that should be used for the given {@link Ssl} instance.
* @param ssl the source ssl instance
* @return a {@link SslBundle} instance
* @throws NoSuchSslBundleException if a bundle lookup fails
*/
public static SslBundle get(Ssl ssl) throws NoSuchSslBundleException {
return get(ssl, null);
}
/**
* Get the {@link SslBundle} that should be used for the given {@link Ssl} instance.
* @param ssl the source ssl instance
* @param sslBundles the bundles that should be used when {@link Ssl#getBundle()} is
* set
* @return a {@link SslBundle} instance
* @throws NoSuchSslBundleException if a bundle lookup fails
*/
public static SslBundle get(Ssl ssl, SslBundles sslBundles) throws NoSuchSslBundleException {
Assert.state(Ssl.isEnabled(ssl), "SSL is not enabled");
String keyPassword = ssl.getKeyPassword();
String bundleName = ssl.getBundle();
if (StringUtils.hasText(bundleName)) {
Assert.state(sslBundles != null,
() -> "SSL bundle '%s' was requested but no SslBundles instance was provided"
.formatted(bundleName));
return sslBundles.getBundle(bundleName);
}
SslStoreBundle stores = createStoreBundle(ssl);
return new WebServerSslBundle(stores, keyPassword, ssl);
}
private static SslStoreBundle createStoreBundle(Ssl ssl) {
KeyStore keyStore = createKeyStore(ssl);
KeyStore trustStore = createTrustStore(ssl);
return new WebServerSslStoreBundle(keyStore, trustStore, ssl.getKeyStorePassword());
}
private static KeyStore createKeyStore(Ssl ssl) {
if (hasPemKeyStoreProperties(ssl)) {
return createPemKeyStoreBundle(ssl).getKeyStore();
}
else if (hasJksKeyStoreProperties(ssl)) {
return createJksKeyStoreBundle(ssl).getKeyStore();
}
return null;
}
private static KeyStore createTrustStore(Ssl ssl) {
if (hasPemTrustStoreProperties(ssl)) {
return createPemTrustStoreBundle(ssl).getTrustStore();
}
else if (hasJksTrustStoreProperties(ssl)) {
return createJksTrustStoreBundle(ssl).getTrustStore();
}
return null;
}
private static boolean hasPemKeyStoreProperties(Ssl ssl) {
return Ssl.isEnabled(ssl) && ssl.getCertificate() != null && ssl.getCertificatePrivateKey() != null;
}
private static boolean hasPemTrustStoreProperties(Ssl ssl) {
return Ssl.isEnabled(ssl) && ssl.getTrustCertificate() != null;
}
private static boolean hasJksKeyStoreProperties(Ssl ssl) {
return Ssl.isEnabled(ssl) && (ssl.getKeyStore() != null
|| (ssl.getKeyStoreType() != null && ssl.getKeyStoreType().equals("PKCS11")));
}
private static boolean hasJksTrustStoreProperties(Ssl ssl) {
return Ssl.isEnabled(ssl) && (ssl.getTrustStore() != null
|| (ssl.getTrustStoreType() != null && ssl.getTrustStoreType().equals("PKCS11")));
}
@Override
public String toString() {
ToStringCreator creator = new ToStringCreator(this);
creator.append("key", this.key);
creator.append("protocol", this.protocol);
creator.append("stores", this.stores);
creator.append("options", this.options);
return creator.toString();
}
private static final class WebServerSslStoreBundle implements SslStoreBundle {
private final KeyStore keyStore;
private final KeyStore trustStore;
private final String keyStorePassword;
private WebServerSslStoreBundle(KeyStore keyStore, KeyStore trustStore, String keyStorePassword) {
Assert.state(keyStore != null || trustStore != null,
"SSL is enabled but no trust material is configured for the default host");
this.keyStore = keyStore;
this.trustStore = trustStore;
this.keyStorePassword = keyStorePassword;
}
@Override
public KeyStore getKeyStore() {
return this.keyStore;
}
@Override
public KeyStore getTrustStore() {
return this.trustStore;
}
@Override
public String getKeyStorePassword() {
return this.keyStorePassword;
}
@Override
public String toString() {
ToStringCreator creator = new ToStringCreator(this);
creator.append("keyStore.type", (this.keyStore != null) ? this.keyStore.getType() : "none");
creator.append("keyStorePassword", (this.keyStorePassword != null) ? "******" : null);
creator.append("trustStore.type", (this.trustStore != null) ? this.trustStore.getType() : "none");
return creator.toString();
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.context;
import org.springframework.context.ConfigurableApplicationContext;
/**
* SPI interface to be implemented by most if not all {@link WebServerApplicationContext
* web server application contexts}. Provides facilities to configure the context, in
* addition to the methods in the {WebServerApplicationContext} interface.
*
* @author Phillip Webb
* @since 2.0.0
*/
public interface ConfigurableWebServerApplicationContext
extends ConfigurableApplicationContext, WebServerApplicationContext {
/**
* Set the server namespace of the context.
* @param serverNamespace the server namespace
* @see #getServerNamespace()
*/
void setServerNamespace(String serverNamespace);
}

View File

@@ -0,0 +1,59 @@
/*
* 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.context;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.web.server.WebServerFactory;
/**
* Exception thrown when there is no {@link WebServerFactory} bean of the required type
* defined in a {@link WebServerApplicationContext}.
*
* @author Guirong Hu
* @author Andy Wilkinson
* @since 2.7.0
*/
public class MissingWebServerFactoryBeanException extends NoSuchBeanDefinitionException {
private final WebApplicationType webApplicationType;
/**
* Create a new {@code MissingWebServerFactoryBeanException}.
* @param webServerApplicationContextClass the class of the
* WebServerApplicationContext that required the WebServerFactory
* @param webServerFactoryClass the class of the WebServerFactory that was missing
* @param webApplicationType the type of the web application
*/
public MissingWebServerFactoryBeanException(
Class<? extends WebServerApplicationContext> webServerApplicationContextClass,
Class<? extends WebServerFactory> webServerFactoryClass, WebApplicationType webApplicationType) {
super(webServerFactoryClass, String.format("Unable to start %s due to missing %s bean",
webServerApplicationContextClass.getSimpleName(), webServerFactoryClass.getSimpleName()));
this.webApplicationType = webApplicationType;
}
/**
* Returns the type of web application for which a {@link WebServerFactory} bean was
* missing.
* @return the type of web application
*/
public WebApplicationType getWebApplicationType() {
return this.webApplicationType;
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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.context;
import java.util.Locale;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.diagnostics.FailureAnalyzer;
import org.springframework.core.annotation.Order;
/**
* A {@link FailureAnalyzer} that performs analysis of failures caused by a
* {@link MissingWebServerFactoryBeanException}.
*
* @author Guirong Hu
* @author Andy Wilkinson
*/
@Order(0)
class MissingWebServerFactoryBeanFailureAnalyzer extends AbstractFailureAnalyzer<MissingWebServerFactoryBeanException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, MissingWebServerFactoryBeanException cause) {
return new FailureAnalysis(
"Web application could not be started as there was no " + cause.getBeanType().getName()
+ " bean defined in the context.",
"Check your application's dependencies for a supported "
+ cause.getWebApplicationType().name().toLowerCase(Locale.ENGLISH) + " web server.\n"
+ "Check the configured web application type.",
cause);
}
}

View File

@@ -0,0 +1,93 @@
/*
* 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.context;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.server.WebServer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.util.StringUtils;
/**
* {@link ApplicationContextInitializer} that sets {@link Environment} properties for the
* ports that {@link WebServer} servers are actually listening on. The property
* {@literal "local.server.port"} can be injected directly into tests using
* {@link Value @Value} or obtained through the {@link Environment}.
* <p>
* If the {@link WebServerInitializedEvent} has a
* {@link WebServerApplicationContext#getServerNamespace() server namespace}, it will be
* used to construct the property name. For example, the "management" actuator context
* will have the property name {@literal "local.management.port"}.
* <p>
* Properties are automatically propagated up to any parent context.
*
* @author Dave Syer
* @author Phillip Webb
* @since 2.0.0
*/
public class ServerPortInfoApplicationContextInitializer implements
ApplicationContextInitializer<ConfigurableApplicationContext>, ApplicationListener<WebServerInitializedEvent> {
private static final String PROPERTY_SOURCE_NAME = "server.ports";
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.addApplicationListener(this);
}
@Override
public void onApplicationEvent(WebServerInitializedEvent event) {
String propertyName = "local." + getName(event.getApplicationContext()) + ".port";
setPortProperty(event.getApplicationContext(), propertyName, event.getWebServer().getPort());
}
private String getName(WebServerApplicationContext context) {
String name = context.getServerNamespace();
return StringUtils.hasText(name) ? name : "server";
}
private void setPortProperty(ApplicationContext context, String propertyName, int port) {
if (context instanceof ConfigurableApplicationContext configurableContext) {
setPortProperty(configurableContext.getEnvironment(), propertyName, port);
}
if (context.getParent() != null) {
setPortProperty(context.getParent(), propertyName, port);
}
}
@SuppressWarnings("unchecked")
private void setPortProperty(ConfigurableEnvironment environment, String propertyName, int port) {
MutablePropertySources sources = environment.getPropertySources();
PropertySource<?> source = sources.get(PROPERTY_SOURCE_NAME);
if (source == null) {
source = new MapPropertySource(PROPERTY_SOURCE_NAME, new HashMap<>());
sources.addFirst(source);
}
((Map<String, Object>) source.getSource()).put(propertyName, port);
}
}

View File

@@ -0,0 +1,90 @@
/*
* 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.context;
import org.springframework.boot.web.server.WebServer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.SmartLifecycle;
import org.springframework.util.ObjectUtils;
/**
* Interface to be implemented by {@link ApplicationContext application contexts} that
* create and manage the lifecycle of an embedded {@link WebServer}.
*
* @author Phillip Webb
* @since 2.0.0
*/
public interface WebServerApplicationContext extends ApplicationContext {
/**
* {@link SmartLifecycle#getPhase() SmartLifecycle phase} in which graceful shutdown
* of the web server is performed.
* @since 4.0.0
*/
int GRACEFUL_SHUTDOWN_PHASE = SmartLifecycle.DEFAULT_PHASE - 1024;
/**
* {@link SmartLifecycle#getPhase() SmartLifecycle phase} in which starting and
* stopping of the web server is performed.
* @since 4.0.0
*/
int START_STOP_LIFECYCLE_PHASE = GRACEFUL_SHUTDOWN_PHASE - 1024;
/**
* Returns the {@link WebServer} that was created by the context or {@code null} if
* the server has not yet been created.
* @return the web server
*/
WebServer getWebServer();
/**
* Returns the namespace of the web server application context or {@code null} if no
* namespace has been set. Used for disambiguation when multiple web servers are
* running in the same application (for example a management context running on a
* different port).
* @return the server namespace
*/
String getServerNamespace();
/**
* Returns {@code true} if the specified context is a
* {@link WebServerApplicationContext} with a matching server namespace.
* @param context the context to check
* @param serverNamespace the server namespace to match against
* @return {@code true} if the server namespace of the context matches
* @since 2.1.8
*/
static boolean hasServerNamespace(ApplicationContext context, String serverNamespace) {
return (context instanceof WebServerApplicationContext webServerApplicationContext)
&& ObjectUtils.nullSafeEquals(webServerApplicationContext.getServerNamespace(), serverNamespace);
}
/**
* Returns the server namespace if the specified context is a
* {@link WebServerApplicationContext}.
* @param context the context
* @return the server namespace or {@code null} if the context is not a
* {@link WebServerApplicationContext}
* @since 2.6.0
*/
static String getServerNamespace(ApplicationContext context) {
return (context instanceof WebServerApplicationContext configurableContext)
? configurableContext.getServerNamespace() : null;
}
}

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.context;
import org.springframework.boot.web.server.WebServer;
import org.springframework.context.SmartLifecycle;
/**
* {@link SmartLifecycle} to trigger {@link WebServer} graceful shutdown.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public final class WebServerGracefulShutdownLifecycle implements SmartLifecycle {
/**
* {@link SmartLifecycle#getPhase() SmartLifecycle phase} in which graceful shutdown
* of the web server is performed.
* @deprecated as of 4.0.0 in favor of
* {@link WebServerApplicationContext#GRACEFUL_SHUTDOWN_PHASE}
*/
@Deprecated(since = "4.0.0", forRemoval = true)
public static final int SMART_LIFECYCLE_PHASE = SmartLifecycle.DEFAULT_PHASE - 1024;
private final WebServer webServer;
private volatile boolean running;
/**
* Creates a new {@code WebServerGracefulShutdownLifecycle} that will gracefully shut
* down the given {@code webServer}.
* @param webServer web server to shut down gracefully
*/
public WebServerGracefulShutdownLifecycle(WebServer webServer) {
this.webServer = webServer;
}
@Override
public void start() {
this.running = true;
}
@Override
public void stop() {
throw new UnsupportedOperationException("Stop must not be invoked directly");
}
@Override
public void stop(Runnable callback) {
this.running = false;
this.webServer.shutDownGracefully((result) -> callback.run());
}
@Override
public boolean isRunning() {
return this.running;
}
@Override
public int getPhase() {
return WebServerApplicationContext.GRACEFUL_SHUTDOWN_PHASE;
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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.context;
import org.springframework.boot.web.server.WebServer;
import org.springframework.context.ApplicationEvent;
/**
* Event to be published when the {@link WebServer} is ready. Useful for obtaining the
* local port of a running server.
*
* @author Brian Clozel
* @author Stephane Nicoll
* @since 2.0.0
*/
@SuppressWarnings("serial")
public abstract class WebServerInitializedEvent extends ApplicationEvent {
protected WebServerInitializedEvent(WebServer webServer) {
super(webServer);
}
/**
* Access the {@link WebServer}.
* @return the embedded web server
*/
public WebServer getWebServer() {
return getSource();
}
/**
* Access the application context that the server was created in. Sometimes it is
* prudent to check that this matches expectations (like being equal to the current
* context) before acting on the server itself.
* @return the applicationContext that the server was created from
*/
public abstract WebServerApplicationContext getApplicationContext();
/**
* Access the source of the event (an {@link WebServer}).
* @return the embedded web server
*/
@Override
public WebServer getSource() {
return (WebServer) super.getSource();
}
}

View File

@@ -0,0 +1,144 @@
/*
* 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.context;
import java.io.File;
import java.util.Locale;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.system.SystemProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.core.log.LogMessage;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
/**
* An {@link ApplicationListener} that saves embedded server port and management port into
* file. This application listener will be triggered whenever the server starts, and the
* file name can be overridden at runtime with a System property or environment variable
* named "PORTFILE" or "portfile".
*
* @author David Liu
* @author Phillip Webb
* @author Andy Wilkinson
* @since 2.0.0
*/
public class WebServerPortFileWriter implements ApplicationListener<WebServerInitializedEvent> {
private static final String DEFAULT_FILE_NAME = "application.port";
private static final String[] PROPERTY_VARIABLES = { "PORTFILE", "portfile" };
private static final Log logger = LogFactory.getLog(WebServerPortFileWriter.class);
private final File file;
/**
* Create a new {@link WebServerPortFileWriter} instance using the filename
* 'application.port'.
*/
public WebServerPortFileWriter() {
this(new File(DEFAULT_FILE_NAME));
}
/**
* Create a new {@link WebServerPortFileWriter} instance with a specified filename.
* @param filename the name of file containing port
*/
public WebServerPortFileWriter(String filename) {
this(new File(filename));
}
/**
* Create a new {@link WebServerPortFileWriter} instance with a specified file.
* @param file the file containing port
*/
public WebServerPortFileWriter(File file) {
Assert.notNull(file, "'file' must not be null");
String override = SystemProperties.get(PROPERTY_VARIABLES);
if (override != null) {
this.file = new File(override);
}
else {
this.file = file;
}
}
@Override
public void onApplicationEvent(WebServerInitializedEvent event) {
File portFile = getPortFile(event.getApplicationContext());
try {
String port = String.valueOf(event.getWebServer().getPort());
createParentDirectory(portFile);
FileCopyUtils.copy(port.getBytes(), portFile);
portFile.deleteOnExit();
}
catch (Exception ex) {
logger.warn(LogMessage.format("Cannot create port file %s", this.file));
}
}
/**
* Return the actual port file that should be written for the given application
* context. The default implementation builds a file from the source file and the
* application context namespace if available.
* @param applicationContext the source application context
* @return the file that should be written
*/
protected File getPortFile(ApplicationContext applicationContext) {
String namespace = getServerNamespace(applicationContext);
if (!StringUtils.hasLength(namespace)) {
return this.file;
}
String filename = this.file.getName();
String extension = StringUtils.getFilenameExtension(filename);
String filenameWithoutExtension = (extension != null)
? filename.substring(0, filename.length() - extension.length() - 1) : filename;
String suffix = (!isUpperCase(filename)) ? namespace.toLowerCase(Locale.ENGLISH)
: namespace.toUpperCase(Locale.ENGLISH);
return new File(this.file.getParentFile(),
filenameWithoutExtension + "-" + suffix + ((!StringUtils.hasLength(extension)) ? "" : "." + extension));
}
private String getServerNamespace(ApplicationContext applicationContext) {
if (applicationContext instanceof WebServerApplicationContext webServerApplicationContext) {
return webServerApplicationContext.getServerNamespace();
}
return null;
}
private boolean isUpperCase(String name) {
for (int i = 0; i < name.length(); i++) {
if (Character.isLetter(name.charAt(i)) && !Character.isUpperCase(name.charAt(i))) {
return false;
}
}
return true;
}
private void createParentDirectory(File file) {
File parent = file.getParentFile();
if (parent != null) {
parent.mkdirs();
}
}
}

View File

@@ -0,0 +1,21 @@
/*
* 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.
*/
/**
* Web integrations with Spring's {@link org.springframework.context.ApplicationContext
* ApplicationContext}.
*/
package org.springframework.boot.web.server.context;

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2019 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.
*/
/**
* Support for embedded web servers.
*/
package org.springframework.boot.web.server;

View File

@@ -0,0 +1,37 @@
/*
* 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.reactive;
import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory;
/**
* Abstract base class for {@link ReactiveWebServerFactory} implementations.
*
* @author Brian Clozel
* @since 4.0.0
*/
public abstract class AbstractReactiveWebServerFactory extends AbstractConfigurableWebServerFactory
implements ConfigurableReactiveWebServerFactory {
public AbstractReactiveWebServerFactory() {
}
public AbstractReactiveWebServerFactory(int port) {
super(port);
}
}

View File

@@ -0,0 +1,29 @@
/*
* 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.reactive;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
/**
* Configurable {@link ReactiveWebServerFactory}.
*
* @author Brian Clozel
* @since 4.0.0
*/
public interface ConfigurableReactiveWebServerFactory extends ConfigurableWebServerFactory, ReactiveWebServerFactory {
}

View File

@@ -0,0 +1,44 @@
/*
* 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.reactive;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerFactory;
import org.springframework.http.server.reactive.HttpHandler;
/**
* Factory interface that can be used to create a reactive {@link WebServer}.
*
* @author Brian Clozel
* @since 4.0.0
* @see WebServer
*/
@FunctionalInterface
public interface ReactiveWebServerFactory extends WebServerFactory {
/**
* Gets a new fully configured but paused {@link WebServer} instance. Clients should
* not be able to connect to the returned server until {@link WebServer#start()} is
* called (which happens when the {@code ApplicationContext} has been fully
* refreshed).
* @param httpHandler the HTTP handler in charge of processing requests
* @return a fully configured and started {@link WebServer}
* @see WebServer#stop()
*/
WebServer getWebServer(HttpHandler httpHandler);
}

View File

@@ -0,0 +1,212 @@
/*
* 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.reactive.context;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.annotation.AnnotatedBeanDefinitionReader;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.AnnotationConfigRegistry;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.annotation.AnnotationScopeMetadataResolver;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.context.annotation.ScopeMetadataResolver;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* {@link ReactiveWebServerApplicationContext} that accepts annotated classes as input -
* in particular
* {@link org.springframework.context.annotation.Configuration @Configuration}-annotated
* classes, but also plain {@link Component @Component} classes and JSR-330 compliant
* classes using {@code javax.inject} annotations. Allows for registering classes one by
* one (specifying class names as config location) as well as for classpath scanning
* (specifying base packages as config location).
* <p>
* Note: In case of multiple {@code @Configuration} classes, later {@code @Bean}
* definitions will override ones defined in earlier loaded files. This can be leveraged
* to deliberately override certain bean definitions through an extra Configuration class.
*
* @author Phillip Webb
* @since 2.0.0
* @see #register(Class...)
* @see #scan(String...)
* @see ReactiveWebServerApplicationContext
* @see AnnotationConfigApplicationContext
*/
public class AnnotationConfigReactiveWebServerApplicationContext extends ReactiveWebServerApplicationContext
implements AnnotationConfigRegistry {
private final AnnotatedBeanDefinitionReader reader;
private final ClassPathBeanDefinitionScanner scanner;
private final Set<Class<?>> annotatedClasses = new LinkedHashSet<>();
private String[] basePackages;
/**
* Create a new {@link AnnotationConfigReactiveWebServerApplicationContext} that needs
* to be populated through {@link #register} calls and then manually
* {@linkplain #refresh refreshed}.
*/
public AnnotationConfigReactiveWebServerApplicationContext() {
this.reader = new AnnotatedBeanDefinitionReader(this);
this.scanner = new ClassPathBeanDefinitionScanner(this);
}
/**
* Create a new {@link AnnotationConfigReactiveWebServerApplicationContext} with the
* given {@code DefaultListableBeanFactory}. The context needs to be populated through
* {@link #register} calls and then manually {@linkplain #refresh refreshed}.
* @param beanFactory the DefaultListableBeanFactory instance to use for this context
*/
public AnnotationConfigReactiveWebServerApplicationContext(DefaultListableBeanFactory beanFactory) {
super(beanFactory);
this.reader = new AnnotatedBeanDefinitionReader(this);
this.scanner = new ClassPathBeanDefinitionScanner(this);
}
/**
* Create a new {@link AnnotationConfigReactiveWebServerApplicationContext}, deriving
* bean definitions from the given annotated classes and automatically refreshing the
* context.
* @param annotatedClasses one or more annotated classes, e.g. {@code @Configuration}
* classes
*/
public AnnotationConfigReactiveWebServerApplicationContext(Class<?>... annotatedClasses) {
this();
register(annotatedClasses);
refresh();
}
/**
* Create a new {@link AnnotationConfigReactiveWebServerApplicationContext}, scanning
* for bean definitions in the given packages and automatically refreshing the
* context.
* @param basePackages the packages to check for annotated classes
*/
public AnnotationConfigReactiveWebServerApplicationContext(String... basePackages) {
this();
scan(basePackages);
refresh();
}
/**
* {@inheritDoc}
* <p>
* Delegates given environment to underlying {@link AnnotatedBeanDefinitionReader} and
* {@link ClassPathBeanDefinitionScanner} members.
*/
@Override
public void setEnvironment(ConfigurableEnvironment environment) {
super.setEnvironment(environment);
this.reader.setEnvironment(environment);
this.scanner.setEnvironment(environment);
}
/**
* Provide a custom {@link BeanNameGenerator} for use with
* {@link AnnotatedBeanDefinitionReader} and/or
* {@link ClassPathBeanDefinitionScanner}, if any.
* <p>
* Default is
* {@link org.springframework.context.annotation.AnnotationBeanNameGenerator}.
* <p>
* Any call to this method must occur prior to calls to {@link #register(Class...)}
* and/or {@link #scan(String...)}.
* @param beanNameGenerator the bean name generator
* @see AnnotatedBeanDefinitionReader#setBeanNameGenerator
* @see ClassPathBeanDefinitionScanner#setBeanNameGenerator
*/
public void setBeanNameGenerator(BeanNameGenerator beanNameGenerator) {
this.reader.setBeanNameGenerator(beanNameGenerator);
this.scanner.setBeanNameGenerator(beanNameGenerator);
getBeanFactory().registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, beanNameGenerator);
}
/**
* Set the {@link ScopeMetadataResolver} to use for detected bean classes.
* <p>
* The default is an {@link AnnotationScopeMetadataResolver}.
* <p>
* Any call to this method must occur prior to calls to {@link #register(Class...)}
* and/or {@link #scan(String...)}.
* @param scopeMetadataResolver the scope metadata resolver
*/
public void setScopeMetadataResolver(ScopeMetadataResolver scopeMetadataResolver) {
this.reader.setScopeMetadataResolver(scopeMetadataResolver);
this.scanner.setScopeMetadataResolver(scopeMetadataResolver);
}
/**
* Register one or more annotated classes to be processed. Note that
* {@link #refresh()} must be called in order for the context to fully process the new
* class.
* <p>
* Calls to {@code #register} are idempotent; adding the same annotated class more
* than once has no additional effect.
* @param annotatedClasses one or more annotated classes, e.g. {@code @Configuration}
* classes
* @see #scan(String...)
* @see #refresh()
*/
@Override
public final void register(Class<?>... annotatedClasses) {
Assert.notEmpty(annotatedClasses, "'annotatedClasses' must not be empty");
this.annotatedClasses.addAll(Arrays.asList(annotatedClasses));
}
/**
* Perform a scan within the specified base packages. Note that {@link #refresh()}
* must be called in order for the context to fully process the new class.
* @param basePackages the packages to check for annotated classes
* @see #register(Class...)
* @see #refresh()
*/
@Override
public final void scan(String... basePackages) {
Assert.notEmpty(basePackages, "'basePackages' must not be empty");
this.basePackages = basePackages;
}
@Override
protected void prepareRefresh() {
this.scanner.clearCache();
super.prepareRefresh();
}
@Override
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
super.postProcessBeanFactory(beanFactory);
if (!ObjectUtils.isEmpty(this.basePackages)) {
this.scanner.scan(this.basePackages);
}
if (!this.annotatedClasses.isEmpty()) {
this.reader.register(ClassUtils.toClassArray(this.annotatedClasses));
}
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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.reactive.context;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.boot.web.context.reactive.StandardReactiveWebEnvironment;
import org.springframework.core.env.ConfigurablePropertyResolver;
import org.springframework.core.env.MutablePropertySources;
/**
* {@link StandardReactiveWebEnvironment} for typical use in a typical
* {@link SpringApplication}.
*
* @author Phillip Webb
*/
class ApplicationReactiveWebEnvironment extends StandardReactiveWebEnvironment {
@Override
protected String doGetActiveProfilesProperty() {
return null;
}
@Override
protected String doGetDefaultProfilesProperty() {
return null;
}
@Override
protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySources propertySources) {
return ConfigurationPropertySources.createPropertyResolver(propertySources);
}
}

View File

@@ -0,0 +1,184 @@
/*
* 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.reactive.context;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.boot.web.context.reactive.GenericReactiveWebApplicationContext;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.context.ConfigurableWebServerApplicationContext;
import org.springframework.boot.web.server.context.MissingWebServerFactoryBeanException;
import org.springframework.boot.web.server.context.WebServerGracefulShutdownLifecycle;
import org.springframework.boot.web.server.reactive.ReactiveWebServerFactory;
import org.springframework.context.ApplicationContextException;
import org.springframework.core.metrics.StartupStep;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.StringUtils;
/**
* A {@link GenericReactiveWebApplicationContext} that can be used to bootstrap itself
* from a contained {@link ReactiveWebServerFactory} bean.
*
* @author Brian Clozel
* @since 2.0.0
*/
public class ReactiveWebServerApplicationContext extends GenericReactiveWebApplicationContext
implements ConfigurableWebServerApplicationContext {
private volatile WebServerManager serverManager;
private String serverNamespace;
/**
* Create a new {@link ReactiveWebServerApplicationContext}.
*/
public ReactiveWebServerApplicationContext() {
}
/**
* Create a new {@link ReactiveWebServerApplicationContext} with the given
* {@code DefaultListableBeanFactory}.
* @param beanFactory the DefaultListableBeanFactory instance to use for this context
*/
public ReactiveWebServerApplicationContext(DefaultListableBeanFactory beanFactory) {
super(beanFactory);
}
@Override
public final void refresh() throws BeansException, IllegalStateException {
try {
super.refresh();
}
catch (RuntimeException ex) {
WebServer webServer = getWebServer();
if (webServer != null) {
try {
webServer.stop();
webServer.destroy();
}
catch (RuntimeException stopOrDestroyEx) {
ex.addSuppressed(stopOrDestroyEx);
}
}
throw ex;
}
}
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start reactive web server", ex);
}
}
private void createWebServer() {
WebServerManager serverManager = this.serverManager;
if (serverManager == null) {
StartupStep createWebServer = getApplicationStartup().start("spring.boot.webserver.create");
String webServerFactoryBeanName = getWebServerFactoryBeanName();
ReactiveWebServerFactory webServerFactory = getWebServerFactory(webServerFactoryBeanName);
createWebServer.tag("factory", webServerFactory.getClass().toString());
boolean lazyInit = getBeanFactory().getBeanDefinition(webServerFactoryBeanName).isLazyInit();
this.serverManager = new WebServerManager(this, webServerFactory, this::getHttpHandler, lazyInit);
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.serverManager.getWebServer()));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this.serverManager));
createWebServer.end();
}
initPropertySources();
}
protected String getWebServerFactoryBeanName() {
// Use bean names so that we don't consider the hierarchy
String[] beanNames = getBeanFactory().getBeanNamesForType(ReactiveWebServerFactory.class);
if (beanNames.length == 0) {
throw new MissingWebServerFactoryBeanException(getClass(), ReactiveWebServerFactory.class,
WebApplicationType.REACTIVE);
}
if (beanNames.length > 1) {
throw new ApplicationContextException("Unable to start ReactiveWebApplicationContext due to multiple "
+ "ReactiveWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
}
return beanNames[0];
}
protected ReactiveWebServerFactory getWebServerFactory(String factoryBeanName) {
return getBeanFactory().getBean(factoryBeanName, ReactiveWebServerFactory.class);
}
/**
* Return the {@link HttpHandler} that should be used to process the reactive web
* server. By default this method searches for a suitable bean in the context itself.
* @return a {@link HttpHandler} (never {@code null}
*/
protected HttpHandler getHttpHandler() {
// Use bean names so that we don't consider the hierarchy
String[] beanNames = getBeanFactory().getBeanNamesForType(HttpHandler.class);
if (beanNames.length == 0) {
throw new ApplicationContextException(
"Unable to start ReactiveWebApplicationContext due to missing HttpHandler bean.");
}
if (beanNames.length > 1) {
throw new ApplicationContextException(
"Unable to start ReactiveWebApplicationContext due to multiple HttpHandler beans : "
+ StringUtils.arrayToCommaDelimitedString(beanNames));
}
return getBeanFactory().getBean(beanNames[0], HttpHandler.class);
}
@Override
protected void doClose() {
if (isActive()) {
AvailabilityChangeEvent.publish(this, ReadinessState.REFUSING_TRAFFIC);
}
super.doClose();
WebServer webServer = getWebServer();
if (webServer != null) {
webServer.destroy();
}
}
/**
* Returns the {@link WebServer} that was created by the context or {@code null} if
* the server has not yet been created.
* @return the web server
*/
@Override
public WebServer getWebServer() {
WebServerManager serverManager = this.serverManager;
return (serverManager != null) ? serverManager.getWebServer() : null;
}
@Override
public String getServerNamespace() {
return this.serverNamespace;
}
@Override
public void setServerNamespace(String serverNamespace) {
this.serverNamespace = serverNamespace;
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.reactive.context;
import org.springframework.aot.AotDetector;
import org.springframework.boot.ApplicationContextFactory;
import org.springframework.boot.WebApplicationType;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
/**
* {@link ApplicationContextFactory} registered in {@code spring.factories} to support
* {@link AnnotationConfigReactiveWebServerApplicationContext} and
* {@link ReactiveWebServerApplicationContext}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class ReactiveWebServerApplicationContextFactory implements ApplicationContextFactory {
@Override
public Class<? extends ConfigurableEnvironment> getEnvironmentType(WebApplicationType webApplicationType) {
return (webApplicationType != WebApplicationType.REACTIVE) ? null : ApplicationReactiveWebEnvironment.class;
}
@Override
public ConfigurableEnvironment createEnvironment(WebApplicationType webApplicationType) {
return (webApplicationType != WebApplicationType.REACTIVE) ? null : new ApplicationReactiveWebEnvironment();
}
@Override
public ConfigurableApplicationContext create(WebApplicationType webApplicationType) {
return (webApplicationType != WebApplicationType.REACTIVE) ? null : createContext();
}
private ConfigurableApplicationContext createContext() {
if (!AotDetector.useGeneratedArtifacts()) {
return new AnnotationConfigReactiveWebServerApplicationContext();
}
return new ReactiveWebServerApplicationContext();
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.reactive.context;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.context.WebServerInitializedEvent;
/**
* Event to be published after the {@link WebServer} is ready. Useful for obtaining the
* local port of a running server.
*
* @author Brian Clozel
* @author Stephane Nicoll
* @since 2.0.0
*/
public class ReactiveWebServerInitializedEvent extends WebServerInitializedEvent {
private final ReactiveWebServerApplicationContext applicationContext;
public ReactiveWebServerInitializedEvent(WebServer webServer,
ReactiveWebServerApplicationContext applicationContext) {
super(webServer);
this.applicationContext = applicationContext;
}
@Override
public ReactiveWebServerApplicationContext getApplicationContext() {
return this.applicationContext;
}
}

View File

@@ -0,0 +1,129 @@
/*
* 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.reactive.context;
import java.util.function.Supplier;
import reactor.core.publisher.Mono;
import org.springframework.boot.web.server.GracefulShutdownCallback;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.reactive.ReactiveWebServerFactory;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
/**
* Internal class used to manage the server and the {@link HttpHandler}, taking care not
* to initialize the handler too early.
*
* @author Andy Wilkinson
*/
class WebServerManager {
private final ReactiveWebServerApplicationContext applicationContext;
private final DelayedInitializationHttpHandler handler;
private final WebServer webServer;
WebServerManager(ReactiveWebServerApplicationContext applicationContext, ReactiveWebServerFactory factory,
Supplier<HttpHandler> handlerSupplier, boolean lazyInit) {
this.applicationContext = applicationContext;
Assert.notNull(factory, "'factory' must not be null");
this.handler = new DelayedInitializationHttpHandler(handlerSupplier, lazyInit);
this.webServer = factory.getWebServer(this.handler);
}
void start() {
this.handler.initializeHandler();
this.webServer.start();
this.applicationContext
.publishEvent(new ReactiveWebServerInitializedEvent(this.webServer, this.applicationContext));
}
void shutDownGracefully(GracefulShutdownCallback callback) {
this.webServer.shutDownGracefully(callback);
}
void stop() {
this.webServer.stop();
}
WebServer getWebServer() {
return this.webServer;
}
HttpHandler getHandler() {
return this.handler;
}
/**
* A delayed {@link HttpHandler} that doesn't initialize things too early.
*/
static final class DelayedInitializationHttpHandler implements HttpHandler {
private final Supplier<HttpHandler> handlerSupplier;
private final boolean lazyInit;
private volatile HttpHandler delegate = this::handleUninitialized;
private DelayedInitializationHttpHandler(Supplier<HttpHandler> handlerSupplier, boolean lazyInit) {
this.handlerSupplier = handlerSupplier;
this.lazyInit = lazyInit;
}
private Mono<Void> handleUninitialized(ServerHttpRequest request, ServerHttpResponse response) {
throw new IllegalStateException("The HttpHandler has not yet been initialized");
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
return this.delegate.handle(request, response);
}
void initializeHandler() {
this.delegate = this.lazyInit ? new LazyHttpHandler(this.handlerSupplier) : this.handlerSupplier.get();
}
HttpHandler getHandler() {
return this.delegate;
}
}
/**
* {@link HttpHandler} that initializes its delegate on first request.
*/
private static final class LazyHttpHandler implements HttpHandler {
private final Mono<HttpHandler> delegate;
private LazyHttpHandler(Supplier<HttpHandler> handlerSupplier) {
this.delegate = Mono.fromSupplier(handlerSupplier);
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
return this.delegate.flatMap((handler) -> handler.handle(request, response));
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.reactive.context;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.context.SmartLifecycle;
/**
* {@link SmartLifecycle} to start and stop the {@link WebServer} in a
* {@link ReactiveWebServerApplicationContext}.
*
* @author Andy Wilkinson
*/
class WebServerStartStopLifecycle implements SmartLifecycle {
private final WebServerManager weServerManager;
private volatile boolean running;
WebServerStartStopLifecycle(WebServerManager weServerManager) {
this.weServerManager = weServerManager;
}
@Override
public void start() {
this.weServerManager.start();
this.running = true;
}
@Override
public void stop() {
this.running = false;
this.weServerManager.stop();
}
@Override
public boolean isRunning() {
return this.running;
}
@Override
public int getPhase() {
return WebServerApplicationContext.START_STOP_LIFECYCLE_PHASE;
}
}

View File

@@ -0,0 +1,21 @@
/*
* 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.
*/
/**
* Reactive web server based integrations with Spring's
* {@link org.springframework.context.ApplicationContext ApplicationContext}.
*/
package org.springframework.boot.web.server.reactive.context;

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.
*/
/**
* Reactive web server abstractions.
*/
package org.springframework.boot.web.server.reactive;

View File

@@ -0,0 +1,195 @@
/*
* 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.servlet;
import java.io.File;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import jakarta.servlet.ServletContext;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.Cookie.SameSite;
import org.springframework.boot.web.server.MimeMappings;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.ServletContextInitializer;
/**
* A configurable {@link ServletWebServerFactory}.
*
* @author Dave Syer
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Eddú Meléndez
* @author Brian Clozel
* @since 4.0.0
* @see ServletWebServerFactory
* @see WebServerFactoryCustomizer
*/
public interface ConfigurableServletWebServerFactory
extends ConfigurableWebServerFactory, ServletWebServerFactory, WebListenerRegistry {
ServletWebServerSettings getSettings();
/**
* Sets the context path for the web server. The context should start with a "/"
* character but not end with a "/" character. The default context path can be
* specified using an empty string.
* @param contextPath the context path to set
*/
default void setContextPath(String contextPath) {
getSettings().setContextPath(ContextPath.of(contextPath));
}
/**
* Returns the context path for the servlet web server.
* @return the context path
*/
default String getContextPath() {
return getSettings().getContextPath().toString();
}
/**
* Sets the display name of the application deployed in the web server.
* @param displayName the displayName to set
* @since 4.0.0
*/
default void setDisplayName(String displayName) {
getSettings().setDisplayName(displayName);
}
/**
* Sets the configuration that will be applied to the container's HTTP session
* support.
* @param session the session configuration
*/
default void setSession(Session session) {
getSettings().setSession(session);
}
/**
* Set if the DefaultServlet should be registered. Defaults to {@code false} since
* 2.4.
* @param registerDefaultServlet if the default servlet should be registered
*/
default void setRegisterDefaultServlet(boolean registerDefaultServlet) {
getSettings().setRegisterDefaultServlet(registerDefaultServlet);
}
/**
* Sets the mime-type mappings.
* @param mimeMappings the mime type mappings (defaults to
* {@link MimeMappings#DEFAULT})
*/
default void setMimeMappings(MimeMappings mimeMappings) {
getSettings().setMimeMappings(mimeMappings);
}
/**
* Adds mime-type mappings.
* @param mimeMappings the mime type mappings to add
* @since 4.0.0
*/
default void addMimeMappings(MimeMappings mimeMappings) {
getSettings().addMimeMappings(mimeMappings);
}
/**
* Sets the document root directory which will be used by the web context to serve
* static files.
* @param documentRoot the document root or {@code null} if not required
*/
default void setDocumentRoot(File documentRoot) {
getSettings().setDocumentRoot(documentRoot);
}
/**
* Sets {@link ServletContextInitializer} that should be applied in addition to
* {@link ServletWebServerFactory#getWebServer(ServletContextInitializer...)}
* parameters. This method will replace any previously set or added initializers.
* @param initializers the initializers to set
* @see #addInitializers
*/
default void setInitializers(List<? extends ServletContextInitializer> initializers) {
getSettings().setInitializers(initializers);
}
/**
* Add {@link ServletContextInitializer}s to those that should be applied in addition
* to {@link ServletWebServerFactory#getWebServer(ServletContextInitializer...)}
* parameters.
* @param initializers the initializers to add
* @see #setInitializers
*/
default void addInitializers(ServletContextInitializer... initializers) {
getSettings().addInitializers(initializers);
}
/**
* Sets the configuration that will be applied to the server's JSP servlet.
* @param jsp the JSP servlet configuration
*/
default void setJsp(Jsp jsp) {
getSettings().setJsp(jsp);
}
/**
* Sets the Locale to Charset mappings.
* @param localeCharsetMappings the Locale to Charset mappings
*/
default void setLocaleCharsetMappings(Map<Locale, Charset> localeCharsetMappings) {
getSettings().setLocaleCharsetMappings(localeCharsetMappings);
}
/**
* Sets the init parameters that are applied to the container's
* {@link ServletContext}.
* @param initParameters the init parameters
*/
default void setInitParameters(Map<String, String> initParameters) {
getSettings().setInitParameters(initParameters);
}
/**
* Sets {@link CookieSameSiteSupplier CookieSameSiteSuppliers} that should be used to
* obtain the {@link SameSite} attribute of any added cookie. This method will replace
* any previously set or added suppliers.
* @param cookieSameSiteSuppliers the suppliers to add
* @see #addCookieSameSiteSuppliers
*/
default void setCookieSameSiteSuppliers(List<? extends CookieSameSiteSupplier> cookieSameSiteSuppliers) {
getSettings().setCookieSameSiteSuppliers(cookieSameSiteSuppliers);
}
/**
* Add {@link CookieSameSiteSupplier CookieSameSiteSuppliers} to those that should be
* used to obtain the {@link SameSite} attribute of any added cookie.
* @param cookieSameSiteSuppliers the suppliers to add
* @see #setCookieSameSiteSuppliers
*/
default void addCookieSameSiteSuppliers(CookieSameSiteSupplier... cookieSameSiteSuppliers) {
getSettings().addCookieSameSiteSuppliers(cookieSameSiteSuppliers);
}
@Override
default void addWebListeners(String... webListenerClassNames) {
getSettings().addWebListenerClassNames(webListenerClassNames);
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.servlet;
import org.springframework.util.Assert;
/**
* The context path of a servlet web server.
*
* @author Andy Wilkinson
* @since 4.0.0
*/
public final class ContextPath {
/**
* The default context path.
*/
public static final ContextPath DEFAULT = ContextPath.of("");
private final String path;
private ContextPath(String path) {
this.path = path;
}
public static ContextPath of(String contextPath) {
Assert.notNull(contextPath, "'contextPath' must not be null");
if (!contextPath.isEmpty()) {
if ("/".equals(contextPath)) {
throw new IllegalArgumentException("Root context path must be specified using an empty string");
}
if (!contextPath.startsWith("/") || contextPath.endsWith("/")) {
throw new IllegalArgumentException("Context path must start with '/' and not end with '/'");
}
}
return new ContextPath(contextPath);
}
@Override
public String toString() {
return this.path;
}
}

View File

@@ -0,0 +1,149 @@
/*
* 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.servlet;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import jakarta.servlet.http.Cookie;
import org.springframework.boot.web.server.Cookie.SameSite;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Strategy interface that can be used with {@link ConfigurableServletWebServerFactory}
* implementations in order to supply custom {@link SameSite} values for specific
* {@link Cookie cookies}.
* <p>
* Basic CookieSameSiteSupplier implementations can be constructed using the {@code of...}
* factory methods, typically combined with name matching. For example: <pre class="code">
* CookieSameSiteSupplier.ofLax().whenHasName("mycookie");
* </pre>
*
* @author Phillip Webb
* @since 4.0.0
* @see ConfigurableServletWebServerFactory#addCookieSameSiteSuppliers(CookieSameSiteSupplier...)
*/
@FunctionalInterface
public interface CookieSameSiteSupplier {
/**
* Get the {@link SameSite} values that should be used for the given {@link Cookie}.
* @param cookie the cookie to check
* @return the {@link SameSite} value to use or {@code null} if the next supplier
* should be checked
*/
SameSite getSameSite(Cookie cookie);
/**
* Limit this supplier so that it's only called if the Cookie has the given name.
* @param name the name to check
* @return a new {@link CookieSameSiteSupplier} that only calls this supplier when the
* name matches
*/
default CookieSameSiteSupplier whenHasName(String name) {
Assert.hasText(name, "'name' must not be empty");
return when((cookie) -> ObjectUtils.nullSafeEquals(cookie.getName(), name));
}
/**
* Limit this supplier so that it's only called if the Cookie has the given name.
* @param nameSupplier a supplier providing the name to check
* @return a new {@link CookieSameSiteSupplier} that only calls this supplier when the
* name matches
*/
default CookieSameSiteSupplier whenHasName(Supplier<String> nameSupplier) {
Assert.notNull(nameSupplier, "'nameSupplier' must not be null");
return when((cookie) -> ObjectUtils.nullSafeEquals(cookie.getName(), nameSupplier.get()));
}
/**
* Limit this supplier so that it's only called if the Cookie name matches the given
* regex.
* @param regex the regex pattern that must match
* @return a new {@link CookieSameSiteSupplier} that only calls this supplier when the
* name matches the regex
*/
default CookieSameSiteSupplier whenHasNameMatching(String regex) {
Assert.hasText(regex, "'regex' must not be empty");
return whenHasNameMatching(Pattern.compile(regex));
}
/**
* Limit this supplier so that it's only called if the Cookie name matches the given
* {@link Pattern}.
* @param pattern the regex pattern that must match
* @return a new {@link CookieSameSiteSupplier} that only calls this supplier when the
* name matches the pattern
*/
default CookieSameSiteSupplier whenHasNameMatching(Pattern pattern) {
Assert.notNull(pattern, "'pattern' must not be null");
return when((cookie) -> pattern.matcher(cookie.getName()).matches());
}
/**
* Limit this supplier so that it's only called if the predicate accepts the Cookie.
* @param predicate the predicate used to match the cookie
* @return a new {@link CookieSameSiteSupplier} that only calls this supplier when the
* cookie matches the predicate
*/
default CookieSameSiteSupplier when(Predicate<Cookie> predicate) {
Assert.notNull(predicate, "'predicate' must not be null");
return (cookie) -> predicate.test(cookie) ? getSameSite(cookie) : null;
}
/**
* Return a new {@link CookieSameSiteSupplier} that always returns
* {@link SameSite#NONE}.
* @return the supplier instance
*/
static CookieSameSiteSupplier ofNone() {
return of(SameSite.NONE);
}
/**
* Return a new {@link CookieSameSiteSupplier} that always returns
* {@link SameSite#LAX}.
* @return the supplier instance
*/
static CookieSameSiteSupplier ofLax() {
return of(SameSite.LAX);
}
/**
* Return a new {@link CookieSameSiteSupplier} that always returns
* {@link SameSite#STRICT}.
* @return the supplier instance
*/
static CookieSameSiteSupplier ofStrict() {
return of(SameSite.STRICT);
}
/**
* Return a new {@link CookieSameSiteSupplier} that always returns the given
* {@link SameSite} value.
* @param sameSite the value to return
* @return the supplier instance
*/
static CookieSameSiteSupplier of(SameSite sameSite) {
Assert.notNull(sameSite, "'sameSite' must not be null");
return (cookie) -> sameSite;
}
}

View File

@@ -0,0 +1,153 @@
/*
* 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.servlet;
import java.io.File;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.CodeSource;
import java.util.Arrays;
import java.util.Locale;
import org.apache.commons.logging.Log;
/**
* Manages a {@link ServletWebServerFactory} document root.
*
* @author Phillip Webb
* @since 4.0.0
*/
public class DocumentRoot {
private static final String[] COMMON_DOC_ROOTS = { "src/main/webapp", "public", "static" };
private final Log logger;
private File directory;
public DocumentRoot(Log logger) {
this.logger = logger;
}
File getDirectory() {
return this.directory;
}
public void setDirectory(File directory) {
this.directory = directory;
}
/**
* Returns the absolute document root when it points to a valid directory, logging a
* warning and returning {@code null} otherwise.
* @return the valid document root
*/
public final File getValidDirectory() {
File file = this.directory;
file = (file != null) ? file : getWarFileDocumentRoot();
file = (file != null) ? file : getExplodedWarFileDocumentRoot();
file = (file != null) ? file : getCommonDocumentRoot();
if (file == null && this.logger.isDebugEnabled()) {
logNoDocumentRoots();
}
else if (this.logger.isDebugEnabled()) {
this.logger.debug("Document root: " + file);
}
return file;
}
private File getWarFileDocumentRoot() {
return getArchiveFileDocumentRoot(".war");
}
private File getArchiveFileDocumentRoot(String extension) {
File file = getCodeSourceArchive();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Code archive: " + file);
}
if (file != null && file.exists() && !file.isDirectory()
&& file.getName().toLowerCase(Locale.ENGLISH).endsWith(extension)) {
return file.getAbsoluteFile();
}
return null;
}
private File getExplodedWarFileDocumentRoot() {
return getExplodedWarFileDocumentRoot(getCodeSourceArchive());
}
private File getCodeSourceArchive() {
return getCodeSourceArchive(getClass().getProtectionDomain().getCodeSource());
}
File getCodeSourceArchive(CodeSource codeSource) {
try {
URL location = (codeSource != null) ? codeSource.getLocation() : null;
if (location == null) {
return null;
}
String path;
URLConnection connection = location.openConnection();
if (connection instanceof JarURLConnection jarURLConnection) {
path = jarURLConnection.getJarFile().getName();
}
else {
path = location.toURI().getPath();
}
int index = path.indexOf("!/");
if (index != -1) {
path = path.substring(0, index);
}
return new File(path);
}
catch (Exception ex) {
return null;
}
}
final File getExplodedWarFileDocumentRoot(File codeSourceFile) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Code archive: " + codeSourceFile);
}
if (codeSourceFile != null && codeSourceFile.exists()) {
String path = codeSourceFile.getAbsolutePath();
int webInfPathIndex = path.indexOf(File.separatorChar + "WEB-INF" + File.separatorChar);
if (webInfPathIndex >= 0) {
path = path.substring(0, webInfPathIndex);
return new File(path);
}
}
return null;
}
private File getCommonDocumentRoot() {
for (String commonDocRoot : COMMON_DOC_ROOTS) {
File root = new File(commonDocRoot);
if (root.exists() && root.isDirectory()) {
return root.getAbsoluteFile();
}
}
return null;
}
private void logNoDocumentRoots() {
this.logger.debug("None of the document roots " + Arrays.asList(COMMON_DOC_ROOTS)
+ " point to a directory and will be ignored.");
}
}

View File

@@ -0,0 +1,89 @@
/*
* 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.servlet;
import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationPropertiesSource;
/**
* Configuration for the server's JSP servlet.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
* @since 4.0.0
*/
@ConfigurationPropertiesSource
public class Jsp {
/**
* Class name of the servlet to use for JSPs. If registered is true and this class is
* on the classpath then it will be registered.
*/
private String className = "org.apache.jasper.servlet.JspServlet";
private Map<String, String> initParameters = new HashMap<>();
/**
* Whether the JSP servlet is registered.
*/
private boolean registered = true;
public Jsp() {
this.initParameters.put("development", "false");
}
/**
* Return the class name of the servlet to use for JSPs. If {@link #getRegistered()
* registered} is {@code true} and this class is on the classpath then it will be
* registered.
* @return the class name of the servlet to use for JSPs
*/
public String getClassName() {
return this.className;
}
public void setClassName(String className) {
this.className = className;
}
/**
* Return the init parameters used to configure the JSP servlet.
* @return the init parameters
*/
public Map<String, String> getInitParameters() {
return this.initParameters;
}
public void setInitParameters(Map<String, String> initParameters) {
this.initParameters = initParameters;
}
/**
* Return whether the JSP servlet is registered.
* @return {@code true} to register the JSP servlet
*/
public boolean getRegistered() {
return this.registered;
}
public void setRegistered(boolean registered) {
this.registered = registered;
}
}

View File

@@ -0,0 +1,116 @@
/*
* 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.servlet;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.SessionCookieConfig;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.web.server.Cookie;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.ServletContextInitializer;
/**
* The {@link ServletContextInitializer ServletContextInitializers} to apply to a servlet
* {@link WebServer}.
*
* @author Andy Wilkinson
* @since 4.0.0
*/
public final class ServletContextInitializers implements Iterable<ServletContextInitializer> {
private final List<ServletContextInitializer> initializers;
private ServletContextInitializers(List<ServletContextInitializer> initializers) {
this.initializers = initializers;
}
@Override
public Iterator<ServletContextInitializer> iterator() {
return this.initializers.iterator();
}
/**
* Creates a new instance from the given {@code settings} and {@code initializers}.
* @param settings the settings
* @param initializers the initializers
* @return the new instance
*/
public static ServletContextInitializers from(ServletWebServerSettings settings,
ServletContextInitializer... initializers) {
List<ServletContextInitializer> mergedInitializers = new ArrayList<>();
mergedInitializers
.add((servletContext) -> settings.getInitParameters().forEach(servletContext::setInitParameter));
mergedInitializers.add(new SessionConfiguringInitializer(settings.getSession()));
mergedInitializers.addAll(Arrays.asList(initializers));
mergedInitializers.addAll(settings.getInitializers());
return new ServletContextInitializers(mergedInitializers);
}
private static final class SessionConfiguringInitializer implements ServletContextInitializer {
private final Session session;
private SessionConfiguringInitializer(Session session) {
this.session = session;
}
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
if (this.session.getTrackingModes() != null) {
servletContext.setSessionTrackingModes(unwrap(this.session.getTrackingModes()));
}
configureSessionCookie(servletContext.getSessionCookieConfig());
}
private void configureSessionCookie(SessionCookieConfig config) {
Cookie cookie = this.session.getCookie();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(cookie::getName).to(config::setName);
map.from(cookie::getDomain).to(config::setDomain);
map.from(cookie::getPath).to(config::setPath);
map.from(cookie::getHttpOnly).to(config::setHttpOnly);
map.from(cookie::getSecure).to(config::setSecure);
map.from(cookie::getMaxAge).asInt(Duration::getSeconds).to(config::setMaxAge);
map.from(cookie::getPartitioned)
.as(Object::toString)
.to((partitioned) -> config.setAttribute("Partitioned", partitioned));
}
private Set<jakarta.servlet.SessionTrackingMode> unwrap(Set<Session.SessionTrackingMode> modes) {
if (modes == null) {
return null;
}
Set<jakarta.servlet.SessionTrackingMode> result = new LinkedHashSet<>();
for (Session.SessionTrackingMode mode : modes) {
result.add(jakarta.servlet.SessionTrackingMode.valueOf(mode.name()));
}
return result;
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.servlet;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerFactory;
import org.springframework.boot.web.servlet.ServletContextInitializer;
/**
* Factory interface that can be used to create a {@link WebServer}.
*
* @author Phillip Webb
* @since 4.0.0
* @see WebServer
*/
@FunctionalInterface
public interface ServletWebServerFactory extends WebServerFactory {
/**
* Gets a new fully configured but paused {@link WebServer} instance. Clients should
* not be able to connect to the returned server until {@link WebServer#start()} is
* called (which happens when the {@code ApplicationContext} has been fully
* refreshed).
* @param initializers {@link ServletContextInitializer}s that should be applied as
* the server starts
* @return a fully configured and started {@link WebServer}
* @see WebServer#stop()
*/
WebServer getWebServer(ServletContextInitializer... initializers);
}

View File

@@ -0,0 +1,189 @@
/*
* 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.servlet;
import java.io.File;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.web.server.MimeMappings;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.util.Assert;
/**
* Settings for a servlet {@link WebServer} to be created by a
* {@link ConfigurableServletWebServerFactory}.
*
* @author Andy Wilkinson
* @since 4.0.0
*/
public class ServletWebServerSettings {
private ContextPath contextPath = ContextPath.DEFAULT;
private String displayName;
private Session session = new Session();
private boolean registerDefaultServlet;
private MimeMappings mimeMappings = MimeMappings.lazyCopy(MimeMappings.DEFAULT);
private File documentRoot;
private List<ServletContextInitializer> initializers = new ArrayList<>();
private Jsp jsp = new Jsp();
private Map<Locale, Charset> localeCharsetMappings = new HashMap<>();
private Map<String, String> initParameters = new HashMap<>();
private List<CookieSameSiteSupplier> cookieSameSiteSuppliers = new ArrayList<>();
private final Set<String> webListenerClassNames = new HashSet<>();
private final StaticResourceJars staticResourceJars = new StaticResourceJars();
public ContextPath getContextPath() {
return this.contextPath;
}
public void setContextPath(ContextPath contextPath) {
this.contextPath = contextPath;
}
public String getDisplayName() {
return this.displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public Session getSession() {
return this.session;
}
public void setSession(Session session) {
this.session = session;
}
public boolean isRegisterDefaultServlet() {
return this.registerDefaultServlet;
}
public void setRegisterDefaultServlet(boolean registerDefaultServlet) {
this.registerDefaultServlet = registerDefaultServlet;
}
public MimeMappings getMimeMappings() {
return this.mimeMappings;
}
public File getDocumentRoot() {
return this.documentRoot;
}
public void setDocumentRoot(File documentRoot) {
this.documentRoot = documentRoot;
}
public List<? extends ServletContextInitializer> getInitializers() {
return this.initializers;
}
public void setJsp(Jsp jsp) {
this.jsp = jsp;
}
public Jsp getJsp() {
return this.jsp;
}
public Map<Locale, Charset> getLocaleCharsetMappings() {
return this.localeCharsetMappings;
}
public Map<String, String> getInitParameters() {
return this.initParameters;
}
public List<? extends CookieSameSiteSupplier> getCookieSameSiteSuppliers() {
return this.cookieSameSiteSuppliers;
}
public void setMimeMappings(MimeMappings mimeMappings) {
Assert.notNull(mimeMappings, "'mimeMappings' must not be null");
this.mimeMappings = new MimeMappings(mimeMappings);
}
public void addMimeMappings(MimeMappings mimeMappings) {
mimeMappings.forEach((mapping) -> this.mimeMappings.add(mapping.getExtension(), mapping.getMimeType()));
}
public void setInitializers(List<? extends ServletContextInitializer> initializers) {
Assert.notNull(initializers, "'initializers' must not be null");
this.initializers = new ArrayList<>(initializers);
}
public void addInitializers(ServletContextInitializer... initializers) {
Assert.notNull(initializers, "'initializers' must not be null");
this.initializers.addAll(Arrays.asList(initializers));
}
public void setLocaleCharsetMappings(Map<Locale, Charset> localeCharsetMappings) {
Assert.notNull(localeCharsetMappings, "'localeCharsetMappings' must not be null");
this.localeCharsetMappings = localeCharsetMappings;
}
public void setInitParameters(Map<String, String> initParameters) {
this.initParameters = initParameters;
}
public void setCookieSameSiteSuppliers(List<? extends CookieSameSiteSupplier> cookieSameSiteSuppliers) {
Assert.notNull(cookieSameSiteSuppliers, "'cookieSameSiteSuppliers' must not be null");
this.cookieSameSiteSuppliers = new ArrayList<>(cookieSameSiteSuppliers);
}
public void addCookieSameSiteSuppliers(CookieSameSiteSupplier... cookieSameSiteSuppliers) {
Assert.notNull(cookieSameSiteSuppliers, "'cookieSameSiteSuppliers' must not be null");
this.cookieSameSiteSuppliers.addAll(Arrays.asList(cookieSameSiteSuppliers));
}
public void addWebListenerClassNames(String... webListenerClassNames) {
this.webListenerClassNames.addAll(Arrays.asList(webListenerClassNames));
}
public Set<String> getWebListenerClassNames() {
return this.webListenerClassNames;
}
public List<URL> getStaticResourceUrls() {
return this.staticResourceJars.getUrls();
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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.servlet;
import java.io.File;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Set;
import org.springframework.boot.context.properties.ConfigurationPropertiesSource;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.boot.web.server.Cookie;
/**
* Session properties.
*
* @author Andy Wilkinson
* @since 4.0.0
*/
@ConfigurationPropertiesSource
public class Session {
/**
* Session timeout. If a duration suffix is not specified, seconds will be used.
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration timeout = Duration.ofMinutes(30);
/**
* Session tracking modes.
*/
private Set<Session.SessionTrackingMode> trackingModes;
/**
* Whether to persist session data between restarts.
*/
private boolean persistent;
/**
* Directory used to store session data.
*/
private File storeDir;
@NestedConfigurationProperty
private final Cookie cookie = new Cookie();
private final SessionStoreDirectory sessionStoreDirectory = new SessionStoreDirectory();
public Duration getTimeout() {
return this.timeout;
}
public void setTimeout(Duration timeout) {
this.timeout = timeout;
}
/**
* Return the {@link SessionTrackingMode session tracking modes}.
* @return the session tracking modes
*/
public Set<Session.SessionTrackingMode> getTrackingModes() {
return this.trackingModes;
}
public void setTrackingModes(Set<Session.SessionTrackingMode> trackingModes) {
this.trackingModes = trackingModes;
}
/**
* Return whether to persist session data between restarts.
* @return {@code true} to persist session data between restarts.
*/
public boolean isPersistent() {
return this.persistent;
}
public void setPersistent(boolean persistent) {
this.persistent = persistent;
}
/**
* Return the directory used to store session data.
* @return the session data store directory
*/
public File getStoreDir() {
return this.storeDir;
}
public void setStoreDir(File storeDir) {
this.sessionStoreDirectory.setDirectory(storeDir);
this.storeDir = storeDir;
}
public Cookie getCookie() {
return this.cookie;
}
public SessionStoreDirectory getSessionStoreDirectory() {
return this.sessionStoreDirectory;
}
/**
* Available session tracking modes (mirrors
* {@link jakarta.servlet.SessionTrackingMode}).
*/
public enum SessionTrackingMode {
/**
* Send a cookie in response to the client's first request.
*/
COOKIE,
/**
* Rewrite the URL to append a session ID.
*/
URL,
/**
* Use SSL build-in mechanism to track the session.
*/
SSL
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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.servlet;
import java.io.File;
import org.springframework.boot.system.ApplicationHome;
import org.springframework.boot.system.ApplicationTemp;
import org.springframework.util.Assert;
/**
* Manages a session store directory.
*
* @author Phillip Webb
* @since 4.0.0
*/
public class SessionStoreDirectory {
private File directory;
File getDirectory() {
return this.directory;
}
void setDirectory(File directory) {
this.directory = directory;
}
public File getValidDirectory(boolean mkdirs) {
File dir = getDirectory();
if (dir == null) {
return new ApplicationTemp().getDir("servlet-sessions");
}
if (!dir.isAbsolute()) {
dir = new File(new ApplicationHome().getDir(), dir.getPath());
}
if (!dir.exists() && mkdirs) {
dir.mkdirs();
}
assertDirectory(mkdirs, dir);
return dir;
}
private void assertDirectory(boolean mkdirs, File dir) {
Assert.state(!mkdirs || dir.exists(), () -> "Session dir " + dir + " does not exist");
Assert.state(!dir.isFile(), () -> "Session dir " + dir + " points to a file");
}
}

View File

@@ -0,0 +1,145 @@
/*
* 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.servlet;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.nio.file.InvalidPathException;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarFile;
import java.util.stream.Stream;
/**
* Logic to extract URLs of static resource jars (those containing
* {@code "META-INF/resources"} directories).
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
class StaticResourceJars {
List<URL> getUrls() {
ClassLoader classLoader = getClass().getClassLoader();
if (classLoader instanceof URLClassLoader urlClassLoader) {
return getUrlsFrom(urlClassLoader.getURLs());
}
else {
return getUrlsFrom(Stream.of(ManagementFactory.getRuntimeMXBean().getClassPath().split(File.pathSeparator))
.map(this::toUrl)
.toArray(URL[]::new));
}
}
List<URL> getUrlsFrom(URL... urls) {
List<URL> resourceJarUrls = new ArrayList<>();
for (URL url : urls) {
addUrl(resourceJarUrls, url);
}
return resourceJarUrls;
}
private URL toUrl(String classPathEntry) {
try {
return new File(classPathEntry).toURI().toURL();
}
catch (MalformedURLException ex) {
throw new IllegalArgumentException("URL could not be created from '" + classPathEntry + "'", ex);
}
}
private File toFile(URL url) {
try {
return new File(url.toURI());
}
catch (URISyntaxException ex) {
throw new IllegalStateException("Failed to create File from URL '" + url + "'");
}
catch (IllegalArgumentException ex) {
return null;
}
}
private void addUrl(List<URL> urls, URL url) {
try {
if (!"file".equals(url.getProtocol())) {
addUrlConnection(urls, url, url.openConnection());
}
else {
File file = toFile(url);
if (file != null) {
addUrlFile(urls, url, file);
}
else {
addUrlConnection(urls, url, url.openConnection());
}
}
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
private void addUrlFile(List<URL> urls, URL url, File file) {
if ((file.isDirectory() && new File(file, "META-INF/resources").isDirectory()) || isResourcesJar(file)) {
urls.add(url);
}
}
private void addUrlConnection(List<URL> urls, URL url, URLConnection connection) {
if (connection instanceof JarURLConnection jarURLConnection && isResourcesJar(jarURLConnection)) {
urls.add(url);
}
}
private boolean isResourcesJar(JarURLConnection connection) {
try {
return isResourcesJar(connection.getJarFile(), !connection.getUseCaches());
}
catch (IOException ex) {
return false;
}
}
private boolean isResourcesJar(File file) {
try {
return isResourcesJar(new JarFile(file), true);
}
catch (IOException | InvalidPathException ex) {
return false;
}
}
private boolean isResourcesJar(JarFile jarFile, boolean closeJarFile) throws IOException {
try {
return jarFile.getName().endsWith(".jar") && (jarFile.getJarEntry("META-INF/resources") != null);
}
finally {
if (closeJarFile) {
jarFile.close();
}
}
}
}

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.servlet;
import jakarta.servlet.annotation.WebListener;
/**
* Interface to be implemented by types that register {@link WebListener @WebListeners}.
*
* @author Andy Wilkinson
* @since 2.4.0
*/
public interface WebListenerRegistrar {
/**
* Register web listeners with the given registry.
* @param registry the web listener registry
*/
void register(WebListenerRegistry registry);
}

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.servlet;
import jakarta.servlet.annotation.WebListener;
/**
* A registry that holds {@link WebListener @WebListeners}.
*
* @author Andy Wilkinson
* @since 4.0.0
*/
public interface WebListenerRegistry {
/**
* Adds web listeners that will be registered with the servlet web server.
* @param webListenerClassNames the class names of the web listeners
*/
void addWebListeners(String... webListenerClassNames);
}

View File

@@ -0,0 +1,208 @@
/*
* 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.servlet.context;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.annotation.AnnotatedBeanDefinitionReader;
import org.springframework.context.annotation.AnnotationConfigRegistry;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.annotation.AnnotationScopeMetadataResolver;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.context.annotation.ScopeMetadataResolver;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* {@link ServletWebServerApplicationContext} that accepts annotated classes as input - in
* particular {@link org.springframework.context.annotation.Configuration @Configuration}
* -annotated classes, but also plain {@link Component @Component} classes and JSR-330
* compliant classes using {@code javax.inject} annotations. Allows for registering
* classes one by one (specifying class names as config location) as well as for classpath
* scanning (specifying base packages as config location).
* <p>
* Note: In case of multiple {@code @Configuration} classes, later {@code @Bean}
* definitions will override ones defined in earlier loaded files. This can be leveraged
* to deliberately override certain bean definitions through an extra Configuration class.
*
* @author Phillip Webb
* @since 1.0.0
* @see #register(Class...)
* @see #scan(String...)
* @see ServletWebServerApplicationContext
*/
public class AnnotationConfigServletWebServerApplicationContext extends ServletWebServerApplicationContext
implements AnnotationConfigRegistry {
private final AnnotatedBeanDefinitionReader reader;
private final ClassPathBeanDefinitionScanner scanner;
private final Set<Class<?>> annotatedClasses = new LinkedHashSet<>();
private String[] basePackages;
/**
* Create a new {@link AnnotationConfigServletWebServerApplicationContext} that needs
* to be populated through {@link #register} calls and then manually
* {@linkplain #refresh refreshed}.
*/
public AnnotationConfigServletWebServerApplicationContext() {
this.reader = new AnnotatedBeanDefinitionReader(this);
this.scanner = new ClassPathBeanDefinitionScanner(this);
}
/**
* Create a new {@link AnnotationConfigServletWebServerApplicationContext} with the
* given {@code DefaultListableBeanFactory}. The context needs to be populated through
* {@link #register} calls and then manually {@linkplain #refresh refreshed}.
* @param beanFactory the DefaultListableBeanFactory instance to use for this context
*/
public AnnotationConfigServletWebServerApplicationContext(DefaultListableBeanFactory beanFactory) {
super(beanFactory);
this.reader = new AnnotatedBeanDefinitionReader(this);
this.scanner = new ClassPathBeanDefinitionScanner(this);
}
/**
* Create a new {@link AnnotationConfigServletWebServerApplicationContext}, deriving
* bean definitions from the given annotated classes and automatically refreshing the
* context.
* @param annotatedClasses one or more annotated classes, e.g. {@code @Configuration}
* classes
*/
public AnnotationConfigServletWebServerApplicationContext(Class<?>... annotatedClasses) {
this();
register(annotatedClasses);
refresh();
}
/**
* Create a new {@link AnnotationConfigServletWebServerApplicationContext}, scanning
* for bean definitions in the given packages and automatically refreshing the
* context.
* @param basePackages the packages to check for annotated classes
*/
public AnnotationConfigServletWebServerApplicationContext(String... basePackages) {
this();
scan(basePackages);
refresh();
}
/**
* {@inheritDoc}
* <p>
* Delegates given environment to underlying {@link AnnotatedBeanDefinitionReader} and
* {@link ClassPathBeanDefinitionScanner} members.
*/
@Override
public void setEnvironment(ConfigurableEnvironment environment) {
super.setEnvironment(environment);
this.reader.setEnvironment(environment);
this.scanner.setEnvironment(environment);
}
/**
* Provide a custom {@link BeanNameGenerator} for use with
* {@link AnnotatedBeanDefinitionReader} and/or
* {@link ClassPathBeanDefinitionScanner}, if any.
* <p>
* Default is
* {@link org.springframework.context.annotation.AnnotationBeanNameGenerator}.
* <p>
* Any call to this method must occur prior to calls to {@link #register(Class...)}
* and/or {@link #scan(String...)}.
* @param beanNameGenerator the bean name generator
* @see AnnotatedBeanDefinitionReader#setBeanNameGenerator
* @see ClassPathBeanDefinitionScanner#setBeanNameGenerator
*/
public void setBeanNameGenerator(BeanNameGenerator beanNameGenerator) {
this.reader.setBeanNameGenerator(beanNameGenerator);
this.scanner.setBeanNameGenerator(beanNameGenerator);
getBeanFactory().registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, beanNameGenerator);
}
/**
* Set the {@link ScopeMetadataResolver} to use for detected bean classes.
* <p>
* The default is an {@link AnnotationScopeMetadataResolver}.
* <p>
* Any call to this method must occur prior to calls to {@link #register(Class...)}
* and/or {@link #scan(String...)}.
* @param scopeMetadataResolver the scope metadata resolver
*/
public void setScopeMetadataResolver(ScopeMetadataResolver scopeMetadataResolver) {
this.reader.setScopeMetadataResolver(scopeMetadataResolver);
this.scanner.setScopeMetadataResolver(scopeMetadataResolver);
}
/**
* Register one or more annotated classes to be processed. Note that
* {@link #refresh()} must be called in order for the context to fully process the new
* class.
* <p>
* Calls to {@code #register} are idempotent; adding the same annotated class more
* than once has no additional effect.
* @param annotatedClasses one or more annotated classes, e.g. {@code @Configuration}
* classes
* @see #scan(String...)
* @see #refresh()
*/
@Override
public final void register(Class<?>... annotatedClasses) {
Assert.notEmpty(annotatedClasses, "'annotatedClasses' must not be empty");
this.annotatedClasses.addAll(Arrays.asList(annotatedClasses));
}
/**
* Perform a scan within the specified base packages. Note that {@link #refresh()}
* must be called in order for the context to fully process the new class.
* @param basePackages the packages to check for annotated classes
* @see #register(Class...)
* @see #refresh()
*/
@Override
public final void scan(String... basePackages) {
Assert.notEmpty(basePackages, "'basePackages' must not be empty");
this.basePackages = basePackages;
}
@Override
protected void prepareRefresh() {
this.scanner.clearCache();
super.prepareRefresh();
}
@Override
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
super.postProcessBeanFactory(beanFactory);
if (this.basePackages != null && this.basePackages.length > 0) {
this.scanner.scan(this.basePackages);
}
if (!this.annotatedClasses.isEmpty()) {
this.reader.register(ClassUtils.toClassArray(this.annotatedClasses));
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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.servlet.context;
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.Assert;
/**
* Abstract base class for handlers of Servlet components discovered through classpath
* scanning.
*
* @author Andy Wilkinson
*/
abstract class ServletComponentHandler {
private final Class<? extends Annotation> annotationType;
private final TypeFilter typeFilter;
protected ServletComponentHandler(Class<? extends Annotation> annotationType) {
this.typeFilter = new AnnotationTypeFilter(annotationType);
this.annotationType = annotationType;
}
TypeFilter getTypeFilter() {
return this.typeFilter;
}
protected String[] extractUrlPatterns(Map<String, Object> attributes) {
String[] value = (String[]) attributes.get("value");
String[] urlPatterns = (String[]) attributes.get("urlPatterns");
if (urlPatterns.length > 0) {
Assert.state(value.length == 0, "The urlPatterns and value attributes are mutually exclusive");
return urlPatterns;
}
return value;
}
protected final Map<String, String> extractInitParameters(Map<String, Object> attributes) {
Map<String, String> initParameters = new HashMap<>();
for (AnnotationAttributes initParam : (AnnotationAttributes[]) attributes.get("initParams")) {
String name = (String) initParam.get("name");
String value = (String) initParam.get("value");
initParameters.put(name, value);
}
return initParameters;
}
void handle(AnnotatedBeanDefinition beanDefinition, BeanDefinitionRegistry registry) {
Map<String, Object> attributes = beanDefinition.getMetadata()
.getAnnotationAttributes(this.annotationType.getName());
if (attributes != null) {
doHandle(attributes, beanDefinition, registry);
}
}
protected abstract void doHandle(Map<String, Object> attributes, AnnotatedBeanDefinition beanDefinition,
BeanDefinitionRegistry registry);
}

View File

@@ -0,0 +1,138 @@
/*
* 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.servlet.context;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.TypeReference;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.mock.web.MockServletContext;
import org.springframework.util.ClassUtils;
import org.springframework.web.context.WebApplicationContext;
/**
* {@link BeanFactoryPostProcessor} that registers beans for Servlet components found via
* package scanning.
*
* @author Andy Wilkinson
* @see ServletComponentScan
* @see ServletComponentScanRegistrar
*/
class ServletComponentRegisteringPostProcessor
implements BeanFactoryPostProcessor, ApplicationContextAware, BeanFactoryInitializationAotProcessor {
private static final boolean MOCK_SERVLET_CONTEXT_AVAILABLE = ClassUtils
.isPresent("org.springframework.mock.web.MockServletContext", null);
private static final List<ServletComponentHandler> HANDLERS;
static {
List<ServletComponentHandler> servletComponentHandlers = new ArrayList<>();
servletComponentHandlers.add(new WebServletHandler());
servletComponentHandlers.add(new WebFilterHandler());
servletComponentHandlers.add(new WebListenerHandler());
HANDLERS = Collections.unmodifiableList(servletComponentHandlers);
}
private final Set<String> packagesToScan;
private ApplicationContext applicationContext;
ServletComponentRegisteringPostProcessor(Set<String> packagesToScan) {
this.packagesToScan = packagesToScan;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (eligibleForServletComponentScanning()) {
ClassPathScanningCandidateComponentProvider componentProvider = createComponentProvider();
for (String packageToScan : this.packagesToScan) {
scanPackage(componentProvider, packageToScan);
}
}
}
private void scanPackage(ClassPathScanningCandidateComponentProvider componentProvider, String packageToScan) {
for (BeanDefinition candidate : componentProvider.findCandidateComponents(packageToScan)) {
if (candidate instanceof AnnotatedBeanDefinition annotatedBeanDefinition) {
for (ServletComponentHandler handler : HANDLERS) {
handler.handle(annotatedBeanDefinition, (BeanDefinitionRegistry) this.applicationContext);
}
}
}
}
private boolean eligibleForServletComponentScanning() {
return this.applicationContext instanceof WebApplicationContext webApplicationContext
&& (webApplicationContext.getServletContext() == null || (MOCK_SERVLET_CONTEXT_AVAILABLE
&& webApplicationContext.getServletContext() instanceof MockServletContext));
}
private ClassPathScanningCandidateComponentProvider createComponentProvider() {
ClassPathScanningCandidateComponentProvider componentProvider = new ClassPathScanningCandidateComponentProvider(
false);
componentProvider.setEnvironment(this.applicationContext.getEnvironment());
componentProvider.setResourceLoader(this.applicationContext);
for (ServletComponentHandler handler : HANDLERS) {
componentProvider.addIncludeFilter(handler.getTypeFilter());
}
return componentProvider;
}
Set<String> getPackagesToScan() {
return Collections.unmodifiableSet(this.packagesToScan);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
return (generationContext, beanFactoryInitializationCode) -> {
for (String beanName : beanFactory.getBeanDefinitionNames()) {
BeanDefinition definition = beanFactory.getBeanDefinition(beanName);
if (Objects.equals(definition.getBeanClassName(),
WebListenerHandler.ServletComponentWebListenerRegistrar.class.getName())) {
String listenerClassName = (String) definition.getConstructorArgumentValues()
.getArgumentValue(0, String.class)
.getValue();
generationContext.getRuntimeHints()
.reflection()
.registerType(TypeReference.of(listenerClassName), MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}
}
};
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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.servlet.context;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.annotation.WebServlet;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AliasFor;
/**
* Enables scanning for Servlet components ({@link WebFilter filters}, {@link WebServlet
* servlets}, and {@link WebListener listeners}). Scanning is only performed when using an
* embedded web server.
* <p>
* Typically, one of {@code value}, {@code basePackages}, or {@code basePackageClasses}
* should be specified to control the packages to be scanned for components. In their
* absence, scanning will be performed from the package of the class with the annotation.
*
* @author Andy Wilkinson
* @since 1.3.0
* @see WebServlet
* @see WebFilter
* @see WebListener
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ServletComponentScanRegistrar.class)
public @interface ServletComponentScan {
/**
* Alias for the {@link #basePackages()} attribute. Allows for more concise annotation
* declarations e.g.: {@code @ServletComponentScan("org.my.pkg")} instead of
* {@code @ServletComponentScan(basePackages="org.my.pkg")}.
* @return the base packages to scan
*/
@AliasFor("basePackages")
String[] value() default {};
/**
* Base packages to scan for annotated servlet components. {@link #value()} is an
* alias for (and mutually exclusive with) this attribute.
* <p>
* Use {@link #basePackageClasses()} for a type-safe alternative to String-based
* package names.
* @return the base packages to scan
*/
@AliasFor("value")
String[] basePackages() default {};
/**
* Type-safe alternative to {@link #basePackages()} for specifying the packages to
* scan for annotated servlet components. The package of each class specified will be
* scanned.
* @return classes from the base packages to scan
*/
Class<?>[] basePackageClasses() default {};
}

View File

@@ -0,0 +1,103 @@
/*
* 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.servlet.context;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.function.Supplier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.ClassUtils;
/**
* {@link ImportBeanDefinitionRegistrar} used by
* {@link ServletComponentScan @ServletComponentScan}.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
*/
class ServletComponentScanRegistrar implements ImportBeanDefinitionRegistrar {
private static final String BEAN_NAME = "servletComponentRegisteringPostProcessor";
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Set<String> packagesToScan = getPackagesToScan(importingClassMetadata);
if (registry.containsBeanDefinition(BEAN_NAME)) {
updatePostProcessor(registry, packagesToScan);
}
else {
addPostProcessor(registry, packagesToScan);
}
}
private void updatePostProcessor(BeanDefinitionRegistry registry, Set<String> packagesToScan) {
ServletComponentRegisteringPostProcessorBeanDefinition definition = (ServletComponentRegisteringPostProcessorBeanDefinition) registry
.getBeanDefinition(BEAN_NAME);
definition.addPackageNames(packagesToScan);
}
private void addPostProcessor(BeanDefinitionRegistry registry, Set<String> packagesToScan) {
ServletComponentRegisteringPostProcessorBeanDefinition definition = new ServletComponentRegisteringPostProcessorBeanDefinition(
packagesToScan);
registry.registerBeanDefinition(BEAN_NAME, definition);
}
private Set<String> getPackagesToScan(AnnotationMetadata metadata) {
AnnotationAttributes attributes = AnnotationAttributes
.fromMap(metadata.getAnnotationAttributes(ServletComponentScan.class.getName()));
String[] basePackages = attributes.getStringArray("basePackages");
Class<?>[] basePackageClasses = attributes.getClassArray("basePackageClasses");
Set<String> packagesToScan = new LinkedHashSet<>(Arrays.asList(basePackages));
for (Class<?> basePackageClass : basePackageClasses) {
packagesToScan.add(ClassUtils.getPackageName(basePackageClass));
}
if (packagesToScan.isEmpty()) {
packagesToScan.add(ClassUtils.getPackageName(metadata.getClassName()));
}
return packagesToScan;
}
static final class ServletComponentRegisteringPostProcessorBeanDefinition extends RootBeanDefinition {
private final Set<String> packageNames = new LinkedHashSet<>();
ServletComponentRegisteringPostProcessorBeanDefinition(Collection<String> packageNames) {
setBeanClass(ServletComponentRegisteringPostProcessor.class);
setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
addPackageNames(packageNames);
}
@Override
public Supplier<?> getInstanceSupplier() {
return () -> new ServletComponentRegisteringPostProcessor(this.packageNames);
}
private void addPackageNames(Collection<String> additionalPackageNames) {
this.packageNames.addAll(additionalPackageNames);
}
}
}

View File

@@ -0,0 +1,322 @@
/*
* 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.servlet.context;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import jakarta.servlet.Filter;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.boot.web.context.servlet.WebApplicationContextInitializer;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.context.ConfigurableWebServerApplicationContext;
import org.springframework.boot.web.server.context.MissingWebServerFactoryBeanException;
import org.springframework.boot.web.server.context.WebServerGracefulShutdownLifecycle;
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
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.ApplicationContext;
import org.springframework.context.ApplicationContextException;
import org.springframework.core.io.Resource;
import org.springframework.core.metrics.StartupStep;
import org.springframework.util.StringUtils;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.context.support.ServletContextAwareProcessor;
import org.springframework.web.context.support.ServletContextResource;
import org.springframework.web.context.support.WebApplicationContextUtils;
/**
* A {@link WebApplicationContext} that can be used to bootstrap itself from a contained
* {@link ServletWebServerFactory} bean.
* <p>
* This context will create, initialize and run an {@link WebServer} by searching for a
* single {@link ServletWebServerFactory} bean within the {@link ApplicationContext}
* itself. The {@link ServletWebServerFactory} is free to use standard Spring concepts
* (such as dependency injection, lifecycle callbacks and property placeholder variables).
* <p>
* In addition, any {@link Servlet} or {@link Filter} beans defined in the context will be
* automatically registered with the web server. In the case of a single Servlet bean, the
* '/' mapping will be used. If multiple Servlet beans are found then the lowercase bean
* name will be used as a mapping prefix. Any Servlet named 'dispatcherServlet' will
* always be mapped to '/'. Filter beans will be mapped to all URLs ('/*').
* <p>
* For more advanced configuration, the context can instead define beans that implement
* the {@link ServletContextInitializer} interface (most often
* {@link ServletRegistrationBean}s and/or {@link FilterRegistrationBean}s). To prevent
* double registration, the use of {@link ServletContextInitializer} beans will disable
* automatic Servlet and Filter bean registration.
* <p>
* Although this context can be used directly, most developers should consider using the
* {@link AnnotationConfigServletWebServerApplicationContext} or
* {@link XmlServletWebServerApplicationContext} variants.
*
* @author Phillip Webb
* @author Dave Syer
* @author Scott Frederick
* @since 2.0.0
* @see AnnotationConfigServletWebServerApplicationContext
* @see XmlServletWebServerApplicationContext
* @see ServletWebServerFactory
*/
public class ServletWebServerApplicationContext extends GenericWebApplicationContext
implements ConfigurableWebServerApplicationContext {
private static final Log logger = LogFactory.getLog(ServletWebServerApplicationContext.class);
/**
* Constant value for the DispatcherServlet bean name. A Servlet bean with this name
* is deemed to be the "main" servlet and is automatically given a mapping of "/" by
* default. To change the default behavior you can use a
* {@link ServletRegistrationBean} or a different bean name.
*/
public static final String DISPATCHER_SERVLET_NAME = "dispatcherServlet";
private volatile WebServer webServer;
private ServletConfig servletConfig;
private String serverNamespace;
/**
* Create a new {@link ServletWebServerApplicationContext}.
*/
public ServletWebServerApplicationContext() {
}
/**
* Create a new {@link ServletWebServerApplicationContext} with the given
* {@code DefaultListableBeanFactory}.
* @param beanFactory the DefaultListableBeanFactory instance to use for this context
*/
public ServletWebServerApplicationContext(DefaultListableBeanFactory beanFactory) {
super(beanFactory);
}
/**
* Register ServletContextAwareProcessor.
* @see ServletContextAwareProcessor
*/
@Override
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
beanFactory.addBeanPostProcessor(new WebApplicationContextServletContextAwareProcessor(this));
beanFactory.ignoreDependencyInterface(ServletContextAware.class);
registerWebApplicationScopes();
}
@Override
public final void refresh() throws BeansException, IllegalStateException {
try {
super.refresh();
}
catch (RuntimeException ex) {
WebServer webServer = this.webServer;
if (webServer != null) {
try {
webServer.stop();
webServer.destroy();
}
catch (RuntimeException stopOrDestroyEx) {
ex.addSuppressed(stopOrDestroyEx);
}
}
throw ex;
}
}
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
@Override
protected void doClose() {
if (isActive()) {
AvailabilityChangeEvent.publish(this, ReadinessState.REFUSING_TRAFFIC);
}
super.doClose();
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.destroy();
}
}
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
StartupStep createWebServer = getApplicationStartup().start("spring.boot.webserver.create");
ServletWebServerFactory factory = getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString());
this.webServer = factory.getWebServer(getSelfInitializer());
createWebServer.end();
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
/**
* Returns the {@link ServletWebServerFactory} that should be used to create the
* embedded {@link WebServer}. By default this method searches for a suitable bean in
* the context itself.
* @return a {@link ServletWebServerFactory} (never {@code null})
*/
protected ServletWebServerFactory getWebServerFactory() {
// Use bean names so that we don't consider the hierarchy
String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
if (beanNames.length == 0) {
throw new MissingWebServerFactoryBeanException(getClass(), ServletWebServerFactory.class,
WebApplicationType.SERVLET);
}
if (beanNames.length > 1) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "
+ "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
}
return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}
/**
* Returns the {@link ServletContextInitializer} that will be used to complete the
* setup of this {@link WebApplicationContext}.
* @return the self initializer
* @see #prepareWebApplicationContext(ServletContext)
*/
private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
return new WebApplicationContextInitializer(this)::initialize;
}
private void registerWebApplicationScopes() {
ExistingWebApplicationScopes existingScopes = new ExistingWebApplicationScopes(getBeanFactory());
WebApplicationContextUtils.registerWebApplicationScopes(getBeanFactory());
existingScopes.restore();
}
@Override
protected Resource getResourceByPath(String path) {
if (getServletContext() == null) {
return new ClassPathContextResource(path, getClassLoader());
}
return new ServletContextResource(getServletContext(), path);
}
@Override
public String getServerNamespace() {
return this.serverNamespace;
}
@Override
public void setServerNamespace(String serverNamespace) {
this.serverNamespace = serverNamespace;
}
@Override
public void setServletConfig(ServletConfig servletConfig) {
this.servletConfig = servletConfig;
}
@Override
public ServletConfig getServletConfig() {
return this.servletConfig;
}
/**
* Returns the {@link WebServer} that was created by the context or {@code null} if
* the server has not yet been created.
* @return the embedded web server
*/
@Override
public WebServer getWebServer() {
return this.webServer;
}
/**
* Utility class to store and restore any user defined scopes. This allows scopes to
* be registered in an ApplicationContextInitializer in the same way as they would in
* a classic non-embedded web application context.
*/
public static class ExistingWebApplicationScopes {
private static final Set<String> SCOPES;
static {
Set<String> scopes = new LinkedHashSet<>();
scopes.add(WebApplicationContext.SCOPE_REQUEST);
scopes.add(WebApplicationContext.SCOPE_SESSION);
SCOPES = Collections.unmodifiableSet(scopes);
}
private final ConfigurableListableBeanFactory beanFactory;
private final Map<String, Scope> scopes = new HashMap<>();
public ExistingWebApplicationScopes(ConfigurableListableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
for (String scopeName : SCOPES) {
Scope scope = beanFactory.getRegisteredScope(scopeName);
if (scope != null) {
this.scopes.put(scopeName, scope);
}
}
}
public void restore() {
this.scopes.forEach((key, value) -> {
if (logger.isInfoEnabled()) {
logger.info("Restoring user defined scope " + key);
}
this.beanFactory.registerScope(key, value);
});
}
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.servlet.context;
import org.springframework.aot.AotDetector;
import org.springframework.boot.ApplicationContextFactory;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.web.context.servlet.ApplicationServletEnvironment;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
/**
* {@link ApplicationContextFactory} registered in {@code spring.factories} to support
* {@link AnnotationConfigServletWebServerApplicationContext} and
* {@link ServletWebServerApplicationContext}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class ServletWebServerApplicationContextFactory implements ApplicationContextFactory {
@Override
public Class<? extends ConfigurableEnvironment> getEnvironmentType(WebApplicationType webApplicationType) {
return (webApplicationType != WebApplicationType.SERVLET) ? null : ApplicationServletEnvironment.class;
}
@Override
public ConfigurableEnvironment createEnvironment(WebApplicationType webApplicationType) {
return (webApplicationType != WebApplicationType.SERVLET) ? null : new ApplicationServletEnvironment();
}
@Override
public ConfigurableApplicationContext create(WebApplicationType webApplicationType) {
return (webApplicationType != WebApplicationType.SERVLET) ? null : createContext();
}
private ConfigurableApplicationContext createContext() {
if (!AotDetector.useGeneratedArtifacts()) {
return new AnnotationConfigServletWebServerApplicationContext();
}
return new ServletWebServerApplicationContext();
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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.servlet.context;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.context.WebServerInitializedEvent;
/**
* Event to be published after the {@link WebServer} is ready. Useful for obtaining the
* local port of a running server.
*
* <p>
* Normally it will have been started, but listeners are free to inspect the server and
* stop and start it if they want to.
*
* @author Dave Syer
* @since 2.0.0
*/
@SuppressWarnings("serial")
public class ServletWebServerInitializedEvent extends WebServerInitializedEvent {
private final ServletWebServerApplicationContext applicationContext;
public ServletWebServerInitializedEvent(WebServer webServer,
ServletWebServerApplicationContext applicationContext) {
super(webServer);
this.applicationContext = applicationContext;
}
/**
* Access the application context that the server was created in. Sometimes it is
* prudent to check that this matches expectations (like being equal to the current
* context) before acting on the server itself.
* @return the applicationContext that the server was created from
*/
@Override
public ServletWebServerApplicationContext getApplicationContext() {
return this.applicationContext;
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.servlet.context;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletContext;
import org.springframework.util.Assert;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import org.springframework.web.context.support.ServletContextAwareProcessor;
/**
* Variant of {@link ServletContextAwareProcessor} for use with a
* {@link ConfigurableWebApplicationContext}. Can be used when registering the processor
* can occur before the {@link ServletContext} or {@link ServletConfig} have been
* initialized.
*
* @author Phillip Webb
* @since 1.0.0
*/
public class WebApplicationContextServletContextAwareProcessor extends ServletContextAwareProcessor {
private final ConfigurableWebApplicationContext webApplicationContext;
public WebApplicationContextServletContextAwareProcessor(ConfigurableWebApplicationContext webApplicationContext) {
Assert.notNull(webApplicationContext, "'webApplicationContext' must not be null");
this.webApplicationContext = webApplicationContext;
}
@Override
protected ServletContext getServletContext() {
ServletContext servletContext = this.webApplicationContext.getServletContext();
return (servletContext != null) ? servletContext : super.getServletContext();
}
@Override
protected ServletConfig getServletConfig() {
ServletConfig servletConfig = this.webApplicationContext.getServletConfig();
return (servletConfig != null) ? servletConfig : super.getServletConfig();
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.servlet.context;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Map;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.annotation.WebFilter;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.util.StringUtils;
/**
* Handler for {@link WebFilter @WebFilter}-annotated classes.
*
* @author Andy Wilkinson
*/
class WebFilterHandler extends ServletComponentHandler {
WebFilterHandler() {
super(WebFilter.class);
}
@Override
public void doHandle(Map<String, Object> attributes, AnnotatedBeanDefinition beanDefinition,
BeanDefinitionRegistry registry) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(FilterRegistrationBean.class);
builder.addPropertyValue("asyncSupported", attributes.get("asyncSupported"));
builder.addPropertyValue("dispatcherTypes", extractDispatcherTypes(attributes));
builder.addPropertyValue("filter", beanDefinition);
builder.addPropertyValue("initParameters", extractInitParameters(attributes));
String name = determineName(attributes, beanDefinition);
builder.addPropertyValue("name", name);
builder.addPropertyValue("servletNames", attributes.get("servletNames"));
builder.addPropertyValue("urlPatterns", extractUrlPatterns(attributes));
registry.registerBeanDefinition(name, builder.getBeanDefinition());
}
private EnumSet<DispatcherType> extractDispatcherTypes(Map<String, Object> attributes) {
DispatcherType[] dispatcherTypes = (DispatcherType[]) attributes.get("dispatcherTypes");
if (dispatcherTypes.length == 0) {
return EnumSet.noneOf(DispatcherType.class);
}
if (dispatcherTypes.length == 1) {
return EnumSet.of(dispatcherTypes[0]);
}
return EnumSet.of(dispatcherTypes[0], Arrays.copyOfRange(dispatcherTypes, 1, dispatcherTypes.length));
}
private String determineName(Map<String, Object> attributes, BeanDefinition beanDefinition) {
return (String) (StringUtils.hasText((String) attributes.get("filterName")) ? attributes.get("filterName")
: beanDefinition.getBeanClassName());
}
}

View File

@@ -0,0 +1,64 @@
/*
* 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.servlet.context;
import java.util.Map;
import jakarta.servlet.annotation.WebListener;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.web.server.servlet.WebListenerRegistrar;
import org.springframework.boot.web.server.servlet.WebListenerRegistry;
/**
* Handler for {@link WebListener @WebListener}-annotated classes.
*
* @author Andy Wilkinson
*/
class WebListenerHandler extends ServletComponentHandler {
WebListenerHandler() {
super(WebListener.class);
}
@Override
protected void doHandle(Map<String, Object> attributes, AnnotatedBeanDefinition beanDefinition,
BeanDefinitionRegistry registry) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.rootBeanDefinition(ServletComponentWebListenerRegistrar.class);
builder.addConstructorArgValue(beanDefinition.getBeanClassName());
registry.registerBeanDefinition(beanDefinition.getBeanClassName() + "Registrar", builder.getBeanDefinition());
}
static class ServletComponentWebListenerRegistrar implements WebListenerRegistrar {
private final String listenerClassName;
ServletComponentWebListenerRegistrar(String listenerClassName) {
this.listenerClassName = listenerClassName;
}
@Override
public void register(WebListenerRegistry registry) {
registry.addWebListeners(this.listenerClassName);
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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.servlet.context;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.context.SmartLifecycle;
/**
* {@link SmartLifecycle} to start and stop the {@link WebServer} in a
* {@link ServletWebServerApplicationContext}.
*
* @author Andy Wilkinson
*/
class WebServerStartStopLifecycle implements SmartLifecycle {
private final ServletWebServerApplicationContext applicationContext;
private final WebServer webServer;
private volatile boolean running;
WebServerStartStopLifecycle(ServletWebServerApplicationContext applicationContext, WebServer webServer) {
this.applicationContext = applicationContext;
this.webServer = webServer;
}
@Override
public void start() {
this.webServer.start();
this.running = true;
this.applicationContext
.publishEvent(new ServletWebServerInitializedEvent(this.webServer, this.applicationContext));
}
@Override
public void stop() {
this.running = false;
this.webServer.stop();
}
@Override
public boolean isRunning() {
return this.running;
}
@Override
public int getPhase() {
return WebServerApplicationContext.START_STOP_LIFECYCLE_PHASE;
}
}

View File

@@ -0,0 +1,77 @@
/*
* 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.servlet.context;
import java.util.Map;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.annotation.WebServlet;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.util.StringUtils;
/**
* Handler for {@link WebServlet @WebServlet}-annotated classes.
*
* @author Andy Wilkinson
*/
class WebServletHandler extends ServletComponentHandler {
WebServletHandler() {
super(WebServlet.class);
}
@Override
public void doHandle(Map<String, Object> attributes, AnnotatedBeanDefinition beanDefinition,
BeanDefinitionRegistry registry) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ServletRegistrationBean.class);
builder.addPropertyValue("asyncSupported", attributes.get("asyncSupported"));
builder.addPropertyValue("initParameters", extractInitParameters(attributes));
builder.addPropertyValue("loadOnStartup", attributes.get("loadOnStartup"));
String name = determineName(attributes, beanDefinition);
builder.addPropertyValue("name", name);
builder.addPropertyValue("servlet", beanDefinition);
builder.addPropertyValue("urlMappings", extractUrlPatterns(attributes));
builder.addPropertyValue("multipartConfig", determineMultipartConfig(beanDefinition));
registry.registerBeanDefinition(name, builder.getBeanDefinition());
}
private String determineName(Map<String, Object> attributes, BeanDefinition beanDefinition) {
return (String) (StringUtils.hasText((String) attributes.get("name")) ? attributes.get("name")
: beanDefinition.getBeanClassName());
}
private BeanDefinition determineMultipartConfig(AnnotatedBeanDefinition beanDefinition) {
Map<String, Object> attributes = beanDefinition.getMetadata()
.getAnnotationAttributes(MultipartConfig.class.getName());
if (attributes == null) {
return null;
}
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(MultipartConfigElement.class);
builder.addConstructorArgValue(attributes.get("location"));
builder.addConstructorArgValue(attributes.get("maxFileSize"));
builder.addConstructorArgValue(attributes.get("maxRequestSize"));
builder.addConstructorArgValue(attributes.get("fileSizeThreshold"));
return builder.getBeanDefinition();
}
}

View File

@@ -0,0 +1,137 @@
/*
* 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.servlet.context;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.context.support.XmlWebApplicationContext;
/**
* {@link ServletWebServerApplicationContext} which takes its configuration from XML
* documents, understood by an
* {@link org.springframework.beans.factory.xml.XmlBeanDefinitionReader}.
* <p>
* Note: In case of multiple config locations, later bean definitions will override ones
* defined in earlier loaded files. This can be leveraged to deliberately override certain
* bean definitions through an extra XML file.
*
* @author Phillip Webb
* @since 1.0.0
* @see #setNamespace
* @see #setConfigLocations
* @see ServletWebServerApplicationContext
* @see XmlWebApplicationContext
*/
public class XmlServletWebServerApplicationContext extends ServletWebServerApplicationContext {
private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);
/**
* Create a new {@link XmlServletWebServerApplicationContext} that needs to be
* {@linkplain #load loaded} and then manually {@link #refresh refreshed}.
*/
public XmlServletWebServerApplicationContext() {
this.reader.setEnvironment(getEnvironment());
}
/**
* Create a new {@link XmlServletWebServerApplicationContext}, loading bean
* definitions from the given resources and automatically refreshing the context.
* @param resources the resources to load from
*/
public XmlServletWebServerApplicationContext(Resource... resources) {
load(resources);
refresh();
}
/**
* Create a new {@link XmlServletWebServerApplicationContext}, loading bean
* definitions from the given resource locations and automatically refreshing the
* context.
* @param resourceLocations the resources to load from
*/
public XmlServletWebServerApplicationContext(String... resourceLocations) {
load(resourceLocations);
refresh();
}
/**
* Create a new {@link XmlServletWebServerApplicationContext}, loading bean
* definitions from the given resource locations and automatically refreshing the
* context.
* @param relativeClass class whose package will be used as a prefix when loading each
* specified resource name
* @param resourceNames relatively-qualified names of resources to load
*/
public XmlServletWebServerApplicationContext(Class<?> relativeClass, String... resourceNames) {
load(relativeClass, resourceNames);
refresh();
}
/**
* Set whether to use XML validation. Default is {@code true}.
* @param validating if validating the XML
*/
public void setValidating(boolean validating) {
this.reader.setValidating(validating);
}
/**
* {@inheritDoc}
* <p>
* Delegates the given environment to underlying {@link XmlBeanDefinitionReader}.
* Should be called before any call to {@link #load}.
*/
@Override
public void setEnvironment(ConfigurableEnvironment environment) {
super.setEnvironment(environment);
this.reader.setEnvironment(getEnvironment());
}
/**
* Load bean definitions from the given XML resources.
* @param resources one or more resources to load from
*/
public final void load(Resource... resources) {
this.reader.loadBeanDefinitions(resources);
}
/**
* Load bean definitions from the given XML resources.
* @param resourceLocations one or more resource locations to load from
*/
public final void load(String... resourceLocations) {
this.reader.loadBeanDefinitions(resourceLocations);
}
/**
* Load bean definitions from the given XML resources.
* @param relativeClass class whose package will be used as a prefix when loading each
* specified resource name
* @param resourceNames relatively-qualified names of resources to load
*/
public final void load(Class<?> relativeClass, String... resourceNames) {
Resource[] resources = new Resource[resourceNames.length];
for (int i = 0; i < resourceNames.length; i++) {
resources[i] = new ClassPathResource(resourceNames[i], relativeClass);
}
this.reader.loadBeanDefinitions(resources);
}
}

View File

@@ -0,0 +1,21 @@
/*
* 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.
*/
/**
* Servlet web server based web integrations with Spring's
* {@link org.springframework.web.context.WebApplicationContext WebApplicationContext}.
*/
package org.springframework.boot.web.server.servlet.context;

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.
*/
/**
* Servlet web server abstractions.
*/
package org.springframework.boot.web.server.servlet;

View File

@@ -0,0 +1,13 @@
# Application Context Factories
org.springframework.boot.ApplicationContextFactory=\
org.springframework.boot.web.server.reactive.context.ReactiveWebServerApplicationContextFactory,\
org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContextFactory
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.web.server.context.ServerPortInfoApplicationContextInitializer
# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.web.server.PortInUseFailureAnalyzer,\
org.springframework.boot.web.server.context.MissingWebServerFactoryBeanFailureAnalyzer

View File

@@ -0,0 +1,2 @@
org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.boot.web.server.MimeMappings$MimeMappingsRuntimeHints

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2012-2020 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;
import org.apache.coyote.http11.Http11NioProtocol;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Compression}.
*
* @author Andy Wilkinson
*/
class CompressionTests {
@Test
void defaultCompressibleMimeTypesMatchesTomcatsDefault() {
assertThat(new Compression().getMimeTypes()).containsExactlyInAnyOrder(getTomcatDefaultCompressibleMimeTypes());
}
private String[] getTomcatDefaultCompressibleMimeTypes() {
Http11NioProtocol protocol = new Http11NioProtocol();
return protocol.getCompressibleMimeTypes();
}
}

View File

@@ -0,0 +1,235 @@
/*
* Copyright 2012-2024 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;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Pattern;
import org.apache.catalina.startup.Tomcat;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.boot.web.server.MimeMappings.DefaultMimeMappings;
import org.springframework.boot.web.server.MimeMappings.Mapping;
import org.springframework.boot.web.server.MimeMappings.MimeMappingsRuntimeHints;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link MimeMappings}.
*
* @author Phillip Webb
* @author Guirong Hu
*/
class MimeMappingsTests {
@Test
void defaultsCannotBeModified() {
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> MimeMappings.DEFAULT.add("foo", "foo/bar"));
}
@Test
void createFromExisting() {
MimeMappings mappings = new MimeMappings();
mappings.add("foo", "bar");
MimeMappings clone = new MimeMappings(mappings);
mappings.add("baz", "bar");
assertThat(clone.get("foo")).isEqualTo("bar");
assertThat(clone.get("baz")).isNull();
}
@Test
void createFromMap() {
Map<String, String> mappings = new HashMap<>();
mappings.put("foo", "bar");
MimeMappings clone = new MimeMappings(mappings);
mappings.put("baz", "bar");
assertThat(clone.get("foo")).isEqualTo("bar");
assertThat(clone.get("baz")).isNull();
}
@Test
void iterate() {
MimeMappings mappings = new MimeMappings();
mappings.add("foo", "bar");
mappings.add("baz", "boo");
List<MimeMappings.Mapping> mappingList = new ArrayList<>();
for (MimeMappings.Mapping mapping : mappings) {
mappingList.add(mapping);
}
assertThat(mappingList.get(0).getExtension()).isEqualTo("foo");
assertThat(mappingList.get(0).getMimeType()).isEqualTo("bar");
assertThat(mappingList.get(1).getExtension()).isEqualTo("baz");
assertThat(mappingList.get(1).getMimeType()).isEqualTo("boo");
}
@Test
void getAll() {
MimeMappings mappings = new MimeMappings();
mappings.add("foo", "bar");
mappings.add("baz", "boo");
List<MimeMappings.Mapping> mappingList = new ArrayList<>(mappings.getAll());
assertThat(mappingList.get(0).getExtension()).isEqualTo("foo");
assertThat(mappingList.get(0).getMimeType()).isEqualTo("bar");
assertThat(mappingList.get(1).getExtension()).isEqualTo("baz");
assertThat(mappingList.get(1).getMimeType()).isEqualTo("boo");
}
@Test
void addNew() {
MimeMappings mappings = new MimeMappings();
assertThat(mappings.add("foo", "bar")).isNull();
}
@Test
void addReplacesExisting() {
MimeMappings mappings = new MimeMappings();
mappings.add("foo", "bar");
assertThat(mappings.add("foo", "baz")).isEqualTo("bar");
}
@Test
void remove() {
MimeMappings mappings = new MimeMappings();
mappings.add("foo", "bar");
assertThat(mappings.remove("foo")).isEqualTo("bar");
assertThat(mappings.remove("foo")).isNull();
}
@Test
void get() {
MimeMappings mappings = new MimeMappings();
mappings.add("foo", "bar");
assertThat(mappings.get("foo")).isEqualTo("bar");
}
@Test
void getMissing() {
MimeMappings mappings = new MimeMappings();
assertThat(mappings.get("foo")).isNull();
}
@Test
void makeUnmodifiable() {
MimeMappings mappings = new MimeMappings();
mappings.add("foo", "bar");
MimeMappings unmodifiable = MimeMappings.unmodifiableMappings(mappings);
try {
unmodifiable.remove("foo");
}
catch (UnsupportedOperationException ex) {
// Expected
}
mappings.remove("foo");
assertThat(unmodifiable.get("foo")).isNull();
}
@Test
void mimeTypesInDefaultMappingsAreCorrectlyStructured() {
String regName = "[A-Za-z0-9!#$&.+\\-^_]{1,127}";
Pattern pattern = Pattern.compile("^" + regName + "/" + regName + "$");
assertThat(MimeMappings.DEFAULT).allSatisfy((mapping) -> assertThat(mapping.getMimeType()).matches(pattern));
}
@Test
void getCommonTypeOnDefaultMimeMappingsDoesNotLoadMappings() {
DefaultMimeMappings mappings = new DefaultMimeMappings();
assertThat(mappings.get("json")).isEqualTo("application/json");
assertThat((Object) mappings).extracting("loaded").isNull();
}
@Test
void getExoticTypeOnDefaultMimeMappingsLoadsMappings() {
DefaultMimeMappings mappings = new DefaultMimeMappings();
assertThat(mappings.get("123")).isEqualTo("application/vnd.lotus-1-2-3");
assertThat((Object) mappings).extracting("loaded").isNotNull();
}
@Test
void iterateOnDefaultMimeMappingsLoadsMappings() {
DefaultMimeMappings mappings = new DefaultMimeMappings();
assertThat(mappings).isNotEmpty();
assertThat((Object) mappings).extracting("loaded").isNotNull();
}
@Test
void commonMappingsAreSubsetOfAllMappings() {
MimeMappings defaultMappings = new DefaultMimeMappings();
MimeMappings commonMappings = (MimeMappings) ReflectionTestUtils.getField(DefaultMimeMappings.class, "COMMON");
for (Mapping commonMapping : commonMappings) {
assertThat(defaultMappings.get(commonMapping.getExtension())).isEqualTo(commonMapping.getMimeType());
}
}
@Test
void lazyCopyWhenNotMutatedDelegates() {
DefaultMimeMappings mappings = new DefaultMimeMappings();
MimeMappings lazyCopy = MimeMappings.lazyCopy(mappings);
assertThat(lazyCopy.get("json")).isEqualTo("application/json");
assertThat((Object) mappings).extracting("loaded").isNull();
}
@Test
void lazyCopyWhenMutatedCreatesCopy() {
DefaultMimeMappings mappings = new DefaultMimeMappings();
MimeMappings lazyCopy = MimeMappings.lazyCopy(mappings);
lazyCopy.add("json", "other/json");
assertThat(lazyCopy.get("json")).isEqualTo("other/json");
assertThat((Object) mappings).extracting("loaded").isNotNull();
}
@Test
void lazyCopyWhenMutatedCreatesCopyOnlyOnce() {
MimeMappings mappings = new MimeMappings();
mappings.add("json", "one/json");
MimeMappings lazyCopy = MimeMappings.lazyCopy(mappings);
lazyCopy.add("first", "copy/yes");
assertThat(lazyCopy.get("json")).isEqualTo("one/json");
mappings.add("json", "two/json");
lazyCopy.add("second", "copy/no");
assertThat(lazyCopy.get("json")).isEqualTo("one/json");
}
@Test
void mimeMappingsMatchesTomcatDefaults() throws IOException {
Properties ourDefaultMimeMappings = PropertiesLoaderUtils
.loadProperties(new ClassPathResource("mime-mappings.properties", getClass()));
Properties tomcatDefaultMimeMappings = PropertiesLoaderUtils
.loadProperties(new ClassPathResource("MimeTypeMappings.properties", Tomcat.class));
assertThat(ourDefaultMimeMappings).containsExactlyInAnyOrderEntriesOf(tomcatDefaultMimeMappings);
}
@Test
void shouldRegisterHints() {
RuntimeHints runtimeHints = new RuntimeHints();
new MimeMappingsRuntimeHints().registerHints(runtimeHints, getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.resource()
.forResource("org/springframework/boot/web/server/mime-mappings.properties")).accepts(runtimeHints);
}
}

View File

@@ -0,0 +1,188 @@
/*
* 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;
import java.util.Iterator;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.boot.web.context.reactive.ReactiveWebApplicationContext;
import org.springframework.boot.web.context.reactive.StandardReactiveWebEnvironment;
import org.springframework.boot.web.server.reactive.MockReactiveWebServerFactory;
import org.springframework.boot.web.server.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
import org.springframework.boot.web.server.servlet.MockServletWebServerFactory;
import org.springframework.boot.web.server.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.PropertySource;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.test.context.support.TestPropertySourceUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.StandardServletEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SpringApplication} with a {@link WebServer}.
*
* @author Andy wilkinson
*/
class SpringApplicationWebServerTests {
private String headlessProperty;
private ConfigurableApplicationContext context;
@BeforeEach
void storeAndClearHeadlessProperty() {
this.headlessProperty = System.getProperty("java.awt.headless");
System.clearProperty("java.awt.headless");
}
@AfterEach
void reinstateHeadlessProperty() {
if (this.headlessProperty == null) {
System.clearProperty("java.awt.headless");
}
else {
System.setProperty("java.awt.headless", this.headlessProperty);
}
}
@AfterEach
void cleanUp() {
if (this.context != null) {
this.context.close();
}
System.clearProperty("spring.main.banner-mode");
}
@Test
void defaultApplicationContextForWeb() {
SpringApplication application = new SpringApplication(ExampleWebConfig.class);
application.setWebApplicationType(WebApplicationType.SERVLET);
this.context = application.run();
assertThat(this.context).isInstanceOf(AnnotationConfigServletWebServerApplicationContext.class);
}
@Test
void defaultApplicationContextForReactiveWeb() {
SpringApplication application = new SpringApplication(ExampleReactiveWebConfig.class);
application.setWebApplicationType(WebApplicationType.REACTIVE);
this.context = application.run();
assertThat(this.context).isInstanceOf(AnnotationConfigReactiveWebServerApplicationContext.class);
}
@Test
void environmentForWeb() {
SpringApplication application = new SpringApplication(ExampleWebConfig.class);
application.setWebApplicationType(WebApplicationType.SERVLET);
this.context = application.run();
assertThat(this.context.getEnvironment()).isInstanceOf(StandardServletEnvironment.class);
assertThat(this.context.getEnvironment().getClass().getName()).endsWith("ApplicationServletEnvironment");
}
@Test
void environmentForReactiveWeb() {
SpringApplication application = new SpringApplication(ExampleReactiveWebConfig.class);
application.setWebApplicationType(WebApplicationType.REACTIVE);
this.context = application.run();
assertThat(this.context.getEnvironment()).isInstanceOf(StandardReactiveWebEnvironment.class);
assertThat(this.context.getEnvironment().getClass().getName()).endsWith("ApplicationReactiveWebEnvironment");
}
@Test
void webApplicationConfiguredViaAPropertyHasTheCorrectTypeOfContextAndEnvironment() {
ConfigurableApplicationContext context = new SpringApplication(ExampleWebConfig.class)
.run("--spring.main.web-application-type=servlet");
assertThat(context).isInstanceOf(WebApplicationContext.class);
assertThat(context.getEnvironment()).isInstanceOf(StandardServletEnvironment.class);
assertThat(context.getEnvironment().getClass().getName()).endsWith("ApplicationServletEnvironment");
}
@Test
void reactiveApplicationConfiguredViaAPropertyHasTheCorrectTypeOfContextAndEnvironment() {
ConfigurableApplicationContext context = new SpringApplication(ExampleReactiveWebConfig.class)
.run("--spring.main.web-application-type=reactive");
assertThat(context).isInstanceOf(ReactiveWebApplicationContext.class);
assertThat(context.getEnvironment()).isInstanceOf(StandardReactiveWebEnvironment.class);
assertThat(context.getEnvironment().getClass().getName()).endsWith("ApplicationReactiveWebEnvironment");
}
@Test
@WithResource(name = "application-withwebapplicationtype.properties",
content = "spring.main.web-application-type=reactive")
void environmentIsConvertedIfTypeDoesNotMatch() {
ConfigurableApplicationContext context = new SpringApplication(ExampleReactiveWebConfig.class)
.run("--spring.profiles.active=withwebapplicationtype");
assertThat(context).isInstanceOf(ReactiveWebApplicationContext.class);
assertThat(context.getEnvironment()).isInstanceOf(StandardReactiveWebEnvironment.class);
assertThat(context.getEnvironment().getClass().getName()).endsWith("ApplicationReactiveWebEnvironment");
}
@Test
void webApplicationSwitchedOffInListener() {
SpringApplication application = new SpringApplication(ExampleWebConfig.class);
application.addListeners((ApplicationListener<ApplicationEnvironmentPreparedEvent>) (event) -> {
assertThat(event.getEnvironment().getClass().getName()).endsWith("ApplicationServletEnvironment");
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(event.getEnvironment(), "foo=bar");
event.getSpringApplication().setWebApplicationType(WebApplicationType.NONE);
});
this.context = application.run();
assertThat(this.context.getEnvironment()).isNotInstanceOf(StandardServletEnvironment.class);
assertThat(this.context.getEnvironment().getProperty("foo")).isEqualTo("bar");
Iterator<PropertySource<?>> iterator = this.context.getEnvironment().getPropertySources().iterator();
assertThat(iterator.next().getName()).isEqualTo("configurationProperties");
assertThat(iterator.next().getName())
.isEqualTo(TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME);
}
@Configuration(proxyBeanMethods = false)
static class ExampleWebConfig {
@Bean
MockServletWebServerFactory webServer() {
return new MockServletWebServerFactory();
}
}
@Configuration(proxyBeanMethods = false)
static class ExampleReactiveWebConfig {
@Bean
MockReactiveWebServerFactory webServerFactory() {
return new MockReactiveWebServerFactory();
}
@Bean
HttpHandler httpHandler() {
return (serverHttpRequest, serverHttpResponse) -> Mono.empty();
}
}
}

View File

@@ -0,0 +1,201 @@
/*
* 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;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link WebServerFactoryCustomizerBeanPostProcessor}.
*
* @author Phillip Webb
*/
@ExtendWith(MockitoExtension.class)
class WebServerFactoryCustomizerBeanPostProcessorTests {
private final WebServerFactoryCustomizerBeanPostProcessor processor = new WebServerFactoryCustomizerBeanPostProcessor();
@Mock
private ListableBeanFactory beanFactory;
@BeforeEach
void setup() {
this.processor.setBeanFactory(this.beanFactory);
}
@Test
void setBeanFactoryWhenNotListableShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.processor.setBeanFactory(mock(BeanFactory.class)))
.withMessageContaining("'beanFactory' must be a ListableBeanFactory");
}
@Test
void postProcessBeforeShouldReturnBean() {
Object bean = new Object();
Object result = this.processor.postProcessBeforeInitialization(bean, "foo");
assertThat(result).isSameAs(bean);
}
@Test
void postProcessAfterShouldReturnBean() {
Object bean = new Object();
Object result = this.processor.postProcessAfterInitialization(bean, "foo");
assertThat(result).isSameAs(bean);
}
@Test
void postProcessAfterShouldCallInterfaceCustomizers() {
Map<String, Object> beans = addInterfaceBeans();
addMockBeans(beans);
postProcessBeforeInitialization(WebServerFactory.class);
assertThat(wasCalled(beans, "one")).isFalse();
assertThat(wasCalled(beans, "two")).isFalse();
assertThat(wasCalled(beans, "all")).isTrue();
}
@Test
void postProcessAfterWhenWebServerFactoryOneShouldCallInterfaceCustomizers() {
Map<String, Object> beans = addInterfaceBeans();
addMockBeans(beans);
postProcessBeforeInitialization(WebServerFactoryOne.class);
assertThat(wasCalled(beans, "one")).isTrue();
assertThat(wasCalled(beans, "two")).isFalse();
assertThat(wasCalled(beans, "all")).isTrue();
}
@Test
void postProcessAfterWhenWebServerFactoryTwoShouldCallInterfaceCustomizers() {
Map<String, Object> beans = addInterfaceBeans();
addMockBeans(beans);
postProcessBeforeInitialization(WebServerFactoryTwo.class);
assertThat(wasCalled(beans, "one")).isFalse();
assertThat(wasCalled(beans, "two")).isTrue();
assertThat(wasCalled(beans, "all")).isTrue();
}
private Map<String, Object> addInterfaceBeans() {
WebServerFactoryOneCustomizer oneCustomizer = new WebServerFactoryOneCustomizer();
WebServerFactoryTwoCustomizer twoCustomizer = new WebServerFactoryTwoCustomizer();
WebServerFactoryAllCustomizer allCustomizer = new WebServerFactoryAllCustomizer();
Map<String, Object> beans = new LinkedHashMap<>();
beans.put("one", oneCustomizer);
beans.put("two", twoCustomizer);
beans.put("all", allCustomizer);
return beans;
}
@Test
void postProcessAfterShouldCallLambdaCustomizers() {
List<String> called = new ArrayList<>();
addLambdaBeans(called);
postProcessBeforeInitialization(WebServerFactory.class);
assertThat(called).containsExactly("all");
}
@Test
void postProcessAfterWhenWebServerFactoryOneShouldCallLambdaCustomizers() {
List<String> called = new ArrayList<>();
addLambdaBeans(called);
postProcessBeforeInitialization(WebServerFactoryOne.class);
assertThat(called).containsExactly("one", "all");
}
@Test
void postProcessAfterWhenWebServerFactoryTwoShouldCallLambdaCustomizers() {
List<String> called = new ArrayList<>();
addLambdaBeans(called);
postProcessBeforeInitialization(WebServerFactoryTwo.class);
assertThat(called).containsExactly("two", "all");
}
private void addLambdaBeans(List<String> called) {
WebServerFactoryCustomizer<WebServerFactoryOne> one = (f) -> called.add("one");
WebServerFactoryCustomizer<WebServerFactoryTwo> two = (f) -> called.add("two");
WebServerFactoryCustomizer<WebServerFactory> all = (f) -> called.add("all");
Map<String, Object> beans = new LinkedHashMap<>();
beans.put("one", one);
beans.put("two", two);
beans.put("all", all);
addMockBeans(beans);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private void addMockBeans(Map<String, ?> beans) {
given(this.beanFactory.getBeansOfType(WebServerFactoryCustomizer.class, false, false))
.willReturn((Map<String, WebServerFactoryCustomizer>) beans);
}
private void postProcessBeforeInitialization(Class<?> type) {
this.processor.postProcessBeforeInitialization(mock(type), "foo");
}
private boolean wasCalled(Map<String, ?> beans, String name) {
return ((MockWebServerFactoryCustomizer<?>) beans.get(name)).wasCalled();
}
interface WebServerFactoryOne extends WebServerFactory {
}
interface WebServerFactoryTwo extends WebServerFactory {
}
static class MockWebServerFactoryCustomizer<T extends WebServerFactory> implements WebServerFactoryCustomizer<T> {
private boolean called;
@Override
public void customize(T factory) {
this.called = true;
}
boolean wasCalled() {
return this.called;
}
}
static class WebServerFactoryOneCustomizer extends MockWebServerFactoryCustomizer<WebServerFactoryOne> {
}
static class WebServerFactoryTwoCustomizer extends MockWebServerFactoryCustomizer<WebServerFactoryTwo> {
}
static class WebServerFactoryAllCustomizer extends MockWebServerFactoryCustomizer<WebServerFactory> {
}
}

View File

@@ -0,0 +1,206 @@
/*
* 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;
import org.junit.jupiter.api.Test;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundleKey;
import org.springframework.boot.ssl.SslOptions;
import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.boot.testsupport.classpath.resources.ResourcePath;
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
import org.springframework.boot.testsupport.ssl.MockPkcs11Security;
import org.springframework.boot.testsupport.ssl.MockPkcs11SecurityProvider;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link WebServerSslBundle}.
*
* @author Scott Frederick
* @author Phillip Webb
* @author Moritz Halbritter
*/
@MockPkcs11Security
class WebServerSslBundleTests {
@Test
void whenSslDisabledThrowsException() {
Ssl ssl = new Ssl();
ssl.setEnabled(false);
assertThatIllegalStateException().isThrownBy(() -> WebServerSslBundle.get(ssl))
.withMessage("SSL is not enabled");
}
@Test
@WithPackageResources("test.p12")
void whenFromJksProperties() {
Ssl ssl = new Ssl();
ssl.setKeyStore("classpath:test.p12");
ssl.setKeyStorePassword("secret");
ssl.setKeyStoreType("PKCS12");
ssl.setTrustStore("classpath:test.p12");
ssl.setTrustStorePassword("secret");
ssl.setTrustStoreType("PKCS12");
ssl.setKeyPassword("password");
ssl.setKeyAlias("alias");
ssl.setClientAuth(Ssl.ClientAuth.NONE);
ssl.setCiphers(new String[] { "ONE", "TWO", "THREE" });
ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" });
ssl.setProtocol("TestProtocol");
SslBundle bundle = WebServerSslBundle.get(ssl);
assertThat(bundle).isNotNull();
assertThat(bundle.getProtocol()).isEqualTo("TestProtocol");
SslBundleKey key = bundle.getKey();
assertThat(key.getPassword()).isEqualTo("password");
assertThat(key.getAlias()).isEqualTo("alias");
SslStoreBundle stores = bundle.getStores();
assertThat(stores.getKeyStorePassword()).isEqualTo("secret");
assertThat(stores.getKeyStore()).isNotNull();
assertThat(stores.getTrustStore()).isNotNull();
SslOptions options = bundle.getOptions();
assertThat(options.getCiphers()).containsExactly("ONE", "TWO", "THREE");
assertThat(options.getEnabledProtocols()).containsExactly("TLSv1.1", "TLSv1.2");
}
@Test
@WithPackageResources("test.jks")
void whenFromJksPropertiesWithPkcs11StoreType(@ResourcePath("test.jks") String keyStorePath) {
Ssl ssl = new Ssl();
ssl.setKeyStoreType("PKCS11");
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
ssl.setKeyStore(keyStorePath);
ssl.setKeyPassword("password");
ssl.setClientAuth(Ssl.ClientAuth.NONE);
assertThatIllegalStateException().isThrownBy(() -> WebServerSslBundle.get(ssl))
.withMessageContaining("must be empty or null for PKCS11 hardware key stores");
}
@Test
void whenFromPkcs11Properties() {
Ssl ssl = new Ssl();
ssl.setKeyStoreType("PKCS11");
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
ssl.setTrustStoreType("PKCS11");
ssl.setTrustStoreProvider(MockPkcs11SecurityProvider.NAME);
ssl.setKeyPassword("password");
ssl.setClientAuth(Ssl.ClientAuth.NONE);
SslBundle bundle = WebServerSslBundle.get(ssl);
assertThat(bundle).isNotNull();
assertThat(bundle.getProtocol()).isEqualTo("TLS");
SslBundleKey key = bundle.getKey();
assertThat(key.getPassword()).isEqualTo("password");
SslStoreBundle stores = bundle.getStores();
assertThat(stores.getKeyStore()).isNotNull();
assertThat(stores.getTrustStore()).isNotNull();
}
@Test
@WithPackageResources({ "test-cert.pem", "test-key.pem", "test-cert-chain.pem" })
void whenFromPemProperties() {
Ssl ssl = new Ssl();
ssl.setCertificate("classpath:test-cert.pem");
ssl.setCertificatePrivateKey("classpath:test-key.pem");
ssl.setTrustCertificate("classpath:test-cert-chain.pem");
ssl.setKeyStoreType("PKCS12");
ssl.setTrustStoreType("PKCS12");
ssl.setKeyPassword("password");
ssl.setClientAuth(Ssl.ClientAuth.NONE);
ssl.setCiphers(new String[] { "ONE", "TWO", "THREE" });
ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" });
ssl.setProtocol("TLSv1.1");
SslBundle bundle = WebServerSslBundle.get(ssl);
assertThat(bundle).isNotNull();
SslBundleKey key = bundle.getKey();
assertThat(key.getAlias()).isNull();
assertThat(key.getPassword()).isEqualTo("password");
SslStoreBundle stores = bundle.getStores();
assertThat(stores.getKeyStorePassword()).isNull();
assertThat(stores.getKeyStore()).isNotNull();
assertThat(stores.getTrustStore()).isNotNull();
SslOptions options = bundle.getOptions();
assertThat(options.getCiphers()).containsExactly("ONE", "TWO", "THREE");
assertThat(options.getEnabledProtocols()).containsExactly("TLSv1.1", "TLSv1.2");
}
@Test
@WithPackageResources({ "test-cert.pem", "test-key.pem", "test.p12" })
void whenPemKeyStoreAndJksTrustStoreProperties() {
Ssl ssl = new Ssl();
ssl.setCertificate("classpath:test-cert.pem");
ssl.setCertificatePrivateKey("classpath:test-key.pem");
ssl.setKeyStoreType("PKCS12");
ssl.setKeyPassword("password");
ssl.setTrustStore("classpath:test.p12");
ssl.setTrustStorePassword("secret");
ssl.setTrustStoreType("PKCS12");
ssl.setClientAuth(Ssl.ClientAuth.NONE);
ssl.setCiphers(new String[] { "ONE", "TWO", "THREE" });
ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" });
ssl.setProtocol("TLSv1.1");
SslBundle bundle = WebServerSslBundle.get(ssl);
assertThat(bundle).isNotNull();
SslBundleKey key = bundle.getKey();
assertThat(key.getAlias()).isNull();
assertThat(key.getPassword()).isEqualTo("password");
SslStoreBundle stores = bundle.getStores();
assertThat(stores.getKeyStorePassword()).isNull();
assertThat(stores.getKeyStore()).isNotNull();
assertThat(stores.getTrustStore()).isNotNull();
SslOptions options = bundle.getOptions();
assertThat(options.getCiphers()).containsExactly("ONE", "TWO", "THREE");
assertThat(options.getEnabledProtocols()).containsExactly("TLSv1.1", "TLSv1.2");
}
@Test
@WithPackageResources({ "test.p12", "test-cert-chain.pem" })
void whenJksKeyStoreAndPemTrustStoreProperties() {
Ssl ssl = new Ssl();
ssl.setKeyStore("classpath:test.p12");
ssl.setKeyStoreType("PKCS12");
ssl.setKeyPassword("password");
ssl.setTrustCertificate("classpath:test-cert-chain.pem");
ssl.setTrustStorePassword("secret");
ssl.setTrustStoreType("PKCS12");
ssl.setClientAuth(Ssl.ClientAuth.NONE);
ssl.setCiphers(new String[] { "ONE", "TWO", "THREE" });
ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" });
ssl.setProtocol("TLSv1.1");
SslBundle bundle = WebServerSslBundle.get(ssl);
assertThat(bundle).isNotNull();
SslBundleKey key = bundle.getKey();
assertThat(key.getAlias()).isNull();
assertThat(key.getPassword()).isEqualTo("password");
SslStoreBundle stores = bundle.getStores();
assertThat(stores.getKeyStorePassword()).isNull();
assertThat(stores.getKeyStore()).isNotNull();
assertThat(stores.getTrustStore()).isNotNull();
SslOptions options = bundle.getOptions();
assertThat(options.getCiphers()).containsExactly("ONE", "TWO", "THREE");
assertThat(options.getEnabledProtocols()).containsExactly("TLSv1.1", "TLSv1.2");
}
@Test
void whenMissingPropertiesThrowsException() {
Ssl ssl = new Ssl();
assertThatIllegalStateException().isThrownBy(() -> WebServerSslBundle.get(ssl))
.withMessageContaining("SSL is enabled but no trust material is configured");
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.context;
import org.junit.jupiter.api.Test;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.web.server.reactive.ReactiveWebServerFactory;
import org.springframework.boot.web.server.reactive.context.ReactiveWebServerApplicationContext;
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.ConfigurableApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link MissingWebServerFactoryBeanFailureAnalyzer}.
*
* @author Guirong Hu
* @author Andy Wilkinson
*/
class MissingWebServerFactoryBeanFailureAnalyzerTests {
@Test
void missingServletWebServerFactoryBeanFailure() {
ApplicationContextException failure = createFailure(new ServletWebServerApplicationContext());
assertThat(failure).isNotNull();
FailureAnalysis analysis = new MissingWebServerFactoryBeanFailureAnalyzer().analyze(failure);
assertThat(analysis).isNotNull();
assertThat(analysis.getDescription()).isEqualTo("Web application could not be started as there was no "
+ ServletWebServerFactory.class.getName() + " bean defined in the context.");
assertThat(analysis.getAction()).isEqualTo(
"Check your application's dependencies for a supported servlet web server.\nCheck the configured web "
+ "application type.");
}
@Test
void missingReactiveWebServerFactoryBeanFailure() {
ApplicationContextException failure = createFailure(new ReactiveWebServerApplicationContext());
FailureAnalysis analysis = new MissingWebServerFactoryBeanFailureAnalyzer().analyze(failure);
assertThat(analysis).isNotNull();
assertThat(analysis.getDescription()).isEqualTo("Web application could not be started as there was no "
+ ReactiveWebServerFactory.class.getName() + " bean defined in the context.");
assertThat(analysis.getAction()).isEqualTo(
"Check your application's dependencies for a supported reactive web server.\nCheck the configured web "
+ "application type.");
}
private ApplicationContextException createFailure(ConfigurableApplicationContext context) {
try {
context.refresh();
context.close();
return null;
}
catch (ApplicationContextException ex) {
return ex;
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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.context;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link WebServerApplicationContext}.
*
* @author Phillip Webb
*/
class WebServerApplicationContextTests {
@Test
void hasServerNamespaceWhenContextIsNotWebServerApplicationContextReturnsFalse() {
ApplicationContext context = mock(ApplicationContext.class);
assertThat(WebServerApplicationContext.hasServerNamespace(context, "test")).isFalse();
}
@Test
void hasServerNamespaceWhenContextIsWebServerApplicationContextAndNamespaceDoesNotMatchReturnsFalse() {
ApplicationContext context = mock(WebServerApplicationContext.class);
assertThat(WebServerApplicationContext.hasServerNamespace(context, "test")).isFalse();
}
@Test
void hasServerNamespaceWhenContextIsWebServerApplicationContextAndNamespaceMatchesReturnsTrue() {
WebServerApplicationContext context = mock(WebServerApplicationContext.class);
given(context.getServerNamespace()).willReturn("test");
assertThat(WebServerApplicationContext.hasServerNamespace(context, "test")).isTrue();
}
}

View File

@@ -0,0 +1,144 @@
/*
* 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.context;
import java.io.File;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.web.server.WebServer;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.contentOf;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests {@link WebServerPortFileWriter}.
*
* @author David Liu
* @author Phillip Webb
* @author Andy Wilkinson
*/
class WebServerPortFileWriterTests {
@TempDir
File tempDir;
@BeforeEach
@AfterEach
void reset() {
System.clearProperty("PORTFILE");
}
@Test
void createPortFile() {
File file = new File(this.tempDir, "port.file");
WebServerPortFileWriter listener = new WebServerPortFileWriter(file);
listener.onApplicationEvent(mockEvent("", 8080));
assertThat(contentOf(file)).isEqualTo("8080");
}
@Test
void overridePortFileWithDefault() {
System.setProperty("PORTFILE", new File(this.tempDir, "port.file").getAbsolutePath());
WebServerPortFileWriter listener = new WebServerPortFileWriter();
listener.onApplicationEvent(mockEvent("", 8080));
String content = contentOf(new File(System.getProperty("PORTFILE")));
assertThat(content).isEqualTo("8080");
}
@Test
void overridePortFileWithExplicitFile() {
File file = new File(this.tempDir, "port.file");
System.setProperty("PORTFILE", new File(this.tempDir, "override.file").getAbsolutePath());
WebServerPortFileWriter listener = new WebServerPortFileWriter(file);
listener.onApplicationEvent(mockEvent("", 8080));
String content = contentOf(new File(System.getProperty("PORTFILE")));
assertThat(content).isEqualTo("8080");
}
@Test
void createManagementPortFile() {
File file = new File(this.tempDir, "port.file");
WebServerPortFileWriter listener = new WebServerPortFileWriter(file);
listener.onApplicationEvent(mockEvent("", 8080));
listener.onApplicationEvent(mockEvent("management", 9090));
assertThat(contentOf(file)).isEqualTo("8080");
String managementFile = file.getName();
managementFile = managementFile.substring(0,
managementFile.length() - StringUtils.getFilenameExtension(managementFile).length() - 1);
managementFile = managementFile + "-management." + StringUtils.getFilenameExtension(file.getName());
String content = contentOf(new File(file.getParentFile(), managementFile));
assertThat(content).isEqualTo("9090");
assertThat(collectFileNames(file.getParentFile())).contains(managementFile);
}
@Test
void createUpperCaseManagementPortFile() {
File file = new File(this.tempDir, "port.file");
file = new File(file.getParentFile(), file.getName().toUpperCase(Locale.ENGLISH));
WebServerPortFileWriter listener = new WebServerPortFileWriter(file);
listener.onApplicationEvent(mockEvent("management", 9090));
String managementFile = file.getName();
managementFile = managementFile.substring(0,
managementFile.length() - StringUtils.getFilenameExtension(managementFile).length() - 1);
managementFile = managementFile + "-MANAGEMENT." + StringUtils.getFilenameExtension(file.getName());
String content = contentOf(new File(file.getParentFile(), managementFile));
assertThat(content).isEqualTo("9090");
assertThat(collectFileNames(file.getParentFile())).contains(managementFile);
}
@Test
void getPortFileWhenPortFileNameDoesNotHaveExtension() {
File file = new File(this.tempDir, "portfile");
WebServerPortFileWriter listener = new WebServerPortFileWriter(file);
WebServerApplicationContext applicationContext = mock(WebServerApplicationContext.class);
given(applicationContext.getServerNamespace()).willReturn("management");
assertThat(listener.getPortFile(applicationContext).getName()).isEqualTo("portfile-management");
}
private WebServerInitializedEvent mockEvent(String namespace, int port) {
WebServer webServer = mock(WebServer.class);
given(webServer.getPort()).willReturn(port);
WebServerApplicationContext applicationContext = mock(WebServerApplicationContext.class);
given(applicationContext.getServerNamespace()).willReturn(namespace);
given(applicationContext.getWebServer()).willReturn(webServer);
WebServerInitializedEvent event = mock(WebServerInitializedEvent.class);
given(event.getApplicationContext()).willReturn(applicationContext);
given(event.getWebServer()).willReturn(webServer);
return event;
}
private Set<String> collectFileNames(File directory) {
Set<String> names = new HashSet<>();
if (directory.isDirectory()) {
for (File file : directory.listFiles()) {
names.add(file.getName());
}
}
return names;
}
}

View File

@@ -0,0 +1,171 @@
/*
* 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.reactive.context;
import org.junit.jupiter.api.Test;
import org.springframework.boot.web.server.reactive.MockReactiveWebServerFactory;
import org.springframework.boot.web.server.reactive.ReactiveWebServerFactory;
import org.springframework.boot.web.server.reactive.context.WebServerManager.DelayedInitializationHttpHandler;
import org.springframework.boot.web.server.reactive.context.config.ExampleReactiveWebServerApplicationConfiguration;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.SimpleApplicationEventMulticaster;
import org.springframework.http.server.reactive.HttpHandler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link AnnotationConfigReactiveWebServerApplicationContext}.
*
* @author Phillip Webb
*/
class AnnotationConfigReactiveWebServerApplicationContextTests {
private AnnotationConfigReactiveWebServerApplicationContext context;
@Test
void createFromScan() {
this.context = new AnnotationConfigReactiveWebServerApplicationContext(
ExampleReactiveWebServerApplicationConfiguration.class.getPackage().getName());
verifyContext();
}
@Test
void createFromConfigClass() {
this.context = new AnnotationConfigReactiveWebServerApplicationContext(
ExampleReactiveWebServerApplicationConfiguration.class);
verifyContext();
}
@Test
void registerAndRefresh() {
this.context = new AnnotationConfigReactiveWebServerApplicationContext();
this.context.register(ExampleReactiveWebServerApplicationConfiguration.class);
this.context.refresh();
verifyContext();
}
@Test
void multipleRegistersAndRefresh() {
this.context = new AnnotationConfigReactiveWebServerApplicationContext();
this.context.register(WebServerConfiguration.class);
this.context.register(HttpHandlerConfiguration.class);
this.context.refresh();
assertThat(this.context.getBeansOfType(WebServerConfiguration.class)).hasSize(1);
assertThat(this.context.getBeansOfType(HttpHandlerConfiguration.class)).hasSize(1);
}
@Test
void scanAndRefresh() {
this.context = new AnnotationConfigReactiveWebServerApplicationContext();
this.context.scan(ExampleReactiveWebServerApplicationConfiguration.class.getPackage().getName());
this.context.refresh();
verifyContext();
}
@Test
void httpHandlerInitialization() {
// gh-14666
this.context = new AnnotationConfigReactiveWebServerApplicationContext(InitializationTestConfig.class);
verifyContext();
}
private void verifyContext() {
MockReactiveWebServerFactory factory = this.context.getBean(MockReactiveWebServerFactory.class);
HttpHandler expectedHandler = this.context.getBean(HttpHandler.class);
HttpHandler actualHandler = factory.getWebServer().getHttpHandler();
if (actualHandler instanceof DelayedInitializationHttpHandler delayedHandler) {
actualHandler = delayedHandler.getHandler();
}
assertThat(actualHandler).isEqualTo(expectedHandler);
}
@Configuration(proxyBeanMethods = false)
static class WebServerConfiguration {
@Bean
ReactiveWebServerFactory webServerFactory() {
return new MockReactiveWebServerFactory();
}
}
@Configuration(proxyBeanMethods = false)
static class HttpHandlerConfiguration {
@Bean
HttpHandler httpHandler() {
return mock(HttpHandler.class);
}
}
@Configuration(proxyBeanMethods = false)
static class InitializationTestConfig {
private static boolean addedListener;
@Bean
ReactiveWebServerFactory webServerFactory() {
return new MockReactiveWebServerFactory();
}
@Bean
HttpHandler httpHandler() {
if (!addedListener) {
throw new RuntimeException(
"Handlers should be added after listeners, we're being initialized too early!");
}
return mock(HttpHandler.class);
}
@Bean
Listener listener() {
return new Listener();
}
@Bean
ApplicationEventMulticaster applicationEventMulticaster() {
return new SimpleApplicationEventMulticaster() {
@Override
public void addApplicationListenerBean(String listenerBeanName) {
super.addApplicationListenerBean(listenerBeanName);
if ("listener".equals(listenerBeanName)) {
addedListener = true;
}
}
};
}
static class Listener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
}
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.reactive.context;
import org.springframework.boot.AbstractApplicationEnvironmentTests;
import org.springframework.core.env.StandardEnvironment;
/**
* Tests for {@link ApplicationReactiveWebEnvironment}.
*
* @author Phillip Webb
*/
class ApplicationReactiveWebEnvironmentTests extends AbstractApplicationEnvironmentTests {
@Override
protected StandardEnvironment createEnvironment() {
return new ApplicationReactiveWebEnvironment();
}
}

View File

@@ -0,0 +1,265 @@
/*
* 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.reactive.context;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.context.ServerPortInfoApplicationContextInitializer;
import org.springframework.boot.web.server.reactive.MockReactiveWebServerFactory;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.http.server.reactive.HttpHandler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.then;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
/**
* Tests for {@link ReactiveWebServerApplicationContext}.
*
* @author Andy Wilkinson
*/
class ReactiveWebServerApplicationContextTests {
private final ReactiveWebServerApplicationContext context = new ReactiveWebServerApplicationContext();
@AfterEach
void cleanUp() {
this.context.close();
}
@Test
void whenThereIsNoWebServerFactoryBeanThenContextRefreshWillFail() {
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(this.context::refresh)
.havingRootCause()
.withMessageContaining(
"Unable to start ReactiveWebServerApplicationContext due to missing ReactiveWebServerFactory bean");
}
@Test
void whenThereIsNoHttpHandlerBeanThenContextRefreshWillFail() {
addWebServerFactoryBean();
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(this.context::refresh)
.havingRootCause()
.withMessageContaining("Unable to start ReactiveWebApplicationContext due to missing HttpHandler bean");
}
@Test
void whenThereAreMultipleWebServerFactoryBeansThenContextRefreshWillFail() {
addWebServerFactoryBean();
addWebServerFactoryBean("anotherWebServerFactory");
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(this.context::refresh)
.havingRootCause()
.withMessageContaining(
"Unable to start ReactiveWebApplicationContext due to multiple ReactiveWebServerFactory beans");
}
@Test
void whenThereAreMultipleHttpHandlerBeansThenContextRefreshWillFail() {
addWebServerFactoryBean();
addHttpHandlerBean("httpHandler1");
addHttpHandlerBean("httpHandler2");
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(this.context::refresh)
.havingRootCause()
.withMessageContaining("Unable to start ReactiveWebApplicationContext due to multiple HttpHandler beans");
}
@Test
void whenContextIsRefreshedThenReactiveWebServerInitializedEventIsPublished() {
addWebServerFactoryBean();
addHttpHandlerBean();
TestApplicationListener listener = new TestApplicationListener();
this.context.addApplicationListener(listener);
this.context.refresh();
List<ApplicationEvent> events = listener.receivedEvents();
assertThat(events).hasSize(2)
.extracting("class")
.containsExactly(ReactiveWebServerInitializedEvent.class, ContextRefreshedEvent.class);
ReactiveWebServerInitializedEvent initializedEvent = (ReactiveWebServerInitializedEvent) events.get(0);
assertThat(initializedEvent.getSource().getPort()).isGreaterThanOrEqualTo(0);
assertThat(initializedEvent.getApplicationContext()).isEqualTo(this.context);
}
@Test
void whenContextIsRefreshedThenLocalServerPortIsAvailableFromTheEnvironment() {
addWebServerFactoryBean();
addHttpHandlerBean();
new ServerPortInfoApplicationContextInitializer().initialize(this.context);
this.context.refresh();
ConfigurableEnvironment environment = this.context.getEnvironment();
assertThat(environment.containsProperty("local.server.port")).isTrue();
assertThat(environment.getProperty("local.server.port")).isEqualTo("8080");
}
@Test
void whenContextRefreshFailedThenWebServerIsStoppedAndDestroyed() {
addWebServerFactoryBean();
addHttpHandlerBean();
this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class));
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh);
WebServer webServer = this.context.getWebServer();
then(webServer).should(times(2)).stop();
then(webServer).should().destroy();
}
@Test
void whenContextRefreshFailedThenWebServerStopFailedCatchStopException() {
addWebServerFactoryBean();
addHttpHandlerBean();
this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class, () -> {
willThrow(new RuntimeException("WebServer has failed to stop")).willCallRealMethod()
.given(this.context.getWebServer())
.stop();
return new RefreshFailure();
}));
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh)
.withStackTraceContaining("WebServer has failed to stop");
WebServer webServer = this.context.getWebServer();
then(webServer).should().stop();
then(webServer).should(never()).destroy();
}
@Test
void whenContextRefreshFailedThenWebServerIsStoppedAndDestroyFailedCatchDestroyException() {
addWebServerFactoryBean();
addHttpHandlerBean();
this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class, () -> {
willThrow(new RuntimeException("WebServer has failed to destroy")).willCallRealMethod()
.given(this.context.getWebServer())
.destroy();
return new RefreshFailure();
}));
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh)
.withStackTraceContaining("WebServer has failed to destroy");
WebServer webServer = this.context.getWebServer();
then(webServer).should().stop();
then(webServer).should().destroy();
}
@Test
void whenContextIsClosedThenWebServerIsStoppedAndDestroyed() {
addWebServerFactoryBean();
addHttpHandlerBean();
this.context.refresh();
MockReactiveWebServerFactory factory = this.context.getBean(MockReactiveWebServerFactory.class);
this.context.close();
then(factory.getWebServer()).should(times(2)).stop();
then(factory.getWebServer()).should().destroy();
}
@Test
@SuppressWarnings("unchecked")
void whenContextIsClosedThenApplicationAvailabilityChangesToRefusingTraffic() {
addWebServerFactoryBean();
addHttpHandlerBean();
TestApplicationListener listener = new TestApplicationListener();
this.context.refresh();
this.context.addApplicationListener(listener);
this.context.close();
List<ApplicationEvent> events = listener.receivedEvents();
assertThat(events).hasSize(2)
.extracting("class")
.contains(AvailabilityChangeEvent.class, ContextClosedEvent.class);
assertThat(((AvailabilityChangeEvent<ReadinessState>) events.get(0)).getState())
.isEqualTo(ReadinessState.REFUSING_TRAFFIC);
}
@Test
void whenContextIsNotActiveThenCloseDoesNotChangeTheApplicationAvailability() {
addWebServerFactoryBean();
addHttpHandlerBean();
TestApplicationListener listener = new TestApplicationListener();
this.context.addApplicationListener(listener);
this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class));
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh);
this.context.close();
assertThat(listener.receivedEvents()).isEmpty();
}
@Test
void whenTheContextIsRefreshedThenASubsequentRefreshAttemptWillFail() {
addWebServerFactoryBean();
addHttpHandlerBean();
this.context.refresh();
assertThatIllegalStateException().isThrownBy(this.context::refresh)
.withMessageContaining("multiple refresh attempts");
}
private void addHttpHandlerBean() {
addHttpHandlerBean("httpHandler");
}
private void addHttpHandlerBean(String beanName) {
this.context.registerBeanDefinition(beanName,
new RootBeanDefinition(HttpHandler.class, () -> (request, response) -> null));
}
private void addWebServerFactoryBean() {
addWebServerFactoryBean("webServerFactory");
}
private void addWebServerFactoryBean(String beanName) {
this.context.registerBeanDefinition(beanName, new RootBeanDefinition(MockReactiveWebServerFactory.class));
}
static class TestApplicationListener implements ApplicationListener<ApplicationEvent> {
private final Deque<ApplicationEvent> events = new ArrayDeque<>();
@Override
public void onApplicationEvent(ApplicationEvent event) {
this.events.add(event);
}
List<ApplicationEvent> receivedEvents() {
List<ApplicationEvent> receivedEvents = new ArrayList<>();
while (!this.events.isEmpty()) {
receivedEvents.add(this.events.pollFirst());
}
return receivedEvents;
}
}
static class RefreshFailure {
RefreshFailure() {
throw new RuntimeException("Fail refresh");
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.reactive.context.config;
import org.springframework.boot.web.server.reactive.MockReactiveWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.HttpHandler;
import static org.mockito.Mockito.mock;
/**
* Example {@code @Configuration} for use with
* {@code AnnotationConfigReactiveWebServerApplicationContextTests}.
*
* @author Phillip Webb
*/
@Configuration(proxyBeanMethods = false)
public class ExampleReactiveWebServerApplicationConfiguration {
@Bean
public MockReactiveWebServerFactory webServerFactory() {
return new MockReactiveWebServerFactory();
}
@Bean
public HttpHandler httpHandler() {
return mock(HttpHandler.class);
}
}

View File

@@ -0,0 +1,177 @@
/*
* 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.servlet;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.Test;
import org.springframework.boot.web.server.Cookie.SameSite;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.fail;
/**
* Tests for {@link CookieSameSiteSupplier}.
*
* @author Phillip Webb
*/
class CookieSameSiteSupplierTests {
@Test
void whenHasNameWhenNameIsNullThrowsException() {
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasName((String) null))
.withMessage("'name' must not be empty");
}
@Test
void whenHasNameWhenNameIsEmptyThrowsException() {
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasName(""))
.withMessage("'name' must not be empty");
}
@Test
void whenHasNameWhenNameMatchesCallsGetSameSite() {
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
assertThat(supplier.whenHasName("test").getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.LAX);
}
@Test
void whenHasNameWhenNameDoesNotMatchDoesNotCallGetSameSite() {
CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called");
assertThat(supplier.whenHasName("test").getSameSite(new Cookie("tset", "x"))).isNull();
}
@Test
void whenHasSuppliedNameWhenNameIsNullThrowsException() {
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasName((Supplier<String>) null))
.withMessage("'nameSupplier' must not be null");
}
@Test
void whenHasSuppliedNameWhenNameMatchesCallsGetSameSite() {
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
assertThat(supplier.whenHasName(() -> "test").getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.LAX);
}
@Test
void whenHasSuppliedNameWhenNameDoesNotMatchDoesNotCallGetSameSite() {
CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called");
assertThat(supplier.whenHasName(() -> "test").getSameSite(new Cookie("tset", "x"))).isNull();
}
@Test
void whenHasNameMatchingRegexWhenRegexIsNullThrowsException() {
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasNameMatching((String) null))
.withMessage("'regex' must not be empty");
}
@Test
void whenHasNameMatchingRegexWhenRegexIsEmptyThrowsException() {
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasNameMatching(""))
.withMessage("'regex' must not be empty");
}
@Test
void whenHasNameMatchingRegexWhenNameMatchesCallsGetSameSite() {
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
assertThat(supplier.whenHasNameMatching("te.*").getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.LAX);
}
@Test
void whenHasNameMatchingRegexWhenNameDoesNotMatchDoesNotCallGetSameSite() {
CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called");
assertThat(supplier.whenHasNameMatching("te.*").getSameSite(new Cookie("tset", "x"))).isNull();
}
@Test
void whenHasNameMatchingPatternWhenPatternIsNullThrowsException() {
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasNameMatching((Pattern) null))
.withMessage("'pattern' must not be null");
}
@Test
void whenHasNameMatchingPatternWhenNameMatchesCallsGetSameSite() {
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
assertThat(supplier.whenHasNameMatching(Pattern.compile("te.*")).getSameSite(new Cookie("test", "x")))
.isEqualTo(SameSite.LAX);
}
@Test
void whenHasNameMatchingPatternWhenNameDoesNotMatchDoesNotCallGetSameSite() {
CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called");
assertThat(supplier.whenHasNameMatching(Pattern.compile("te.*")).getSameSite(new Cookie("tset", "x"))).isNull();
}
@Test
void whenWhenPredicateIsNullThrowsException() {
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
assertThatIllegalArgumentException().isThrownBy(() -> supplier.when(null))
.withMessage("'predicate' must not be null");
}
@Test
void whenWhenPredicateMatchesCallsGetSameSite() {
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
assertThat(supplier.when((cookie) -> cookie.getName().equals("test")).getSameSite(new Cookie("test", "x")))
.isEqualTo(SameSite.LAX);
}
@Test
void whenWhenPredicateDoesNotMatchDoesNotCallGetSameSite() {
CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called");
assertThat(supplier.when((cookie) -> cookie.getName().equals("test")).getSameSite(new Cookie("tset", "x")))
.isNull();
}
@Test
void ofNoneSuppliesNone() {
assertThat(CookieSameSiteSupplier.ofNone().getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.NONE);
}
@Test
void ofLaxSuppliesLax() {
assertThat(CookieSameSiteSupplier.ofLax().getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.LAX);
}
@Test
void ofStrictSuppliesStrict() {
assertThat(CookieSameSiteSupplier.ofStrict().getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.STRICT);
}
@Test
void ofWhenNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> CookieSameSiteSupplier.of(null))
.withMessage("'sameSite' must not be null");
}
@Test
void ofSuppliesValue() {
assertThat(CookieSameSiteSupplier.of(SameSite.STRICT).getSameSite(new Cookie("test", "x")))
.isEqualTo(SameSite.STRICT);
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.servlet;
import java.io.File;
import java.net.URL;
import java.security.CodeSource;
import java.security.cert.Certificate;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DocumentRoot}.
*
* @author Phillip Webb
*/
class DocumentRootTests {
@TempDir
File tempDir;
private final DocumentRoot documentRoot = new DocumentRoot(LogFactory.getLog(getClass()));
@Test
void explodedWarFileDocumentRootWhenRunningFromExplodedWar() throws Exception {
File codeSourceFile = new File(this.tempDir, "test.war/WEB-INF/lib/spring-boot.jar");
codeSourceFile.getParentFile().mkdirs();
codeSourceFile.createNewFile();
File directory = this.documentRoot.getExplodedWarFileDocumentRoot(codeSourceFile);
assertThat(directory).isEqualTo(codeSourceFile.getParentFile().getParentFile().getParentFile());
}
@Test
void explodedWarFileDocumentRootWhenRunningFromPackagedWar() {
File codeSourceFile = new File(this.tempDir, "test.war");
File directory = this.documentRoot.getExplodedWarFileDocumentRoot(codeSourceFile);
assertThat(directory).isNull();
}
@Test
void codeSourceArchivePath() throws Exception {
CodeSource codeSource = new CodeSource(new URL("file", "", "/some/test/path/"), (Certificate[]) null);
File codeSourceArchive = this.documentRoot.getCodeSourceArchive(codeSource);
assertThat(codeSourceArchive).isEqualTo(new File("/some/test/path/"));
}
@Test
void codeSourceArchivePathContainingSpaces() throws Exception {
CodeSource codeSource = new CodeSource(new URL("file", "", "/test/path/with%20space/"), (Certificate[]) null);
File codeSourceArchive = this.documentRoot.getCodeSourceArchive(codeSource);
assertThat(codeSourceArchive).isEqualTo(new File("/test/path/with space/"));
}
}

View File

@@ -0,0 +1,170 @@
/*
* 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.servlet;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.List;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
* Tests for {@link StaticResourceJars}.
*
* @author Rupert Madden-Abbott
* @author Andy Wilkinson
*/
class StaticResourceJarsTests {
@TempDir
File tempDir;
@Test
void includeJarWithStaticResources() throws Exception {
File jarFile = createResourcesJar("test-resources.jar");
List<URL> staticResourceJarUrls = new StaticResourceJars().getUrlsFrom(jarFile.toURI().toURL());
assertThat(staticResourceJarUrls).hasSize(1);
}
@Test
void includeJarWithStaticResourcesWithUrlEncodedSpaces() throws Exception {
File jarFile = createResourcesJar("test resources.jar");
List<URL> staticResourceJarUrls = new StaticResourceJars().getUrlsFrom(jarFile.toURI().toURL());
assertThat(staticResourceJarUrls).hasSize(1);
}
@Test
void includeJarWithStaticResourcesWithPlusInItsPath() throws Exception {
File jarFile = createResourcesJar("test + resources.jar");
List<URL> staticResourceJarUrls = new StaticResourceJars().getUrlsFrom(jarFile.toURI().toURL());
assertThat(staticResourceJarUrls).hasSize(1);
}
@Test
void excludeJarWithoutStaticResources() throws Exception {
File jarFile = createJar("dependency.jar");
List<URL> staticResourceJarUrls = new StaticResourceJars().getUrlsFrom(jarFile.toURI().toURL());
assertThat(staticResourceJarUrls).isEmpty();
}
@Test
void uncPathsAreTolerated() throws Exception {
File jarFile = createResourcesJar("test-resources.jar");
List<URL> staticResourceJarUrls = new StaticResourceJars().getUrlsFrom(jarFile.toURI().toURL(),
new URL("file://unc.example.com/test.jar"));
assertThat(staticResourceJarUrls).hasSize(1);
}
@Test
void ignoreWildcardUrls() throws Exception {
File jarFile = createResourcesJar("test-resources.jar");
URL folderUrl = jarFile.getParentFile().toURI().toURL();
URL wildcardUrl = new URL(folderUrl + "*.jar");
List<URL> staticResourceJarUrls = new StaticResourceJars().getUrlsFrom(wildcardUrl);
assertThat(staticResourceJarUrls).isEmpty();
}
@Test
void doesNotCloseJarFromCachedConnection() throws Exception {
File jarFile = createResourcesJar("test-resources.jar");
TrackedURLStreamHandler handler = new TrackedURLStreamHandler(true);
URL url = new URL("jar", null, 0, jarFile.toURI().toURL() + "!/", handler);
try {
new StaticResourceJars().getUrlsFrom(url);
assertThatNoException()
.isThrownBy(() -> ((JarURLConnection) handler.getConnection()).getJarFile().getComment());
}
finally {
((JarURLConnection) handler.getConnection()).getJarFile().close();
}
}
@Test
void closesJarFromNonCachedConnection() throws Exception {
File jarFile = createResourcesJar("test-resources.jar");
TrackedURLStreamHandler handler = new TrackedURLStreamHandler(false);
URL url = new URL("jar", null, 0, jarFile.toURI().toURL() + "!/", handler);
new StaticResourceJars().getUrlsFrom(url);
assertThatIllegalStateException()
.isThrownBy(() -> ((JarURLConnection) handler.getConnection()).getJarFile().getComment())
.withMessageContaining("closed");
}
private File createResourcesJar(String name) throws IOException {
return createJar(name, (output) -> {
JarEntry jarEntry = new JarEntry("META-INF/resources");
try {
output.putNextEntry(jarEntry);
output.closeEntry();
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
});
}
private File createJar(String name) throws IOException {
return createJar(name, null);
}
private File createJar(String name, Consumer<JarOutputStream> customizer) throws IOException {
File jarFile = new File(this.tempDir, name);
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(jarFile));
if (customizer != null) {
customizer.accept(jarOutputStream);
}
jarOutputStream.close();
return jarFile;
}
private static class TrackedURLStreamHandler extends URLStreamHandler {
private final boolean useCaches;
private URLConnection connection;
TrackedURLStreamHandler(boolean useCaches) {
this.useCaches = useCaches;
}
@Override
protected URLConnection openConnection(URL u) throws IOException {
this.connection = new URL(u.toExternalForm()).openConnection();
this.connection.setUseCaches(this.useCaches);
return this.connection;
}
URLConnection getConnection() {
return this.connection;
}
}
}

View File

@@ -0,0 +1,222 @@
/*
* 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.servlet.context;
import jakarta.servlet.GenericServlet;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.server.servlet.MockServletWebServerFactory;
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
import org.springframework.boot.web.server.servlet.context.config.ExampleServletWebServerApplicationConfiguration;
import org.springframework.boot.web.servlet.mock.MockServlet;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
/**
* Tests for {@link AnnotationConfigServletWebServerApplicationContext}.
*
* @author Phillip Webb
*/
class AnnotationConfigServletWebServerApplicationContextTests {
private AnnotationConfigServletWebServerApplicationContext context;
@AfterEach
void close() {
if (this.context != null) {
this.context.close();
}
}
@Test
void createFromScan() {
this.context = new AnnotationConfigServletWebServerApplicationContext(
ExampleServletWebServerApplicationConfiguration.class.getPackage().getName());
verifyContext();
}
@Test
void sessionScopeAvailable() {
this.context = new AnnotationConfigServletWebServerApplicationContext(
ExampleServletWebServerApplicationConfiguration.class, SessionScopedComponent.class);
verifyContext();
}
@Test
void sessionScopeAvailableToServlet() {
this.context = new AnnotationConfigServletWebServerApplicationContext(
ExampleServletWebServerApplicationConfiguration.class, ExampleServletWithAutowired.class,
SessionScopedComponent.class);
Servlet servlet = this.context.getBean(ExampleServletWithAutowired.class);
assertThat(servlet).isNotNull();
}
@Test
void createFromConfigClass() {
this.context = new AnnotationConfigServletWebServerApplicationContext(
ExampleServletWebServerApplicationConfiguration.class);
verifyContext();
}
@Test
void registerAndRefresh() {
this.context = new AnnotationConfigServletWebServerApplicationContext();
this.context.register(ExampleServletWebServerApplicationConfiguration.class);
this.context.refresh();
verifyContext();
}
@Test
void multipleRegistersAndRefresh() {
this.context = new AnnotationConfigServletWebServerApplicationContext();
this.context.register(WebServerConfiguration.class);
this.context.register(ServletContextAwareConfiguration.class);
this.context.refresh();
assertThat(this.context.getBeansOfType(Servlet.class)).hasSize(1);
assertThat(this.context.getBeansOfType(ServletWebServerFactory.class)).hasSize(1);
}
@Test
void scanAndRefresh() {
this.context = new AnnotationConfigServletWebServerApplicationContext();
this.context.scan(ExampleServletWebServerApplicationConfiguration.class.getPackage().getName());
this.context.refresh();
verifyContext();
}
@Test
void createAndInitializeCyclic() {
this.context = new AnnotationConfigServletWebServerApplicationContext(
ServletContextAwareEmbeddedConfiguration.class);
verifyContext();
// You can't initialize the application context and inject the servlet context
// because of a cycle - we'd like this to be not null, but it never will be
assertThat(this.context.getBean(ServletContextAwareEmbeddedConfiguration.class).getServletContext()).isNull();
}
@Test
void createAndInitializeWithParent() {
AnnotationConfigServletWebServerApplicationContext parent = new AnnotationConfigServletWebServerApplicationContext(
WebServerConfiguration.class);
this.context = new AnnotationConfigServletWebServerApplicationContext();
this.context.register(WebServerConfiguration.class, ServletContextAwareConfiguration.class);
this.context.setParent(parent);
this.context.refresh();
verifyContext();
assertThat(this.context.getBean(ServletContextAwareConfiguration.class).getServletContext()).isNotNull();
}
private void verifyContext() {
MockServletWebServerFactory factory = this.context.getBean(MockServletWebServerFactory.class);
Servlet servlet = this.context.getBean(Servlet.class);
then(factory.getServletContext()).should().addServlet("servlet", servlet);
}
@Component
static class ExampleServletWithAutowired extends GenericServlet {
@Autowired
private SessionScopedComponent component;
@Override
public void service(ServletRequest req, ServletResponse res) {
assertThat(this.component).isNotNull();
}
}
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
static class SessionScopedComponent {
}
@Configuration(proxyBeanMethods = false)
@EnableWebMvc
static class ServletContextAwareEmbeddedConfiguration implements ServletContextAware {
private ServletContext servletContext;
@Bean
ServletWebServerFactory webServerFactory() {
return new MockServletWebServerFactory();
}
@Bean
Servlet servlet() {
return new MockServlet();
}
@Override
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
ServletContext getServletContext() {
return this.servletContext;
}
}
@Configuration(proxyBeanMethods = false)
static class WebServerConfiguration {
@Bean
ServletWebServerFactory webServerFactory() {
return new MockServletWebServerFactory();
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebMvc
static class ServletContextAwareConfiguration implements ServletContextAware {
private ServletContext servletContext;
@Bean
Servlet servlet() {
return new MockServlet();
}
@Override
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
ServletContext getServletContext() {
return this.servletContext;
}
}
}

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.servlet.context;
import org.springframework.boot.AbstractApplicationEnvironmentTests;
import org.springframework.boot.web.context.servlet.ApplicationServletEnvironment;
import org.springframework.core.env.StandardEnvironment;
/**
* Tests for {@link ApplicationServletEnvironment}.
*
* @author Phillip Webb
*/
class ApplicationServletEnvironmentTests extends AbstractApplicationEnvironmentTests {
@Override
protected StandardEnvironment createEnvironment() {
return new ApplicationServletEnvironment();
}
}

View File

@@ -0,0 +1,144 @@
/*
* 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.servlet.context;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Map;
import java.util.Properties;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.annotation.WebServlet;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.testsupport.classpath.ForkedClassPath;
import org.springframework.boot.web.context.servlet.AnnotationConfigServletWebApplicationContext;
import org.springframework.boot.web.server.servlet.WebListenerRegistrar;
import org.springframework.boot.web.server.servlet.WebListenerRegistry;
import org.springframework.boot.web.server.servlet.context.testcomponents.filter.TestFilter;
import org.springframework.boot.web.server.servlet.context.testcomponents.listener.TestListener;
import org.springframework.boot.web.server.servlet.context.testcomponents.servlet.TestMultipartServlet;
import org.springframework.boot.web.server.servlet.context.testcomponents.servlet.TestServlet;
import org.springframework.boot.web.servlet.RegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.mock.web.MockServletContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Integration tests for {@link ServletComponentScan @ServletComponentScan} with a mock
* web environment.
*
* @author Andy Wilkinson
*/
class MockWebEnvironmentServletComponentScanIntegrationTests {
private AnnotationConfigServletWebApplicationContext context;
@TempDir
File temp;
@AfterEach
void cleanUp() {
if (this.context != null) {
this.context.close();
}
}
@Test
@ForkedClassPath
void componentsAreRegistered() {
prepareContext();
this.context.refresh();
Map<String, RegistrationBean> registrationBeans = this.context.getBeansOfType(RegistrationBean.class);
assertThat(registrationBeans).hasSize(3);
assertThat(registrationBeans.keySet()).containsExactlyInAnyOrder(TestServlet.class.getName(),
TestFilter.class.getName(), TestMultipartServlet.class.getName());
WebListenerRegistry registry = mock(WebListenerRegistry.class);
this.context.getBean(WebListenerRegistrar.class).register(registry);
then(registry).should().addWebListeners(TestListener.class.getName());
}
@Test
@ForkedClassPath
void indexedComponentsAreRegistered() throws IOException {
writeIndex(this.temp);
prepareContext();
try (URLClassLoader classLoader = new URLClassLoader(new URL[] { this.temp.toURI().toURL() },
getClass().getClassLoader())) {
this.context.setClassLoader(classLoader);
this.context.refresh();
Map<String, RegistrationBean> registrationBeans = this.context.getBeansOfType(RegistrationBean.class);
assertThat(registrationBeans).hasSize(2);
assertThat(registrationBeans.keySet()).containsExactlyInAnyOrder(TestServlet.class.getName(),
TestFilter.class.getName());
WebListenerRegistry registry = mock(WebListenerRegistry.class);
this.context.getBean(WebListenerRegistrar.class).register(registry);
then(registry).should().addWebListeners(TestListener.class.getName());
}
}
@Test
@ForkedClassPath
void multipartConfigIsHonoured() {
prepareContext();
this.context.refresh();
@SuppressWarnings("rawtypes")
Map<String, ServletRegistrationBean> beans = this.context.getBeansOfType(ServletRegistrationBean.class);
ServletRegistrationBean<?> servletRegistrationBean = beans.get(TestMultipartServlet.class.getName());
assertThat(servletRegistrationBean).isNotNull();
MultipartConfigElement multipartConfig = servletRegistrationBean.getMultipartConfig();
assertThat(multipartConfig).isNotNull();
assertThat(multipartConfig.getLocation()).isEqualTo("test");
assertThat(multipartConfig.getMaxRequestSize()).isEqualTo(2048);
assertThat(multipartConfig.getMaxFileSize()).isEqualTo(1024);
assertThat(multipartConfig.getFileSizeThreshold()).isEqualTo(512);
}
private void writeIndex(File temp) throws IOException {
File metaInf = new File(temp, "META-INF");
metaInf.mkdirs();
Properties index = new Properties();
index.setProperty(TestFilter.class.getName(), WebFilter.class.getName());
index.setProperty(TestListener.class.getName(), WebListener.class.getName());
index.setProperty(TestServlet.class.getName(), WebServlet.class.getName());
try (FileWriter writer = new FileWriter(new File(metaInf, "spring.components"))) {
index.store(writer, null);
}
}
private void prepareContext() {
this.context = new AnnotationConfigServletWebApplicationContext();
this.context.register(ScanningConfiguration.class);
this.context.setServletContext(new MockServletContext());
}
@ServletComponentScan(basePackages = "org.springframework.boot.web.server.servlet.context.testcomponents")
static class ScanningConfiguration {
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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.servlet.context;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Map;
import java.util.Properties;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.annotation.WebServlet;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
import org.springframework.boot.web.server.servlet.MockServletWebServerFactory;
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
import org.springframework.boot.web.server.servlet.WebListenerRegistrar;
import org.springframework.boot.web.server.servlet.context.testcomponents.filter.TestFilter;
import org.springframework.boot.web.server.servlet.context.testcomponents.listener.TestListener;
import org.springframework.boot.web.server.servlet.context.testcomponents.servlet.TestMultipartServlet;
import org.springframework.boot.web.server.servlet.context.testcomponents.servlet.TestServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link ServletComponentScan @ServletComponentScan}
*
* @author Andy Wilkinson
*/
class ServletComponentScanIntegrationTests {
private AnnotationConfigServletWebServerApplicationContext context;
@TempDir
File temp;
@AfterEach
void cleanUp() {
if (this.context != null) {
this.context.close();
}
}
@Test
void componentsAreRegistered() {
this.context = new AnnotationConfigServletWebServerApplicationContext();
this.context.register(TestConfiguration.class);
this.context.refresh();
assertThat(this.context.getServletContext().getFilterRegistrations()).hasSize(1)
.containsKey(TestFilter.class.getName());
assertThat(this.context.getServletContext().getServletRegistrations()).hasSize(2)
.containsKeys(TestServlet.class.getName(), TestMultipartServlet.class.getName());
assertThat(this.context.getBean(MockServletWebServerFactory.class).getSettings().getWebListenerClassNames())
.containsExactly(TestListener.class.getName());
}
@Test
void indexedComponentsAreRegistered() throws IOException {
writeIndex(this.temp);
this.context = new AnnotationConfigServletWebServerApplicationContext();
try (URLClassLoader classLoader = new URLClassLoader(new URL[] { this.temp.toURI().toURL() },
getClass().getClassLoader())) {
this.context.setClassLoader(classLoader);
this.context.register(TestConfiguration.class);
this.context.refresh();
assertThat(this.context.getServletContext().getFilterRegistrations()).hasSize(1)
.containsKey(TestFilter.class.getName());
assertThat(this.context.getServletContext().getServletRegistrations()).hasSize(1)
.containsKeys(TestServlet.class.getName());
assertThat(this.context.getBean(MockServletWebServerFactory.class).getSettings().getWebListenerClassNames())
.containsExactly(TestListener.class.getName());
}
}
@Test
void multipartConfigIsHonoured() {
this.context = new AnnotationConfigServletWebServerApplicationContext();
this.context.register(TestConfiguration.class);
this.context.refresh();
@SuppressWarnings("rawtypes")
Map<String, ServletRegistrationBean> beans = this.context.getBeansOfType(ServletRegistrationBean.class);
ServletRegistrationBean<?> servletRegistrationBean = beans.get(TestMultipartServlet.class.getName());
assertThat(servletRegistrationBean).isNotNull();
MultipartConfigElement multipartConfig = servletRegistrationBean.getMultipartConfig();
assertThat(multipartConfig).isNotNull();
assertThat(multipartConfig.getLocation()).isEqualTo("test");
assertThat(multipartConfig.getMaxRequestSize()).isEqualTo(2048);
assertThat(multipartConfig.getMaxFileSize()).isEqualTo(1024);
assertThat(multipartConfig.getFileSizeThreshold()).isEqualTo(512);
}
private void writeIndex(File temp) throws IOException {
File metaInf = new File(temp, "META-INF");
metaInf.mkdirs();
Properties index = new Properties();
index.setProperty(TestFilter.class.getName(), WebFilter.class.getName());
index.setProperty(TestListener.class.getName(), WebListener.class.getName());
index.setProperty(TestServlet.class.getName(), WebServlet.class.getName());
try (FileWriter writer = new FileWriter(new File(metaInf, "spring.components"))) {
index.store(writer, null);
}
}
@ServletComponentScan(basePackages = "org.springframework.boot.web.server.servlet.context.testcomponents")
static class TestConfiguration {
@Bean
protected ServletWebServerFactory webServerFactory(ObjectProvider<WebListenerRegistrar> webListenerRegistrars) {
ConfigurableServletWebServerFactory factory = new MockServletWebServerFactory();
webListenerRegistrars.orderedStream().forEach((registrar) -> registrar.register(factory));
return factory;
}
}
}

View File

@@ -0,0 +1,235 @@
/*
* 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.servlet.context;
import java.util.function.Consumer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.web.server.servlet.context.testcomponents.listener.TestListener;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.aot.ApplicationContextAotGenerator;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.annotation.AnnotationConfigurationException;
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.javapoet.ClassName;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
* Tests for {@link ServletComponentScanRegistrar}
*
* @author Andy Wilkinson
*/
class ServletComponentScanRegistrarTests {
private AnnotationConfigApplicationContext context;
@AfterEach
void after() {
if (this.context != null) {
this.context.close();
}
}
@Test
void packagesConfiguredWithValue() {
this.context = new AnnotationConfigApplicationContext(ValuePackages.class);
ServletComponentRegisteringPostProcessor postProcessor = this.context
.getBean(ServletComponentRegisteringPostProcessor.class);
assertThat(postProcessor.getPackagesToScan()).contains("com.example.foo", "com.example.bar");
}
@Test
void packagesConfiguredWithValueAsm() {
this.context = new AnnotationConfigApplicationContext();
this.context.registerBeanDefinition("valuePackages", new RootBeanDefinition(ValuePackages.class.getName()));
this.context.refresh();
ServletComponentRegisteringPostProcessor postProcessor = this.context
.getBean(ServletComponentRegisteringPostProcessor.class);
assertThat(postProcessor.getPackagesToScan()).contains("com.example.foo", "com.example.bar");
}
@Test
void packagesConfiguredWithBackPackages() {
this.context = new AnnotationConfigApplicationContext(BasePackages.class);
ServletComponentRegisteringPostProcessor postProcessor = this.context
.getBean(ServletComponentRegisteringPostProcessor.class);
assertThat(postProcessor.getPackagesToScan()).contains("com.example.foo", "com.example.bar");
}
@Test
void packagesConfiguredWithBasePackageClasses() {
this.context = new AnnotationConfigApplicationContext(BasePackageClasses.class);
ServletComponentRegisteringPostProcessor postProcessor = this.context
.getBean(ServletComponentRegisteringPostProcessor.class);
assertThat(postProcessor.getPackagesToScan()).contains(getClass().getPackage().getName());
}
@Test
void packagesConfiguredWithBothValueAndBasePackages() {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> this.context = new AnnotationConfigApplicationContext(ValueAndBasePackages.class))
.withMessageContaining("'value'")
.withMessageContaining("'basePackages'")
.withMessageContaining("com.example.foo")
.withMessageContaining("com.example.bar");
}
@Test
void packagesFromMultipleAnnotationsAreMerged() {
this.context = new AnnotationConfigApplicationContext(BasePackages.class, AdditionalPackages.class);
ServletComponentRegisteringPostProcessor postProcessor = this.context
.getBean(ServletComponentRegisteringPostProcessor.class);
assertThat(postProcessor.getPackagesToScan()).contains("com.example.foo", "com.example.bar", "com.example.baz");
}
@Test
void withNoBasePackagesScanningUsesBasePackageOfAnnotatedClass() {
this.context = new AnnotationConfigApplicationContext(NoBasePackages.class);
ServletComponentRegisteringPostProcessor postProcessor = this.context
.getBean(ServletComponentRegisteringPostProcessor.class);
assertThat(postProcessor.getPackagesToScan())
.containsExactly("org.springframework.boot.web.server.servlet.context");
}
@Test
void noBasePackageAndBasePackageAreCombinedCorrectly() {
this.context = new AnnotationConfigApplicationContext(NoBasePackages.class, BasePackages.class);
ServletComponentRegisteringPostProcessor postProcessor = this.context
.getBean(ServletComponentRegisteringPostProcessor.class);
assertThat(postProcessor.getPackagesToScan()).containsExactlyInAnyOrder(
"org.springframework.boot.web.server.servlet.context", "com.example.foo", "com.example.bar");
}
@Test
void basePackageAndNoBasePackageAreCombinedCorrectly() {
this.context = new AnnotationConfigApplicationContext(BasePackages.class, NoBasePackages.class);
ServletComponentRegisteringPostProcessor postProcessor = this.context
.getBean(ServletComponentRegisteringPostProcessor.class);
assertThat(postProcessor.getPackagesToScan()).containsExactlyInAnyOrder(
"org.springframework.boot.web.server.servlet.context", "com.example.foo", "com.example.bar");
}
@Test
@CompileWithForkedClassLoader
void processAheadOfTimeDoesNotRegisterServletComponentRegisteringPostProcessor() {
GenericApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBean(BasePackages.class);
compile(context, (freshContext) -> {
freshContext.refresh();
assertThat(freshContext.getBeansOfType(ServletComponentRegisteringPostProcessor.class)).isEmpty();
});
}
@Test
void processAheadOfTimeRegistersReflectionHintsForWebListeners() {
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext();
context.registerBean(ScanListenerPackage.class);
TestGenerationContext generationContext = new TestGenerationContext(
ClassName.get(getClass().getPackageName(), "TestTarget"));
new ApplicationContextAotGenerator().processAheadOfTime(context, generationContext);
assertThat(RuntimeHintsPredicates.reflection()
.onType(TestListener.class)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS))
.accepts(generationContext.getRuntimeHints());
}
@Test
void processAheadOfTimeSucceedsForWebServletWithMultipartConfig() {
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext();
context.registerBean(ScanServletPackage.class);
TestGenerationContext generationContext = new TestGenerationContext(
ClassName.get(getClass().getPackageName(), "TestTarget"));
assertThatNoException()
.isThrownBy(() -> new ApplicationContextAotGenerator().processAheadOfTime(context, generationContext));
}
@SuppressWarnings("unchecked")
private void compile(GenericApplicationContext context, Consumer<GenericApplicationContext> freshContext) {
TestGenerationContext generationContext = new TestGenerationContext(
ClassName.get(getClass().getPackageName(), "TestTarget"));
ClassName className = new ApplicationContextAotGenerator().processAheadOfTime(context, generationContext);
generationContext.writeGeneratedContent();
TestCompiler.forSystem().with(generationContext).compile((compiled) -> {
GenericApplicationContext freshApplicationContext = new GenericApplicationContext();
ApplicationContextInitializer<GenericApplicationContext> initializer = compiled
.getInstance(ApplicationContextInitializer.class, className.toString());
initializer.initialize(freshApplicationContext);
freshContext.accept(freshApplicationContext);
});
}
@Configuration(proxyBeanMethods = false)
@ServletComponentScan({ "com.example.foo", "com.example.bar" })
static class ValuePackages {
}
@Configuration(proxyBeanMethods = false)
@ServletComponentScan(basePackages = { "com.example.foo", "com.example.bar" })
static class BasePackages {
}
@Configuration(proxyBeanMethods = false)
@ServletComponentScan(basePackages = "com.example.baz")
static class AdditionalPackages {
}
@Configuration(proxyBeanMethods = false)
@ServletComponentScan(basePackageClasses = ServletComponentScanRegistrarTests.class)
static class BasePackageClasses {
}
@Configuration(proxyBeanMethods = false)
@ServletComponentScan(value = "com.example.foo", basePackages = "com.example.bar")
static class ValueAndBasePackages {
}
@Configuration(proxyBeanMethods = false)
@ServletComponentScan
static class NoBasePackages {
}
@Configuration(proxyBeanMethods = false)
@ServletComponentScan("org.springframework.boot.web.server.servlet.context.testcomponents.listener")
static class ScanListenerPackage {
}
@Configuration(proxyBeanMethods = false)
@ServletComponentScan("org.springframework.boot.web.server.servlet.context.testcomponents.servlet")
static class ScanServletPackage {
}
}

View File

@@ -0,0 +1,619 @@
/*
* 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.servlet.context;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.EnumSet;
import java.util.List;
import java.util.Properties;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.Scope;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.testsupport.system.CapturedOutput;
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.context.ServerPortInfoApplicationContextInitializer;
import org.springframework.boot.web.server.servlet.MockServletWebServerFactory;
import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean;
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.ApplicationContextException;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockFilterConfig;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.SessionScope;
import org.springframework.web.filter.GenericFilterBean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.withSettings;
/**
* Tests for {@link ServletWebServerApplicationContext}.
*
* @author Phillip Webb
* @author Stephane Nicoll
*/
@ExtendWith({ OutputCaptureExtension.class, MockitoExtension.class })
class ServletWebServerApplicationContextTests {
private final ServletWebServerApplicationContext context = new ServletWebServerApplicationContext();
@Captor
private ArgumentCaptor<Filter> filterCaptor;
@AfterEach
void cleanup() {
this.context.close();
}
@Test
void startRegistrations() {
addWebServerFactoryBean();
this.context.refresh();
MockServletWebServerFactory factory = getWebServerFactory();
// Ensure that the context has been set up
assertThat(this.context.getServletContext()).isEqualTo(factory.getServletContext());
then(factory.getServletContext()).should()
.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
// Ensure WebApplicationContextUtils.registerWebApplicationScopes was called
assertThat(this.context.getBeanFactory().getRegisteredScope(WebApplicationContext.SCOPE_SESSION))
.isInstanceOf(SessionScope.class);
// Ensure WebApplicationContextUtils.registerEnvironmentBeans was called
assertThat(this.context.containsBean(WebApplicationContext.SERVLET_CONTEXT_BEAN_NAME)).isTrue();
}
@Test
void doesNotRegistersShutdownHook() {
// See gh-314 for background. We no longer register the shutdown hook
// since it is really the caller's responsibility. The shutdown hook could
// also be problematic in a classic WAR deployment.
addWebServerFactoryBean();
this.context.refresh();
assertThat(this.context).hasFieldOrPropertyWithValue("shutdownHook", null);
}
@Test
void ServletWebServerInitializedEventPublished() {
addWebServerFactoryBean();
this.context.registerBeanDefinition("listener", new RootBeanDefinition(TestApplicationListener.class));
this.context.refresh();
List<ApplicationEvent> events = this.context.getBean(TestApplicationListener.class).receivedEvents();
assertThat(events).hasSize(2)
.extracting("class")
.containsExactly(ServletWebServerInitializedEvent.class, ContextRefreshedEvent.class);
ServletWebServerInitializedEvent initializedEvent = (ServletWebServerInitializedEvent) events.get(0);
assertThat(initializedEvent.getSource().getPort()).isGreaterThanOrEqualTo(0);
assertThat(initializedEvent.getApplicationContext()).isEqualTo(this.context);
}
@Test
void localPortIsAvailable() {
addWebServerFactoryBean();
new ServerPortInfoApplicationContextInitializer().initialize(this.context);
this.context.refresh();
ConfigurableEnvironment environment = this.context.getEnvironment();
assertThat(environment.containsProperty("local.server.port")).isTrue();
assertThat(environment.getProperty("local.server.port")).isEqualTo("8080");
}
@Test
void stopOnStop() {
addWebServerFactoryBean();
this.context.refresh();
MockServletWebServerFactory factory = getWebServerFactory();
then(factory.getWebServer()).should().start();
this.context.stop();
then(factory.getWebServer()).should().stop();
}
@Test
void startOnStartAfterStop() {
addWebServerFactoryBean();
this.context.refresh();
MockServletWebServerFactory factory = getWebServerFactory();
then(factory.getWebServer()).should().start();
this.context.stop();
then(factory.getWebServer()).should().stop();
this.context.start();
then(factory.getWebServer()).should(times(2)).start();
}
@Test
void stopAndDestroyOnClose() {
addWebServerFactoryBean();
this.context.refresh();
MockServletWebServerFactory factory = getWebServerFactory();
this.context.close();
then(factory.getWebServer()).should(times(2)).stop();
then(factory.getWebServer()).should().destroy();
}
@Test
void applicationIsUnreadyDuringShutdown() {
TestApplicationListener listener = new TestApplicationListener();
addWebServerFactoryBean();
this.context.refresh();
this.context.addApplicationListener(listener);
this.context.close();
assertThat(listener.receivedEvents()).hasSize(2)
.extracting("class")
.contains(AvailabilityChangeEvent.class, ContextClosedEvent.class);
}
@Test
void whenContextIsNotActiveThenCloseDoesNotChangeTheApplicationAvailability() {
addWebServerFactoryBean();
TestApplicationListener listener = new TestApplicationListener();
this.context.addApplicationListener(listener);
this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class));
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh);
this.context.close();
assertThat(listener.receivedEvents()).isEmpty();
}
@Test
void whenContextRefreshFailedThenWebServerIsStoppedAndDestroyed() {
addWebServerFactoryBean();
this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class));
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh);
WebServer webServer = this.context.getWebServer();
then(webServer).should(times(2)).stop();
then(webServer).should().destroy();
}
@Test
void whenContextRefreshFailedThenWebServerStopFailedCatchStopException() {
addWebServerFactoryBean();
this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class, () -> {
willThrow(new RuntimeException("WebServer has failed to stop")).willCallRealMethod()
.given(this.context.getWebServer())
.stop();
return new RefreshFailure();
}));
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh)
.withStackTraceContaining("WebServer has failed to stop");
WebServer webServer = this.context.getWebServer();
then(webServer).should().stop();
then(webServer).should(never()).destroy();
}
@Test
void whenContextRefreshFailedThenWebServerIsStoppedAndDestroyFailedCatchDestroyException() {
addWebServerFactoryBean();
this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class, () -> {
willThrow(new RuntimeException("WebServer has failed to destroy")).willCallRealMethod()
.given(this.context.getWebServer())
.destroy();
return new RefreshFailure();
}));
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh)
.withStackTraceContaining("WebServer has failed to destroy");
WebServer webServer = this.context.getWebServer();
then(webServer).should().stop();
then(webServer).should().destroy();
}
@Test
void cannotSecondRefresh() {
addWebServerFactoryBean();
this.context.refresh();
assertThatIllegalStateException().isThrownBy(this.context::refresh);
}
@Test
void servletContextAwareBeansAreInjected() {
addWebServerFactoryBean();
ServletContextAware bean = mock(ServletContextAware.class);
this.context.registerBeanDefinition("bean", beanDefinition(bean));
this.context.refresh();
then(bean).should().setServletContext(getWebServerFactory().getServletContext());
}
@Test
void missingServletWebServerFactory() {
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(this.context::refresh)
.havingRootCause()
.withMessageContaining("Unable to start ServletWebServerApplicationContext due to missing "
+ "ServletWebServerFactory bean");
}
@Test
void tooManyWebServerFactories() {
addWebServerFactoryBean();
this.context.registerBeanDefinition("webServerFactory2",
new RootBeanDefinition(MockServletWebServerFactory.class));
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(this.context::refresh)
.havingRootCause()
.withMessageContaining("Unable to start ServletWebServerApplicationContext due to "
+ "multiple ServletWebServerFactory beans");
}
@Test
void singleServletBean() {
addWebServerFactoryBean();
Servlet servlet = mock(Servlet.class);
this.context.registerBeanDefinition("servletBean", beanDefinition(servlet));
this.context.refresh();
MockServletWebServerFactory factory = getWebServerFactory();
then(factory.getServletContext()).should().addServlet("servletBean", servlet);
then(factory.getRegisteredServlet(0).getRegistration()).should().addMapping("/");
}
@Test
void orderedBeanInsertedCorrectly() {
addWebServerFactoryBean();
OrderedFilter filter = new OrderedFilter();
this.context.registerBeanDefinition("filterBean", beanDefinition(filter));
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
registration.setName("filterBeanRegistration");
registration.setFilter(mock(Filter.class));
registration.setOrder(100);
this.context.registerBeanDefinition("filterRegistrationBean", beanDefinition(registration));
this.context.refresh();
MockServletWebServerFactory factory = getWebServerFactory();
then(factory.getServletContext()).should().addFilter("filterBean", filter);
then(factory.getServletContext()).should().addFilter("filterBeanRegistration", registration.getFilter());
assertThat(factory.getRegisteredFilter(0).getFilter()).isEqualTo(filter);
}
@Test
void multipleServletBeans() {
addWebServerFactoryBean();
Servlet servlet1 = mock(Servlet.class, withSettings().extraInterfaces(Ordered.class));
given(((Ordered) servlet1).getOrder()).willReturn(1);
Servlet servlet2 = mock(Servlet.class, withSettings().extraInterfaces(Ordered.class));
given(((Ordered) servlet2).getOrder()).willReturn(2);
this.context.registerBeanDefinition("servletBean2", beanDefinition(servlet2));
this.context.registerBeanDefinition("servletBean1", beanDefinition(servlet1));
this.context.refresh();
MockServletWebServerFactory factory = getWebServerFactory();
ServletContext servletContext = factory.getServletContext();
InOrder ordered = inOrder(servletContext);
then(servletContext).should(ordered).addServlet("servletBean1", servlet1);
then(servletContext).should(ordered).addServlet("servletBean2", servlet2);
then(factory.getRegisteredServlet(0).getRegistration()).should().addMapping("/servletBean1/");
then(factory.getRegisteredServlet(1).getRegistration()).should().addMapping("/servletBean2/");
}
@Test
void multipleServletBeansWithMainDispatcher() {
addWebServerFactoryBean();
Servlet servlet1 = mock(Servlet.class, withSettings().extraInterfaces(Ordered.class));
given(((Ordered) servlet1).getOrder()).willReturn(1);
Servlet servlet2 = mock(Servlet.class, withSettings().extraInterfaces(Ordered.class));
given(((Ordered) servlet2).getOrder()).willReturn(2);
this.context.registerBeanDefinition("servletBean2", beanDefinition(servlet2));
this.context.registerBeanDefinition("dispatcherServlet", beanDefinition(servlet1));
this.context.refresh();
MockServletWebServerFactory factory = getWebServerFactory();
ServletContext servletContext = factory.getServletContext();
InOrder ordered = inOrder(servletContext);
then(servletContext).should(ordered).addServlet("dispatcherServlet", servlet1);
then(servletContext).should(ordered).addServlet("servletBean2", servlet2);
then(factory.getRegisteredServlet(0).getRegistration()).should().addMapping("/");
then(factory.getRegisteredServlet(1).getRegistration()).should().addMapping("/servletBean2/");
}
@Test
void servletAndFilterBeans() {
addWebServerFactoryBean();
Servlet servlet = mock(Servlet.class);
Filter filter1 = mock(Filter.class, withSettings().extraInterfaces(Ordered.class));
given(((Ordered) filter1).getOrder()).willReturn(1);
Filter filter2 = mock(Filter.class, withSettings().extraInterfaces(Ordered.class));
given(((Ordered) filter2).getOrder()).willReturn(2);
this.context.registerBeanDefinition("servletBean", beanDefinition(servlet));
this.context.registerBeanDefinition("filterBean2", beanDefinition(filter2));
this.context.registerBeanDefinition("filterBean1", beanDefinition(filter1));
this.context.refresh();
MockServletWebServerFactory factory = getWebServerFactory();
ServletContext servletContext = factory.getServletContext();
InOrder ordered = inOrder(servletContext);
then(factory.getServletContext()).should().addServlet("servletBean", servlet);
then(factory.getRegisteredServlet(0).getRegistration()).should().addMapping("/");
then(factory.getServletContext()).should(ordered).addFilter("filterBean1", filter1);
then(factory.getServletContext()).should(ordered).addFilter("filterBean2", filter2);
then(factory.getRegisteredFilter(0).getRegistration()).should()
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
then(factory.getRegisteredFilter(1).getRegistration()).should()
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
}
@Test
void servletContextInitializerBeans() throws Exception {
addWebServerFactoryBean();
ServletContextInitializer initializer1 = mock(ServletContextInitializer.class,
withSettings().extraInterfaces(Ordered.class));
given(((Ordered) initializer1).getOrder()).willReturn(1);
ServletContextInitializer initializer2 = mock(ServletContextInitializer.class,
withSettings().extraInterfaces(Ordered.class));
given(((Ordered) initializer2).getOrder()).willReturn(2);
this.context.registerBeanDefinition("initializerBean2", beanDefinition(initializer2));
this.context.registerBeanDefinition("initializerBean1", beanDefinition(initializer1));
this.context.refresh();
ServletContext servletContext = getWebServerFactory().getServletContext();
InOrder ordered = inOrder(initializer1, initializer2);
then(initializer1).should(ordered).onStartup(servletContext);
then(initializer2).should(ordered).onStartup(servletContext);
}
@Test
void servletContextListenerBeans() {
addWebServerFactoryBean();
ServletContextListener initializer = mock(ServletContextListener.class);
this.context.registerBeanDefinition("initializerBean", beanDefinition(initializer));
this.context.refresh();
ServletContext servletContext = getWebServerFactory().getServletContext();
then(servletContext).should().addListener(initializer);
}
@Test
void unorderedServletContextInitializerBeans() throws Exception {
addWebServerFactoryBean();
ServletContextInitializer initializer1 = mock(ServletContextInitializer.class);
ServletContextInitializer initializer2 = mock(ServletContextInitializer.class);
this.context.registerBeanDefinition("initializerBean2", beanDefinition(initializer2));
this.context.registerBeanDefinition("initializerBean1", beanDefinition(initializer1));
this.context.refresh();
ServletContext servletContext = getWebServerFactory().getServletContext();
then(initializer1).should().onStartup(servletContext);
then(initializer2).should().onStartup(servletContext);
}
@Test
void servletContextInitializerBeansDoesNotSkipServletsAndFilters() throws Exception {
addWebServerFactoryBean();
ServletContextInitializer initializer = mock(ServletContextInitializer.class);
Servlet servlet = mock(Servlet.class);
Filter filter = mock(Filter.class);
this.context.registerBeanDefinition("initializerBean", beanDefinition(initializer));
this.context.registerBeanDefinition("servletBean", beanDefinition(servlet));
this.context.registerBeanDefinition("filterBean", beanDefinition(filter));
this.context.refresh();
ServletContext servletContext = getWebServerFactory().getServletContext();
then(initializer).should().onStartup(servletContext);
then(servletContext).should().addServlet(anyString(), any(Servlet.class));
then(servletContext).should().addFilter(anyString(), any(Filter.class));
}
@Test
void servletContextInitializerBeansSkipsRegisteredServletsAndFilters() {
addWebServerFactoryBean();
Servlet servlet = mock(Servlet.class);
Filter filter = mock(Filter.class);
ServletRegistrationBean<Servlet> initializer = new ServletRegistrationBean<>(servlet, "/foo");
this.context.registerBeanDefinition("initializerBean", beanDefinition(initializer));
this.context.registerBeanDefinition("servletBean", beanDefinition(servlet));
this.context.registerBeanDefinition("filterBean", beanDefinition(filter));
this.context.refresh();
ServletContext servletContext = getWebServerFactory().getServletContext();
then(servletContext).should(atMost(1)).addServlet(anyString(), any(Servlet.class));
then(servletContext).should(atMost(1)).addFilter(anyString(), any(Filter.class));
}
@Test
void filterRegistrationBeansSkipsRegisteredFilters() {
addWebServerFactoryBean();
Filter filter = mock(Filter.class);
FilterRegistrationBean<Filter> initializer = new FilterRegistrationBean<>(filter);
this.context.registerBeanDefinition("initializerBean", beanDefinition(initializer));
this.context.registerBeanDefinition("filterBean", beanDefinition(filter));
this.context.refresh();
ServletContext servletContext = getWebServerFactory().getServletContext();
then(servletContext).should(atMost(1)).addFilter(anyString(), any(Filter.class));
}
@Test
void delegatingFilterProxyRegistrationBeansSkipsTargetBeanNames() {
addWebServerFactoryBean();
DelegatingFilterProxyRegistrationBean initializer = new DelegatingFilterProxyRegistrationBean("filterBean");
this.context.registerBeanDefinition("initializerBean", beanDefinition(initializer));
BeanDefinition filterBeanDefinition = beanDefinition(new IllegalStateException("Create FilterBean Failure"));
filterBeanDefinition.setLazyInit(true);
this.context.registerBeanDefinition("filterBean", filterBeanDefinition);
this.context.refresh();
ServletContext servletContext = getWebServerFactory().getServletContext();
then(servletContext).should(atMost(1)).addFilter(anyString(), this.filterCaptor.capture());
// Up to this point the filterBean should not have been created, calling
// the delegate proxy will trigger creation and an exception
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> {
this.filterCaptor.getValue().init(new MockFilterConfig());
this.filterCaptor.getValue()
.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse(), new MockFilterChain());
}).withMessageContaining("Create FilterBean Failure");
}
@Test
void postProcessWebServerFactory() {
RootBeanDefinition beanDefinition = new RootBeanDefinition(MockServletWebServerFactory.class);
MutablePropertyValues pv = new MutablePropertyValues();
pv.add("port", "${port}");
beanDefinition.setPropertyValues(pv);
this.context.registerBeanDefinition("webServerFactory", beanDefinition);
PropertySourcesPlaceholderConfigurer propertySupport = new PropertySourcesPlaceholderConfigurer();
Properties properties = new Properties();
properties.put("port", 8080);
propertySupport.setProperties(properties);
this.context.registerBeanDefinition("propertySupport", beanDefinition(propertySupport));
this.context.refresh();
assertThat(getWebServerFactory().getWebServer().getPort()).isEqualTo(8080);
}
@Test
void doesNotReplaceExistingScopes() {
// gh-2082
Scope scope = mock(Scope.class);
ConfigurableListableBeanFactory factory = this.context.getBeanFactory();
factory.registerScope(WebApplicationContext.SCOPE_REQUEST, scope);
factory.registerScope(WebApplicationContext.SCOPE_SESSION, scope);
addWebServerFactoryBean();
this.context.refresh();
assertThat(factory.getRegisteredScope(WebApplicationContext.SCOPE_REQUEST)).isSameAs(scope);
assertThat(factory.getRegisteredScope(WebApplicationContext.SCOPE_SESSION)).isSameAs(scope);
}
@Test
void servletRequestCanBeInjectedEarly(CapturedOutput output) {
// gh-14990
int initialOutputLength = output.length();
addWebServerFactoryBean();
RootBeanDefinition beanDefinition = new RootBeanDefinition(WithAutowiredServletRequest.class);
beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);
this.context.registerBeanDefinition("withAutowiredServletRequest", beanDefinition);
this.context.addBeanFactoryPostProcessor((beanFactory) -> {
WithAutowiredServletRequest bean = beanFactory.getBean(WithAutowiredServletRequest.class);
assertThat(bean.getRequest()).isNotNull();
});
this.context.refresh();
assertThat(output.toString().substring(initialOutputLength)).doesNotContain("Replacing scope");
}
@Test
void webApplicationScopeIsRegistered() {
addWebServerFactoryBean();
this.context.refresh();
assertThat(this.context.getBeanFactory().getRegisteredScope(WebApplicationContext.SCOPE_APPLICATION))
.isNotNull();
}
private void addWebServerFactoryBean() {
this.context.registerBeanDefinition("webServerFactory",
new RootBeanDefinition(MockServletWebServerFactory.class));
}
MockServletWebServerFactory getWebServerFactory() {
return this.context.getBean(MockServletWebServerFactory.class);
}
private BeanDefinition beanDefinition(Object bean) {
RootBeanDefinition beanDefinition = new RootBeanDefinition();
beanDefinition.setBeanClass(getClass());
beanDefinition.setFactoryMethodName("getBean");
ConstructorArgumentValues constructorArguments = new ConstructorArgumentValues();
constructorArguments.addGenericArgumentValue(bean);
beanDefinition.setConstructorArgumentValues(constructorArguments);
return beanDefinition;
}
static <T> T getBean(T object) {
if (object instanceof RuntimeException runtimeException) {
throw runtimeException;
}
return object;
}
static class TestApplicationListener implements ApplicationListener<ApplicationEvent> {
private final Deque<ApplicationEvent> events = new ArrayDeque<>();
@Override
public void onApplicationEvent(ApplicationEvent event) {
this.events.add(event);
}
List<ApplicationEvent> receivedEvents() {
List<ApplicationEvent> receivedEvents = new ArrayList<>();
while (!this.events.isEmpty()) {
receivedEvents.add(this.events.pollFirst());
}
return receivedEvents;
}
}
@Order(10)
static class OrderedFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
}
}
static class WithAutowiredServletRequest {
private final ServletRequest request;
WithAutowiredServletRequest(ServletRequest request) {
this.request = request;
}
ServletRequest getRequest() {
return this.request;
}
}
static class RefreshFailure {
RefreshFailure() {
throw new RuntimeException("Fail refresh");
}
}
}

View File

@@ -0,0 +1,212 @@
/*
* 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.servlet.context;
import java.io.IOException;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.annotation.WebInitParam;
import org.junit.jupiter.api.Test;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link WebFilterHandler}
*
* @author Andy Wilkinson
*/
class WebFilterHandlerTests {
private final WebFilterHandler handler = new WebFilterHandler();
private final SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
@SuppressWarnings("unchecked")
@Test
void defaultFilterConfiguration() throws IOException {
AnnotatedBeanDefinition definition = createBeanDefinition(DefaultConfigurationFilter.class);
this.handler.handle(definition, this.registry);
BeanDefinition filterRegistrationBean = this.registry
.getBeanDefinition(DefaultConfigurationFilter.class.getName());
MutablePropertyValues propertyValues = filterRegistrationBean.getPropertyValues();
assertThat(propertyValues.get("asyncSupported")).isEqualTo(false);
assertThat((EnumSet<DispatcherType>) propertyValues.get("dispatcherTypes"))
.containsExactly(DispatcherType.REQUEST);
assertThat(((Map<String, String>) propertyValues.get("initParameters"))).isEmpty();
assertThat((String[]) propertyValues.get("servletNames")).isEmpty();
assertThat((String[]) propertyValues.get("urlPatterns")).isEmpty();
assertThat(propertyValues.get("name")).isEqualTo(DefaultConfigurationFilter.class.getName());
assertThat(propertyValues.get("filter")).isEqualTo(definition);
}
@Test
void filterWithCustomName() throws IOException {
AnnotatedBeanDefinition definition = createBeanDefinition(CustomNameFilter.class);
this.handler.handle(definition, this.registry);
BeanDefinition filterRegistrationBean = this.registry.getBeanDefinition("custom");
MutablePropertyValues propertyValues = filterRegistrationBean.getPropertyValues();
assertThat(propertyValues.get("name")).isEqualTo("custom");
}
@Test
void asyncSupported() throws IOException {
BeanDefinition filterRegistrationBean = handleBeanDefinitionForClass(AsyncSupportedFilter.class);
MutablePropertyValues propertyValues = filterRegistrationBean.getPropertyValues();
assertThat(propertyValues.get("asyncSupported")).isEqualTo(true);
}
@Test
@SuppressWarnings("unchecked")
void dispatcherTypes() throws IOException {
BeanDefinition filterRegistrationBean = handleBeanDefinitionForClass(DispatcherTypesFilter.class);
MutablePropertyValues propertyValues = filterRegistrationBean.getPropertyValues();
assertThat((Set<DispatcherType>) propertyValues.get("dispatcherTypes")).containsExactly(DispatcherType.FORWARD,
DispatcherType.INCLUDE, DispatcherType.REQUEST);
}
@SuppressWarnings("unchecked")
@Test
void initParameters() throws IOException {
BeanDefinition filterRegistrationBean = handleBeanDefinitionForClass(InitParametersFilter.class);
MutablePropertyValues propertyValues = filterRegistrationBean.getPropertyValues();
assertThat((Map<String, String>) propertyValues.get("initParameters")).containsEntry("a", "alpha")
.containsEntry("b", "bravo");
}
@Test
void servletNames() throws IOException {
BeanDefinition filterRegistrationBean = handleBeanDefinitionForClass(ServletNamesFilter.class);
MutablePropertyValues propertyValues = filterRegistrationBean.getPropertyValues();
assertThat((String[]) propertyValues.get("servletNames")).contains("alpha", "bravo");
}
@Test
void urlPatterns() throws IOException {
BeanDefinition filterRegistrationBean = handleBeanDefinitionForClass(UrlPatternsFilter.class);
MutablePropertyValues propertyValues = filterRegistrationBean.getPropertyValues();
assertThat((String[]) propertyValues.get("urlPatterns")).contains("alpha", "bravo");
}
@Test
void urlPatternsFromValue() throws IOException {
BeanDefinition filterRegistrationBean = handleBeanDefinitionForClass(UrlPatternsFromValueFilter.class);
MutablePropertyValues propertyValues = filterRegistrationBean.getPropertyValues();
assertThat((String[]) propertyValues.get("urlPatterns")).contains("alpha", "bravo");
}
@Test
void urlPatternsDeclaredTwice() {
assertThatIllegalStateException()
.isThrownBy(() -> handleBeanDefinitionForClass(UrlPatternsDeclaredTwiceFilter.class))
.withMessageContaining("The urlPatterns and value attributes are mutually exclusive");
}
private AnnotatedBeanDefinition createBeanDefinition(Class<?> filterClass) throws IOException {
AnnotatedBeanDefinition definition = mock(AnnotatedBeanDefinition.class);
given(definition.getBeanClassName()).willReturn(filterClass.getName());
given(definition.getMetadata()).willReturn(
new SimpleMetadataReaderFactory().getMetadataReader(filterClass.getName()).getAnnotationMetadata());
return definition;
}
private BeanDefinition handleBeanDefinitionForClass(Class<?> filterClass) throws IOException {
this.handler.handle(createBeanDefinition(filterClass), this.registry);
return this.registry.getBeanDefinition(filterClass.getName());
}
@WebFilter
class DefaultConfigurationFilter extends BaseFilter {
}
@WebFilter(asyncSupported = true)
class AsyncSupportedFilter extends BaseFilter {
}
@WebFilter(dispatcherTypes = { DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE })
class DispatcherTypesFilter extends BaseFilter {
}
@WebFilter(initParams = { @WebInitParam(name = "a", value = "alpha"), @WebInitParam(name = "b", value = "bravo") })
class InitParametersFilter extends BaseFilter {
}
@WebFilter(servletNames = { "alpha", "bravo" })
class ServletNamesFilter extends BaseFilter {
}
@WebFilter(urlPatterns = { "alpha", "bravo" })
class UrlPatternsFilter extends BaseFilter {
}
@WebFilter({ "alpha", "bravo" })
class UrlPatternsFromValueFilter extends BaseFilter {
}
@WebFilter(value = { "alpha", "bravo" }, urlPatterns = { "alpha", "bravo" })
class UrlPatternsDeclaredTwiceFilter extends BaseFilter {
}
@WebFilter(filterName = "custom")
class CustomNameFilter extends BaseFilter {
}
class BaseFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
}
@Override
public void destroy() {
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.servlet.context;
import java.io.IOException;
import jakarta.servlet.ServletContextAttributeEvent;
import jakarta.servlet.ServletContextAttributeListener;
import jakarta.servlet.annotation.WebListener;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link WebListenerHandler}.
*
* @author Andy Wilkinson
*/
class WebListenerHandlerTests {
private final WebListenerHandler handler = new WebListenerHandler();
private final SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
@Test
void listener() throws IOException {
AnnotatedBeanDefinition definition = mock(AnnotatedBeanDefinition.class);
given(definition.getBeanClassName()).willReturn(TestListener.class.getName());
given(definition.getMetadata())
.willReturn(new SimpleMetadataReaderFactory().getMetadataReader(TestListener.class.getName())
.getAnnotationMetadata());
this.handler.handle(definition, this.registry);
this.registry.getBeanDefinition(TestListener.class.getName() + "Registrar");
}
@WebListener
static class TestListener implements ServletContextAttributeListener {
@Override
public void attributeAdded(ServletContextAttributeEvent event) {
}
@Override
public void attributeRemoved(ServletContextAttributeEvent event) {
}
@Override
public void attributeReplaced(ServletContextAttributeEvent event) {
}
}
}

View File

@@ -0,0 +1,159 @@
/*
* 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.servlet.context;
import java.io.IOException;
import java.util.Map;
import jakarta.servlet.annotation.WebInitParam;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import org.junit.jupiter.api.Test;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link WebServletHandler}.
*
* @author Andy Wilkinson
*/
class WebServletHandlerTests {
private final WebServletHandler handler = new WebServletHandler();
private final SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
@SuppressWarnings("unchecked")
@Test
void defaultServletConfiguration() throws IOException {
AnnotatedBeanDefinition servletDefinition = createBeanDefinition(DefaultConfigurationServlet.class);
this.handler.handle(servletDefinition, this.registry);
BeanDefinition servletRegistrationBean = this.registry
.getBeanDefinition(DefaultConfigurationServlet.class.getName());
MutablePropertyValues propertyValues = servletRegistrationBean.getPropertyValues();
assertThat(propertyValues.get("asyncSupported")).isEqualTo(false);
assertThat(((Map<String, String>) propertyValues.get("initParameters"))).isEmpty();
assertThat((Integer) propertyValues.get("loadOnStartup")).isEqualTo(-1);
assertThat(propertyValues.get("name")).isEqualTo(DefaultConfigurationServlet.class.getName());
assertThat((String[]) propertyValues.get("urlMappings")).isEmpty();
assertThat(propertyValues.get("servlet")).isEqualTo(servletDefinition);
}
@Test
void servletWithCustomName() throws IOException {
AnnotatedBeanDefinition definition = createBeanDefinition(CustomNameServlet.class);
this.handler.handle(definition, this.registry);
BeanDefinition servletRegistrationBean = this.registry.getBeanDefinition("custom");
MutablePropertyValues propertyValues = servletRegistrationBean.getPropertyValues();
assertThat(propertyValues.get("name")).isEqualTo("custom");
}
@Test
void asyncSupported() throws IOException {
BeanDefinition servletRegistrationBean = handleBeanDefinitionForClass(AsyncSupportedServlet.class);
MutablePropertyValues propertyValues = servletRegistrationBean.getPropertyValues();
assertThat(propertyValues.get("asyncSupported")).isEqualTo(true);
}
@SuppressWarnings("unchecked")
@Test
void initParameters() throws IOException {
BeanDefinition servletRegistrationBean = handleBeanDefinitionForClass(InitParametersServlet.class);
MutablePropertyValues propertyValues = servletRegistrationBean.getPropertyValues();
assertThat((Map<String, String>) propertyValues.get("initParameters")).containsEntry("a", "alpha")
.containsEntry("b", "bravo");
}
@Test
void urlMappings() throws IOException {
BeanDefinition servletRegistrationBean = handleBeanDefinitionForClass(UrlPatternsServlet.class);
MutablePropertyValues propertyValues = servletRegistrationBean.getPropertyValues();
assertThat((String[]) propertyValues.get("urlMappings")).contains("alpha", "bravo");
}
@Test
void urlMappingsFromValue() throws IOException {
BeanDefinition servletRegistrationBean = handleBeanDefinitionForClass(UrlPatternsFromValueServlet.class);
MutablePropertyValues propertyValues = servletRegistrationBean.getPropertyValues();
assertThat((String[]) propertyValues.get("urlMappings")).contains("alpha", "bravo");
}
@Test
void urlPatternsDeclaredTwice() {
assertThatIllegalStateException()
.isThrownBy(() -> handleBeanDefinitionForClass(UrlPatternsDeclaredTwiceServlet.class))
.withMessageContaining("The urlPatterns and value attributes are mutually exclusive");
}
private AnnotatedBeanDefinition createBeanDefinition(Class<?> servletClass) throws IOException {
AnnotatedBeanDefinition definition = mock(AnnotatedBeanDefinition.class);
given(definition.getBeanClassName()).willReturn(servletClass.getName());
given(definition.getMetadata()).willReturn(
new SimpleMetadataReaderFactory().getMetadataReader(servletClass.getName()).getAnnotationMetadata());
return definition;
}
private BeanDefinition handleBeanDefinitionForClass(Class<?> filterClass) throws IOException {
this.handler.handle(createBeanDefinition(filterClass), this.registry);
return this.registry.getBeanDefinition(filterClass.getName());
}
@WebServlet
class DefaultConfigurationServlet extends HttpServlet {
}
@WebServlet(asyncSupported = true)
class AsyncSupportedServlet extends HttpServlet {
}
@WebServlet(initParams = { @WebInitParam(name = "a", value = "alpha"), @WebInitParam(name = "b", value = "bravo") })
class InitParametersServlet extends HttpServlet {
}
@WebServlet(urlPatterns = { "alpha", "bravo" })
class UrlPatternsServlet extends HttpServlet {
}
@WebServlet({ "alpha", "bravo" })
class UrlPatternsFromValueServlet extends HttpServlet {
}
@WebServlet(value = { "alpha", "bravo" }, urlPatterns = { "alpha", "bravo" })
class UrlPatternsDeclaredTwiceServlet extends HttpServlet {
}
@WebServlet(name = "custom")
class CustomNameServlet extends HttpServlet {
}
}

View File

@@ -0,0 +1,90 @@
/*
* 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.servlet.context;
import jakarta.servlet.Servlet;
import org.junit.jupiter.api.Test;
import org.springframework.boot.web.server.servlet.MockServletWebServerFactory;
import org.springframework.core.io.ClassPathResource;
import static org.mockito.BDDMockito.then;
/**
* Tests for {@link XmlServletWebServerApplicationContext}.
*
* @author Phillip Webb
*/
class XmlServletWebServerApplicationContextTests {
private static final String PATH = XmlServletWebServerApplicationContextTests.class.getPackage()
.getName()
.replace('.', '/') + "/";
private static final String FILE = "exampleEmbeddedWebApplicationConfiguration.xml";
private XmlServletWebServerApplicationContext context;
@Test
void createFromResource() {
this.context = new XmlServletWebServerApplicationContext(new ClassPathResource(FILE, getClass()));
verifyContext();
}
@Test
void createFromResourceLocation() {
this.context = new XmlServletWebServerApplicationContext(PATH + FILE);
verifyContext();
}
@Test
void createFromRelativeResourceLocation() {
this.context = new XmlServletWebServerApplicationContext(getClass(), FILE);
verifyContext();
}
@Test
void loadAndRefreshFromResource() {
this.context = new XmlServletWebServerApplicationContext();
this.context.load(new ClassPathResource(FILE, getClass()));
this.context.refresh();
verifyContext();
}
@Test
void loadAndRefreshFromResourceLocation() {
this.context = new XmlServletWebServerApplicationContext();
this.context.load(PATH + FILE);
this.context.refresh();
verifyContext();
}
@Test
void loadAndRefreshFromRelativeResourceLocation() {
this.context = new XmlServletWebServerApplicationContext();
this.context.load(getClass(), FILE);
this.context.refresh();
verifyContext();
}
private void verifyContext() {
MockServletWebServerFactory factory = this.context.getBean(MockServletWebServerFactory.class);
Servlet servlet = this.context.getBean(Servlet.class);
then(factory.getServletContext()).should().addServlet("servlet", servlet);
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.servlet.context.config;
import jakarta.servlet.Servlet;
import org.springframework.boot.web.server.servlet.MockServletWebServerFactory;
import org.springframework.boot.web.servlet.mock.MockServlet;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Example {@code @Configuration} for use with
* {@code AnnotationConfigServletWebServerApplicationContextTests}.
*
* @author Phillip Webb
*/
@Configuration(proxyBeanMethods = false)
public class ExampleServletWebServerApplicationConfiguration {
@Bean
public MockServletWebServerFactory webServerFactory() {
return new MockServletWebServerFactory();
}
@Bean
public Servlet servlet() {
return new MockServlet();
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.servlet.context.testcomponents.filter;
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
@WebFilter("/*")
public class TestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
request.setAttribute("filterAttribute", "bravo");
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.servlet.context.testcomponents.listener;
import java.io.IOException;
import java.util.EnumSet;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebListener;
@WebListener
public class TestListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
sce.getServletContext()
.addFilter("listenerAddedFilter", new ListenerAddedFilter())
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
sce.getServletContext().setAttribute("listenerAttribute", "alpha");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
static class ListenerAddedFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
request.setAttribute("listenerAddedFilterAttribute", "charlie");
chain.doFilter(request, response);
}
}
}

View File

@@ -0,0 +1,27 @@
/*
* 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.servlet.context.testcomponents.servlet;
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
@WebServlet("/test-multipart")
@MultipartConfig(location = "test", maxFileSize = 1024, maxRequestSize = 2048, fileSizeThreshold = 512)
public class TestMultipartServlet extends HttpServlet {
}

Some files were not shown because too many files have changed in this diff Show More