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