Create spring-boot-web-server module
This commit is contained in:
committed by
Phillip Webb
parent
0cf76bf43b
commit
96bee8e034
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 <mime-mapping>} 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {};
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.aot.hint.RuntimeHintsRegistrar=\
|
||||
org.springframework.boot.web.server.MimeMappings$MimeMappingsRuntimeHints
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server;
|
||||
|
||||
import org.apache.coyote.http11.Http11NioProtocol;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link Compression}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class CompressionTests {
|
||||
|
||||
@Test
|
||||
void defaultCompressibleMimeTypesMatchesTomcatsDefault() {
|
||||
assertThat(new Compression().getMimeTypes()).containsExactlyInAnyOrder(getTomcatDefaultCompressibleMimeTypes());
|
||||
}
|
||||
|
||||
private String[] getTomcatDefaultCompressibleMimeTypes() {
|
||||
Http11NioProtocol protocol = new Http11NioProtocol();
|
||||
return protocol.getCompressibleMimeTypes();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
|
||||
import org.springframework.boot.web.server.MimeMappings.DefaultMimeMappings;
|
||||
import org.springframework.boot.web.server.MimeMappings.Mapping;
|
||||
import org.springframework.boot.web.server.MimeMappings.MimeMappingsRuntimeHints;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.support.PropertiesLoaderUtils;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link MimeMappings}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Guirong Hu
|
||||
*/
|
||||
class MimeMappingsTests {
|
||||
|
||||
@Test
|
||||
void defaultsCannotBeModified() {
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class)
|
||||
.isThrownBy(() -> MimeMappings.DEFAULT.add("foo", "foo/bar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromExisting() {
|
||||
MimeMappings mappings = new MimeMappings();
|
||||
mappings.add("foo", "bar");
|
||||
MimeMappings clone = new MimeMappings(mappings);
|
||||
mappings.add("baz", "bar");
|
||||
assertThat(clone.get("foo")).isEqualTo("bar");
|
||||
assertThat(clone.get("baz")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromMap() {
|
||||
Map<String, String> mappings = new HashMap<>();
|
||||
mappings.put("foo", "bar");
|
||||
MimeMappings clone = new MimeMappings(mappings);
|
||||
mappings.put("baz", "bar");
|
||||
assertThat(clone.get("foo")).isEqualTo("bar");
|
||||
assertThat(clone.get("baz")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void iterate() {
|
||||
MimeMappings mappings = new MimeMappings();
|
||||
mappings.add("foo", "bar");
|
||||
mappings.add("baz", "boo");
|
||||
List<MimeMappings.Mapping> mappingList = new ArrayList<>();
|
||||
for (MimeMappings.Mapping mapping : mappings) {
|
||||
mappingList.add(mapping);
|
||||
}
|
||||
assertThat(mappingList.get(0).getExtension()).isEqualTo("foo");
|
||||
assertThat(mappingList.get(0).getMimeType()).isEqualTo("bar");
|
||||
assertThat(mappingList.get(1).getExtension()).isEqualTo("baz");
|
||||
assertThat(mappingList.get(1).getMimeType()).isEqualTo("boo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAll() {
|
||||
MimeMappings mappings = new MimeMappings();
|
||||
mappings.add("foo", "bar");
|
||||
mappings.add("baz", "boo");
|
||||
List<MimeMappings.Mapping> mappingList = new ArrayList<>(mappings.getAll());
|
||||
assertThat(mappingList.get(0).getExtension()).isEqualTo("foo");
|
||||
assertThat(mappingList.get(0).getMimeType()).isEqualTo("bar");
|
||||
assertThat(mappingList.get(1).getExtension()).isEqualTo("baz");
|
||||
assertThat(mappingList.get(1).getMimeType()).isEqualTo("boo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void addNew() {
|
||||
MimeMappings mappings = new MimeMappings();
|
||||
assertThat(mappings.add("foo", "bar")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void addReplacesExisting() {
|
||||
MimeMappings mappings = new MimeMappings();
|
||||
mappings.add("foo", "bar");
|
||||
assertThat(mappings.add("foo", "baz")).isEqualTo("bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void remove() {
|
||||
MimeMappings mappings = new MimeMappings();
|
||||
mappings.add("foo", "bar");
|
||||
assertThat(mappings.remove("foo")).isEqualTo("bar");
|
||||
assertThat(mappings.remove("foo")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void get() {
|
||||
MimeMappings mappings = new MimeMappings();
|
||||
mappings.add("foo", "bar");
|
||||
assertThat(mappings.get("foo")).isEqualTo("bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getMissing() {
|
||||
MimeMappings mappings = new MimeMappings();
|
||||
assertThat(mappings.get("foo")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void makeUnmodifiable() {
|
||||
MimeMappings mappings = new MimeMappings();
|
||||
mappings.add("foo", "bar");
|
||||
MimeMappings unmodifiable = MimeMappings.unmodifiableMappings(mappings);
|
||||
try {
|
||||
unmodifiable.remove("foo");
|
||||
}
|
||||
catch (UnsupportedOperationException ex) {
|
||||
// Expected
|
||||
}
|
||||
mappings.remove("foo");
|
||||
assertThat(unmodifiable.get("foo")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mimeTypesInDefaultMappingsAreCorrectlyStructured() {
|
||||
String regName = "[A-Za-z0-9!#$&.+\\-^_]{1,127}";
|
||||
Pattern pattern = Pattern.compile("^" + regName + "/" + regName + "$");
|
||||
assertThat(MimeMappings.DEFAULT).allSatisfy((mapping) -> assertThat(mapping.getMimeType()).matches(pattern));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCommonTypeOnDefaultMimeMappingsDoesNotLoadMappings() {
|
||||
DefaultMimeMappings mappings = new DefaultMimeMappings();
|
||||
assertThat(mappings.get("json")).isEqualTo("application/json");
|
||||
assertThat((Object) mappings).extracting("loaded").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getExoticTypeOnDefaultMimeMappingsLoadsMappings() {
|
||||
DefaultMimeMappings mappings = new DefaultMimeMappings();
|
||||
assertThat(mappings.get("123")).isEqualTo("application/vnd.lotus-1-2-3");
|
||||
assertThat((Object) mappings).extracting("loaded").isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void iterateOnDefaultMimeMappingsLoadsMappings() {
|
||||
DefaultMimeMappings mappings = new DefaultMimeMappings();
|
||||
assertThat(mappings).isNotEmpty();
|
||||
assertThat((Object) mappings).extracting("loaded").isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void commonMappingsAreSubsetOfAllMappings() {
|
||||
MimeMappings defaultMappings = new DefaultMimeMappings();
|
||||
MimeMappings commonMappings = (MimeMappings) ReflectionTestUtils.getField(DefaultMimeMappings.class, "COMMON");
|
||||
for (Mapping commonMapping : commonMappings) {
|
||||
assertThat(defaultMappings.get(commonMapping.getExtension())).isEqualTo(commonMapping.getMimeType());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void lazyCopyWhenNotMutatedDelegates() {
|
||||
DefaultMimeMappings mappings = new DefaultMimeMappings();
|
||||
MimeMappings lazyCopy = MimeMappings.lazyCopy(mappings);
|
||||
assertThat(lazyCopy.get("json")).isEqualTo("application/json");
|
||||
assertThat((Object) mappings).extracting("loaded").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void lazyCopyWhenMutatedCreatesCopy() {
|
||||
DefaultMimeMappings mappings = new DefaultMimeMappings();
|
||||
MimeMappings lazyCopy = MimeMappings.lazyCopy(mappings);
|
||||
lazyCopy.add("json", "other/json");
|
||||
assertThat(lazyCopy.get("json")).isEqualTo("other/json");
|
||||
assertThat((Object) mappings).extracting("loaded").isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void lazyCopyWhenMutatedCreatesCopyOnlyOnce() {
|
||||
MimeMappings mappings = new MimeMappings();
|
||||
mappings.add("json", "one/json");
|
||||
MimeMappings lazyCopy = MimeMappings.lazyCopy(mappings);
|
||||
lazyCopy.add("first", "copy/yes");
|
||||
assertThat(lazyCopy.get("json")).isEqualTo("one/json");
|
||||
mappings.add("json", "two/json");
|
||||
lazyCopy.add("second", "copy/no");
|
||||
assertThat(lazyCopy.get("json")).isEqualTo("one/json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mimeMappingsMatchesTomcatDefaults() throws IOException {
|
||||
Properties ourDefaultMimeMappings = PropertiesLoaderUtils
|
||||
.loadProperties(new ClassPathResource("mime-mappings.properties", getClass()));
|
||||
Properties tomcatDefaultMimeMappings = PropertiesLoaderUtils
|
||||
.loadProperties(new ClassPathResource("MimeTypeMappings.properties", Tomcat.class));
|
||||
assertThat(ourDefaultMimeMappings).containsExactlyInAnyOrderEntriesOf(tomcatDefaultMimeMappings);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRegisterHints() {
|
||||
RuntimeHints runtimeHints = new RuntimeHints();
|
||||
new MimeMappingsRuntimeHints().registerHints(runtimeHints, getClass().getClassLoader());
|
||||
assertThat(RuntimeHintsPredicates.resource()
|
||||
.forResource("org/springframework/boot/web/server/mime-mappings.properties")).accepts(runtimeHints);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
import org.springframework.boot.web.context.reactive.ReactiveWebApplicationContext;
|
||||
import org.springframework.boot.web.context.reactive.StandardReactiveWebEnvironment;
|
||||
import org.springframework.boot.web.server.reactive.MockReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
|
||||
import org.springframework.boot.web.server.servlet.MockServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.context.AnnotationConfigServletWebServerApplicationContext;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.test.context.support.TestPropertySourceUtils;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.context.support.StandardServletEnvironment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link SpringApplication} with a {@link WebServer}.
|
||||
*
|
||||
* @author Andy wilkinson
|
||||
*/
|
||||
class SpringApplicationWebServerTests {
|
||||
|
||||
private String headlessProperty;
|
||||
|
||||
private ConfigurableApplicationContext context;
|
||||
|
||||
@BeforeEach
|
||||
void storeAndClearHeadlessProperty() {
|
||||
this.headlessProperty = System.getProperty("java.awt.headless");
|
||||
System.clearProperty("java.awt.headless");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void reinstateHeadlessProperty() {
|
||||
if (this.headlessProperty == null) {
|
||||
System.clearProperty("java.awt.headless");
|
||||
}
|
||||
else {
|
||||
System.setProperty("java.awt.headless", this.headlessProperty);
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanUp() {
|
||||
if (this.context != null) {
|
||||
this.context.close();
|
||||
}
|
||||
System.clearProperty("spring.main.banner-mode");
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultApplicationContextForWeb() {
|
||||
SpringApplication application = new SpringApplication(ExampleWebConfig.class);
|
||||
application.setWebApplicationType(WebApplicationType.SERVLET);
|
||||
this.context = application.run();
|
||||
assertThat(this.context).isInstanceOf(AnnotationConfigServletWebServerApplicationContext.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultApplicationContextForReactiveWeb() {
|
||||
SpringApplication application = new SpringApplication(ExampleReactiveWebConfig.class);
|
||||
application.setWebApplicationType(WebApplicationType.REACTIVE);
|
||||
this.context = application.run();
|
||||
assertThat(this.context).isInstanceOf(AnnotationConfigReactiveWebServerApplicationContext.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void environmentForWeb() {
|
||||
SpringApplication application = new SpringApplication(ExampleWebConfig.class);
|
||||
application.setWebApplicationType(WebApplicationType.SERVLET);
|
||||
this.context = application.run();
|
||||
assertThat(this.context.getEnvironment()).isInstanceOf(StandardServletEnvironment.class);
|
||||
assertThat(this.context.getEnvironment().getClass().getName()).endsWith("ApplicationServletEnvironment");
|
||||
}
|
||||
|
||||
@Test
|
||||
void environmentForReactiveWeb() {
|
||||
SpringApplication application = new SpringApplication(ExampleReactiveWebConfig.class);
|
||||
application.setWebApplicationType(WebApplicationType.REACTIVE);
|
||||
this.context = application.run();
|
||||
assertThat(this.context.getEnvironment()).isInstanceOf(StandardReactiveWebEnvironment.class);
|
||||
assertThat(this.context.getEnvironment().getClass().getName()).endsWith("ApplicationReactiveWebEnvironment");
|
||||
}
|
||||
|
||||
@Test
|
||||
void webApplicationConfiguredViaAPropertyHasTheCorrectTypeOfContextAndEnvironment() {
|
||||
ConfigurableApplicationContext context = new SpringApplication(ExampleWebConfig.class)
|
||||
.run("--spring.main.web-application-type=servlet");
|
||||
assertThat(context).isInstanceOf(WebApplicationContext.class);
|
||||
assertThat(context.getEnvironment()).isInstanceOf(StandardServletEnvironment.class);
|
||||
assertThat(context.getEnvironment().getClass().getName()).endsWith("ApplicationServletEnvironment");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reactiveApplicationConfiguredViaAPropertyHasTheCorrectTypeOfContextAndEnvironment() {
|
||||
ConfigurableApplicationContext context = new SpringApplication(ExampleReactiveWebConfig.class)
|
||||
.run("--spring.main.web-application-type=reactive");
|
||||
assertThat(context).isInstanceOf(ReactiveWebApplicationContext.class);
|
||||
assertThat(context.getEnvironment()).isInstanceOf(StandardReactiveWebEnvironment.class);
|
||||
assertThat(context.getEnvironment().getClass().getName()).endsWith("ApplicationReactiveWebEnvironment");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "application-withwebapplicationtype.properties",
|
||||
content = "spring.main.web-application-type=reactive")
|
||||
void environmentIsConvertedIfTypeDoesNotMatch() {
|
||||
ConfigurableApplicationContext context = new SpringApplication(ExampleReactiveWebConfig.class)
|
||||
.run("--spring.profiles.active=withwebapplicationtype");
|
||||
assertThat(context).isInstanceOf(ReactiveWebApplicationContext.class);
|
||||
assertThat(context.getEnvironment()).isInstanceOf(StandardReactiveWebEnvironment.class);
|
||||
assertThat(context.getEnvironment().getClass().getName()).endsWith("ApplicationReactiveWebEnvironment");
|
||||
}
|
||||
|
||||
@Test
|
||||
void webApplicationSwitchedOffInListener() {
|
||||
SpringApplication application = new SpringApplication(ExampleWebConfig.class);
|
||||
application.addListeners((ApplicationListener<ApplicationEnvironmentPreparedEvent>) (event) -> {
|
||||
assertThat(event.getEnvironment().getClass().getName()).endsWith("ApplicationServletEnvironment");
|
||||
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(event.getEnvironment(), "foo=bar");
|
||||
event.getSpringApplication().setWebApplicationType(WebApplicationType.NONE);
|
||||
});
|
||||
this.context = application.run();
|
||||
assertThat(this.context.getEnvironment()).isNotInstanceOf(StandardServletEnvironment.class);
|
||||
assertThat(this.context.getEnvironment().getProperty("foo")).isEqualTo("bar");
|
||||
Iterator<PropertySource<?>> iterator = this.context.getEnvironment().getPropertySources().iterator();
|
||||
assertThat(iterator.next().getName()).isEqualTo("configurationProperties");
|
||||
assertThat(iterator.next().getName())
|
||||
.isEqualTo(TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ExampleWebConfig {
|
||||
|
||||
@Bean
|
||||
MockServletWebServerFactory webServer() {
|
||||
return new MockServletWebServerFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ExampleReactiveWebConfig {
|
||||
|
||||
@Bean
|
||||
MockReactiveWebServerFactory webServerFactory() {
|
||||
return new MockReactiveWebServerFactory();
|
||||
}
|
||||
|
||||
@Bean
|
||||
HttpHandler httpHandler() {
|
||||
return (serverHttpRequest, serverHttpResponse) -> Mono.empty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebServerFactoryCustomizerBeanPostProcessor}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class WebServerFactoryCustomizerBeanPostProcessorTests {
|
||||
|
||||
private final WebServerFactoryCustomizerBeanPostProcessor processor = new WebServerFactoryCustomizerBeanPostProcessor();
|
||||
|
||||
@Mock
|
||||
private ListableBeanFactory beanFactory;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.processor.setBeanFactory(this.beanFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void setBeanFactoryWhenNotListableShouldThrowException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.processor.setBeanFactory(mock(BeanFactory.class)))
|
||||
.withMessageContaining("'beanFactory' must be a ListableBeanFactory");
|
||||
}
|
||||
|
||||
@Test
|
||||
void postProcessBeforeShouldReturnBean() {
|
||||
Object bean = new Object();
|
||||
Object result = this.processor.postProcessBeforeInitialization(bean, "foo");
|
||||
assertThat(result).isSameAs(bean);
|
||||
}
|
||||
|
||||
@Test
|
||||
void postProcessAfterShouldReturnBean() {
|
||||
Object bean = new Object();
|
||||
Object result = this.processor.postProcessAfterInitialization(bean, "foo");
|
||||
assertThat(result).isSameAs(bean);
|
||||
}
|
||||
|
||||
@Test
|
||||
void postProcessAfterShouldCallInterfaceCustomizers() {
|
||||
Map<String, Object> beans = addInterfaceBeans();
|
||||
addMockBeans(beans);
|
||||
postProcessBeforeInitialization(WebServerFactory.class);
|
||||
assertThat(wasCalled(beans, "one")).isFalse();
|
||||
assertThat(wasCalled(beans, "two")).isFalse();
|
||||
assertThat(wasCalled(beans, "all")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void postProcessAfterWhenWebServerFactoryOneShouldCallInterfaceCustomizers() {
|
||||
Map<String, Object> beans = addInterfaceBeans();
|
||||
addMockBeans(beans);
|
||||
postProcessBeforeInitialization(WebServerFactoryOne.class);
|
||||
assertThat(wasCalled(beans, "one")).isTrue();
|
||||
assertThat(wasCalled(beans, "two")).isFalse();
|
||||
assertThat(wasCalled(beans, "all")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void postProcessAfterWhenWebServerFactoryTwoShouldCallInterfaceCustomizers() {
|
||||
Map<String, Object> beans = addInterfaceBeans();
|
||||
addMockBeans(beans);
|
||||
postProcessBeforeInitialization(WebServerFactoryTwo.class);
|
||||
assertThat(wasCalled(beans, "one")).isFalse();
|
||||
assertThat(wasCalled(beans, "two")).isTrue();
|
||||
assertThat(wasCalled(beans, "all")).isTrue();
|
||||
}
|
||||
|
||||
private Map<String, Object> addInterfaceBeans() {
|
||||
WebServerFactoryOneCustomizer oneCustomizer = new WebServerFactoryOneCustomizer();
|
||||
WebServerFactoryTwoCustomizer twoCustomizer = new WebServerFactoryTwoCustomizer();
|
||||
WebServerFactoryAllCustomizer allCustomizer = new WebServerFactoryAllCustomizer();
|
||||
Map<String, Object> beans = new LinkedHashMap<>();
|
||||
beans.put("one", oneCustomizer);
|
||||
beans.put("two", twoCustomizer);
|
||||
beans.put("all", allCustomizer);
|
||||
return beans;
|
||||
}
|
||||
|
||||
@Test
|
||||
void postProcessAfterShouldCallLambdaCustomizers() {
|
||||
List<String> called = new ArrayList<>();
|
||||
addLambdaBeans(called);
|
||||
postProcessBeforeInitialization(WebServerFactory.class);
|
||||
assertThat(called).containsExactly("all");
|
||||
}
|
||||
|
||||
@Test
|
||||
void postProcessAfterWhenWebServerFactoryOneShouldCallLambdaCustomizers() {
|
||||
List<String> called = new ArrayList<>();
|
||||
addLambdaBeans(called);
|
||||
postProcessBeforeInitialization(WebServerFactoryOne.class);
|
||||
assertThat(called).containsExactly("one", "all");
|
||||
}
|
||||
|
||||
@Test
|
||||
void postProcessAfterWhenWebServerFactoryTwoShouldCallLambdaCustomizers() {
|
||||
List<String> called = new ArrayList<>();
|
||||
addLambdaBeans(called);
|
||||
postProcessBeforeInitialization(WebServerFactoryTwo.class);
|
||||
assertThat(called).containsExactly("two", "all");
|
||||
}
|
||||
|
||||
private void addLambdaBeans(List<String> called) {
|
||||
WebServerFactoryCustomizer<WebServerFactoryOne> one = (f) -> called.add("one");
|
||||
WebServerFactoryCustomizer<WebServerFactoryTwo> two = (f) -> called.add("two");
|
||||
WebServerFactoryCustomizer<WebServerFactory> all = (f) -> called.add("all");
|
||||
Map<String, Object> beans = new LinkedHashMap<>();
|
||||
beans.put("one", one);
|
||||
beans.put("two", two);
|
||||
beans.put("all", all);
|
||||
addMockBeans(beans);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
private void addMockBeans(Map<String, ?> beans) {
|
||||
given(this.beanFactory.getBeansOfType(WebServerFactoryCustomizer.class, false, false))
|
||||
.willReturn((Map<String, WebServerFactoryCustomizer>) beans);
|
||||
}
|
||||
|
||||
private void postProcessBeforeInitialization(Class<?> type) {
|
||||
this.processor.postProcessBeforeInitialization(mock(type), "foo");
|
||||
}
|
||||
|
||||
private boolean wasCalled(Map<String, ?> beans, String name) {
|
||||
return ((MockWebServerFactoryCustomizer<?>) beans.get(name)).wasCalled();
|
||||
}
|
||||
|
||||
interface WebServerFactoryOne extends WebServerFactory {
|
||||
|
||||
}
|
||||
|
||||
interface WebServerFactoryTwo extends WebServerFactory {
|
||||
|
||||
}
|
||||
|
||||
static class MockWebServerFactoryCustomizer<T extends WebServerFactory> implements WebServerFactoryCustomizer<T> {
|
||||
|
||||
private boolean called;
|
||||
|
||||
@Override
|
||||
public void customize(T factory) {
|
||||
this.called = true;
|
||||
}
|
||||
|
||||
boolean wasCalled() {
|
||||
return this.called;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class WebServerFactoryOneCustomizer extends MockWebServerFactoryCustomizer<WebServerFactoryOne> {
|
||||
|
||||
}
|
||||
|
||||
static class WebServerFactoryTwoCustomizer extends MockWebServerFactoryCustomizer<WebServerFactoryTwo> {
|
||||
|
||||
}
|
||||
|
||||
static class WebServerFactoryAllCustomizer extends MockWebServerFactoryCustomizer<WebServerFactory> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundleKey;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.boot.ssl.SslStoreBundle;
|
||||
import org.springframework.boot.testsupport.classpath.resources.ResourcePath;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.boot.testsupport.ssl.MockPkcs11Security;
|
||||
import org.springframework.boot.testsupport.ssl.MockPkcs11SecurityProvider;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebServerSslBundle}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
* @author Phillip Webb
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
@MockPkcs11Security
|
||||
class WebServerSslBundleTests {
|
||||
|
||||
@Test
|
||||
void whenSslDisabledThrowsException() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setEnabled(false);
|
||||
assertThatIllegalStateException().isThrownBy(() -> WebServerSslBundle.get(ssl))
|
||||
.withMessage("SSL is not enabled");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.p12")
|
||||
void whenFromJksProperties() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStore("classpath:test.p12");
|
||||
ssl.setKeyStorePassword("secret");
|
||||
ssl.setKeyStoreType("PKCS12");
|
||||
ssl.setTrustStore("classpath:test.p12");
|
||||
ssl.setTrustStorePassword("secret");
|
||||
ssl.setTrustStoreType("PKCS12");
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setKeyAlias("alias");
|
||||
ssl.setClientAuth(Ssl.ClientAuth.NONE);
|
||||
ssl.setCiphers(new String[] { "ONE", "TWO", "THREE" });
|
||||
ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" });
|
||||
ssl.setProtocol("TestProtocol");
|
||||
SslBundle bundle = WebServerSslBundle.get(ssl);
|
||||
assertThat(bundle).isNotNull();
|
||||
assertThat(bundle.getProtocol()).isEqualTo("TestProtocol");
|
||||
SslBundleKey key = bundle.getKey();
|
||||
assertThat(key.getPassword()).isEqualTo("password");
|
||||
assertThat(key.getAlias()).isEqualTo("alias");
|
||||
SslStoreBundle stores = bundle.getStores();
|
||||
assertThat(stores.getKeyStorePassword()).isEqualTo("secret");
|
||||
assertThat(stores.getKeyStore()).isNotNull();
|
||||
assertThat(stores.getTrustStore()).isNotNull();
|
||||
SslOptions options = bundle.getOptions();
|
||||
assertThat(options.getCiphers()).containsExactly("ONE", "TWO", "THREE");
|
||||
assertThat(options.getEnabledProtocols()).containsExactly("TLSv1.1", "TLSv1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void whenFromJksPropertiesWithPkcs11StoreType(@ResourcePath("test.jks") String keyStorePath) {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStoreType("PKCS11");
|
||||
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
|
||||
ssl.setKeyStore(keyStorePath);
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setClientAuth(Ssl.ClientAuth.NONE);
|
||||
assertThatIllegalStateException().isThrownBy(() -> WebServerSslBundle.get(ssl))
|
||||
.withMessageContaining("must be empty or null for PKCS11 hardware key stores");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFromPkcs11Properties() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStoreType("PKCS11");
|
||||
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
|
||||
ssl.setTrustStoreType("PKCS11");
|
||||
ssl.setTrustStoreProvider(MockPkcs11SecurityProvider.NAME);
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setClientAuth(Ssl.ClientAuth.NONE);
|
||||
SslBundle bundle = WebServerSslBundle.get(ssl);
|
||||
assertThat(bundle).isNotNull();
|
||||
assertThat(bundle.getProtocol()).isEqualTo("TLS");
|
||||
SslBundleKey key = bundle.getKey();
|
||||
assertThat(key.getPassword()).isEqualTo("password");
|
||||
SslStoreBundle stores = bundle.getStores();
|
||||
assertThat(stores.getKeyStore()).isNotNull();
|
||||
assertThat(stores.getTrustStore()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources({ "test-cert.pem", "test-key.pem", "test-cert-chain.pem" })
|
||||
void whenFromPemProperties() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setCertificate("classpath:test-cert.pem");
|
||||
ssl.setCertificatePrivateKey("classpath:test-key.pem");
|
||||
ssl.setTrustCertificate("classpath:test-cert-chain.pem");
|
||||
ssl.setKeyStoreType("PKCS12");
|
||||
ssl.setTrustStoreType("PKCS12");
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setClientAuth(Ssl.ClientAuth.NONE);
|
||||
ssl.setCiphers(new String[] { "ONE", "TWO", "THREE" });
|
||||
ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" });
|
||||
ssl.setProtocol("TLSv1.1");
|
||||
SslBundle bundle = WebServerSslBundle.get(ssl);
|
||||
assertThat(bundle).isNotNull();
|
||||
SslBundleKey key = bundle.getKey();
|
||||
assertThat(key.getAlias()).isNull();
|
||||
assertThat(key.getPassword()).isEqualTo("password");
|
||||
SslStoreBundle stores = bundle.getStores();
|
||||
assertThat(stores.getKeyStorePassword()).isNull();
|
||||
assertThat(stores.getKeyStore()).isNotNull();
|
||||
assertThat(stores.getTrustStore()).isNotNull();
|
||||
SslOptions options = bundle.getOptions();
|
||||
assertThat(options.getCiphers()).containsExactly("ONE", "TWO", "THREE");
|
||||
assertThat(options.getEnabledProtocols()).containsExactly("TLSv1.1", "TLSv1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources({ "test-cert.pem", "test-key.pem", "test.p12" })
|
||||
void whenPemKeyStoreAndJksTrustStoreProperties() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setCertificate("classpath:test-cert.pem");
|
||||
ssl.setCertificatePrivateKey("classpath:test-key.pem");
|
||||
ssl.setKeyStoreType("PKCS12");
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setTrustStore("classpath:test.p12");
|
||||
ssl.setTrustStorePassword("secret");
|
||||
ssl.setTrustStoreType("PKCS12");
|
||||
ssl.setClientAuth(Ssl.ClientAuth.NONE);
|
||||
ssl.setCiphers(new String[] { "ONE", "TWO", "THREE" });
|
||||
ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" });
|
||||
ssl.setProtocol("TLSv1.1");
|
||||
SslBundle bundle = WebServerSslBundle.get(ssl);
|
||||
assertThat(bundle).isNotNull();
|
||||
SslBundleKey key = bundle.getKey();
|
||||
assertThat(key.getAlias()).isNull();
|
||||
assertThat(key.getPassword()).isEqualTo("password");
|
||||
SslStoreBundle stores = bundle.getStores();
|
||||
assertThat(stores.getKeyStorePassword()).isNull();
|
||||
assertThat(stores.getKeyStore()).isNotNull();
|
||||
assertThat(stores.getTrustStore()).isNotNull();
|
||||
SslOptions options = bundle.getOptions();
|
||||
assertThat(options.getCiphers()).containsExactly("ONE", "TWO", "THREE");
|
||||
assertThat(options.getEnabledProtocols()).containsExactly("TLSv1.1", "TLSv1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources({ "test.p12", "test-cert-chain.pem" })
|
||||
void whenJksKeyStoreAndPemTrustStoreProperties() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStore("classpath:test.p12");
|
||||
ssl.setKeyStoreType("PKCS12");
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setTrustCertificate("classpath:test-cert-chain.pem");
|
||||
ssl.setTrustStorePassword("secret");
|
||||
ssl.setTrustStoreType("PKCS12");
|
||||
ssl.setClientAuth(Ssl.ClientAuth.NONE);
|
||||
ssl.setCiphers(new String[] { "ONE", "TWO", "THREE" });
|
||||
ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" });
|
||||
ssl.setProtocol("TLSv1.1");
|
||||
SslBundle bundle = WebServerSslBundle.get(ssl);
|
||||
assertThat(bundle).isNotNull();
|
||||
SslBundleKey key = bundle.getKey();
|
||||
assertThat(key.getAlias()).isNull();
|
||||
assertThat(key.getPassword()).isEqualTo("password");
|
||||
SslStoreBundle stores = bundle.getStores();
|
||||
assertThat(stores.getKeyStorePassword()).isNull();
|
||||
assertThat(stores.getKeyStore()).isNotNull();
|
||||
assertThat(stores.getTrustStore()).isNotNull();
|
||||
SslOptions options = bundle.getOptions();
|
||||
assertThat(options.getCiphers()).containsExactly("ONE", "TWO", "THREE");
|
||||
assertThat(options.getEnabledProtocols()).containsExactly("TLSv1.1", "TLSv1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMissingPropertiesThrowsException() {
|
||||
Ssl ssl = new Ssl();
|
||||
assertThatIllegalStateException().isThrownBy(() -> WebServerSslBundle.get(ssl))
|
||||
.withMessageContaining("SSL is enabled but no trust material is configured");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.context;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.diagnostics.FailureAnalysis;
|
||||
import org.springframework.boot.web.server.reactive.ReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.reactive.context.ReactiveWebServerApplicationContext;
|
||||
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext;
|
||||
import org.springframework.context.ApplicationContextException;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link MissingWebServerFactoryBeanFailureAnalyzer}.
|
||||
*
|
||||
* @author Guirong Hu
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class MissingWebServerFactoryBeanFailureAnalyzerTests {
|
||||
|
||||
@Test
|
||||
void missingServletWebServerFactoryBeanFailure() {
|
||||
ApplicationContextException failure = createFailure(new ServletWebServerApplicationContext());
|
||||
assertThat(failure).isNotNull();
|
||||
FailureAnalysis analysis = new MissingWebServerFactoryBeanFailureAnalyzer().analyze(failure);
|
||||
assertThat(analysis).isNotNull();
|
||||
assertThat(analysis.getDescription()).isEqualTo("Web application could not be started as there was no "
|
||||
+ ServletWebServerFactory.class.getName() + " bean defined in the context.");
|
||||
assertThat(analysis.getAction()).isEqualTo(
|
||||
"Check your application's dependencies for a supported servlet web server.\nCheck the configured web "
|
||||
+ "application type.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingReactiveWebServerFactoryBeanFailure() {
|
||||
ApplicationContextException failure = createFailure(new ReactiveWebServerApplicationContext());
|
||||
FailureAnalysis analysis = new MissingWebServerFactoryBeanFailureAnalyzer().analyze(failure);
|
||||
assertThat(analysis).isNotNull();
|
||||
assertThat(analysis.getDescription()).isEqualTo("Web application could not be started as there was no "
|
||||
+ ReactiveWebServerFactory.class.getName() + " bean defined in the context.");
|
||||
assertThat(analysis.getAction()).isEqualTo(
|
||||
"Check your application's dependencies for a supported reactive web server.\nCheck the configured web "
|
||||
+ "application type.");
|
||||
}
|
||||
|
||||
private ApplicationContextException createFailure(ConfigurableApplicationContext context) {
|
||||
try {
|
||||
context.refresh();
|
||||
context.close();
|
||||
return null;
|
||||
}
|
||||
catch (ApplicationContextException ex) {
|
||||
return ex;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.context;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebServerApplicationContext}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class WebServerApplicationContextTests {
|
||||
|
||||
@Test
|
||||
void hasServerNamespaceWhenContextIsNotWebServerApplicationContextReturnsFalse() {
|
||||
ApplicationContext context = mock(ApplicationContext.class);
|
||||
assertThat(WebServerApplicationContext.hasServerNamespace(context, "test")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasServerNamespaceWhenContextIsWebServerApplicationContextAndNamespaceDoesNotMatchReturnsFalse() {
|
||||
ApplicationContext context = mock(WebServerApplicationContext.class);
|
||||
assertThat(WebServerApplicationContext.hasServerNamespace(context, "test")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasServerNamespaceWhenContextIsWebServerApplicationContextAndNamespaceMatchesReturnsTrue() {
|
||||
WebServerApplicationContext context = mock(WebServerApplicationContext.class);
|
||||
given(context.getServerNamespace()).willReturn("test");
|
||||
assertThat(WebServerApplicationContext.hasServerNamespace(context, "test")).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.context;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.contentOf;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests {@link WebServerPortFileWriter}.
|
||||
*
|
||||
* @author David Liu
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class WebServerPortFileWriterTests {
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
@BeforeEach
|
||||
@AfterEach
|
||||
void reset() {
|
||||
System.clearProperty("PORTFILE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPortFile() {
|
||||
File file = new File(this.tempDir, "port.file");
|
||||
WebServerPortFileWriter listener = new WebServerPortFileWriter(file);
|
||||
listener.onApplicationEvent(mockEvent("", 8080));
|
||||
assertThat(contentOf(file)).isEqualTo("8080");
|
||||
}
|
||||
|
||||
@Test
|
||||
void overridePortFileWithDefault() {
|
||||
System.setProperty("PORTFILE", new File(this.tempDir, "port.file").getAbsolutePath());
|
||||
WebServerPortFileWriter listener = new WebServerPortFileWriter();
|
||||
listener.onApplicationEvent(mockEvent("", 8080));
|
||||
String content = contentOf(new File(System.getProperty("PORTFILE")));
|
||||
assertThat(content).isEqualTo("8080");
|
||||
}
|
||||
|
||||
@Test
|
||||
void overridePortFileWithExplicitFile() {
|
||||
File file = new File(this.tempDir, "port.file");
|
||||
System.setProperty("PORTFILE", new File(this.tempDir, "override.file").getAbsolutePath());
|
||||
WebServerPortFileWriter listener = new WebServerPortFileWriter(file);
|
||||
listener.onApplicationEvent(mockEvent("", 8080));
|
||||
String content = contentOf(new File(System.getProperty("PORTFILE")));
|
||||
assertThat(content).isEqualTo("8080");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createManagementPortFile() {
|
||||
File file = new File(this.tempDir, "port.file");
|
||||
WebServerPortFileWriter listener = new WebServerPortFileWriter(file);
|
||||
listener.onApplicationEvent(mockEvent("", 8080));
|
||||
listener.onApplicationEvent(mockEvent("management", 9090));
|
||||
assertThat(contentOf(file)).isEqualTo("8080");
|
||||
String managementFile = file.getName();
|
||||
managementFile = managementFile.substring(0,
|
||||
managementFile.length() - StringUtils.getFilenameExtension(managementFile).length() - 1);
|
||||
managementFile = managementFile + "-management." + StringUtils.getFilenameExtension(file.getName());
|
||||
String content = contentOf(new File(file.getParentFile(), managementFile));
|
||||
assertThat(content).isEqualTo("9090");
|
||||
assertThat(collectFileNames(file.getParentFile())).contains(managementFile);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createUpperCaseManagementPortFile() {
|
||||
File file = new File(this.tempDir, "port.file");
|
||||
file = new File(file.getParentFile(), file.getName().toUpperCase(Locale.ENGLISH));
|
||||
WebServerPortFileWriter listener = new WebServerPortFileWriter(file);
|
||||
listener.onApplicationEvent(mockEvent("management", 9090));
|
||||
String managementFile = file.getName();
|
||||
managementFile = managementFile.substring(0,
|
||||
managementFile.length() - StringUtils.getFilenameExtension(managementFile).length() - 1);
|
||||
managementFile = managementFile + "-MANAGEMENT." + StringUtils.getFilenameExtension(file.getName());
|
||||
String content = contentOf(new File(file.getParentFile(), managementFile));
|
||||
assertThat(content).isEqualTo("9090");
|
||||
assertThat(collectFileNames(file.getParentFile())).contains(managementFile);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPortFileWhenPortFileNameDoesNotHaveExtension() {
|
||||
File file = new File(this.tempDir, "portfile");
|
||||
WebServerPortFileWriter listener = new WebServerPortFileWriter(file);
|
||||
WebServerApplicationContext applicationContext = mock(WebServerApplicationContext.class);
|
||||
given(applicationContext.getServerNamespace()).willReturn("management");
|
||||
assertThat(listener.getPortFile(applicationContext).getName()).isEqualTo("portfile-management");
|
||||
}
|
||||
|
||||
private WebServerInitializedEvent mockEvent(String namespace, int port) {
|
||||
WebServer webServer = mock(WebServer.class);
|
||||
given(webServer.getPort()).willReturn(port);
|
||||
WebServerApplicationContext applicationContext = mock(WebServerApplicationContext.class);
|
||||
given(applicationContext.getServerNamespace()).willReturn(namespace);
|
||||
given(applicationContext.getWebServer()).willReturn(webServer);
|
||||
WebServerInitializedEvent event = mock(WebServerInitializedEvent.class);
|
||||
given(event.getApplicationContext()).willReturn(applicationContext);
|
||||
given(event.getWebServer()).willReturn(webServer);
|
||||
return event;
|
||||
}
|
||||
|
||||
private Set<String> collectFileNames(File directory) {
|
||||
Set<String> names = new HashSet<>();
|
||||
if (directory.isDirectory()) {
|
||||
for (File file : directory.listFiles()) {
|
||||
names.add(file.getName());
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.reactive.context;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.web.server.reactive.MockReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.reactive.ReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.reactive.context.WebServerManager.DelayedInitializationHttpHandler;
|
||||
import org.springframework.boot.web.server.reactive.context.config.ExampleReactiveWebServerApplicationConfiguration;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.ApplicationEventMulticaster;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.context.event.SimpleApplicationEventMulticaster;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link AnnotationConfigReactiveWebServerApplicationContext}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class AnnotationConfigReactiveWebServerApplicationContextTests {
|
||||
|
||||
private AnnotationConfigReactiveWebServerApplicationContext context;
|
||||
|
||||
@Test
|
||||
void createFromScan() {
|
||||
this.context = new AnnotationConfigReactiveWebServerApplicationContext(
|
||||
ExampleReactiveWebServerApplicationConfiguration.class.getPackage().getName());
|
||||
verifyContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromConfigClass() {
|
||||
this.context = new AnnotationConfigReactiveWebServerApplicationContext(
|
||||
ExampleReactiveWebServerApplicationConfiguration.class);
|
||||
verifyContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerAndRefresh() {
|
||||
this.context = new AnnotationConfigReactiveWebServerApplicationContext();
|
||||
this.context.register(ExampleReactiveWebServerApplicationConfiguration.class);
|
||||
this.context.refresh();
|
||||
verifyContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleRegistersAndRefresh() {
|
||||
this.context = new AnnotationConfigReactiveWebServerApplicationContext();
|
||||
this.context.register(WebServerConfiguration.class);
|
||||
this.context.register(HttpHandlerConfiguration.class);
|
||||
this.context.refresh();
|
||||
assertThat(this.context.getBeansOfType(WebServerConfiguration.class)).hasSize(1);
|
||||
assertThat(this.context.getBeansOfType(HttpHandlerConfiguration.class)).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scanAndRefresh() {
|
||||
this.context = new AnnotationConfigReactiveWebServerApplicationContext();
|
||||
this.context.scan(ExampleReactiveWebServerApplicationConfiguration.class.getPackage().getName());
|
||||
this.context.refresh();
|
||||
verifyContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpHandlerInitialization() {
|
||||
// gh-14666
|
||||
this.context = new AnnotationConfigReactiveWebServerApplicationContext(InitializationTestConfig.class);
|
||||
verifyContext();
|
||||
}
|
||||
|
||||
private void verifyContext() {
|
||||
MockReactiveWebServerFactory factory = this.context.getBean(MockReactiveWebServerFactory.class);
|
||||
HttpHandler expectedHandler = this.context.getBean(HttpHandler.class);
|
||||
HttpHandler actualHandler = factory.getWebServer().getHttpHandler();
|
||||
if (actualHandler instanceof DelayedInitializationHttpHandler delayedHandler) {
|
||||
actualHandler = delayedHandler.getHandler();
|
||||
}
|
||||
assertThat(actualHandler).isEqualTo(expectedHandler);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class WebServerConfiguration {
|
||||
|
||||
@Bean
|
||||
ReactiveWebServerFactory webServerFactory() {
|
||||
return new MockReactiveWebServerFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class HttpHandlerConfiguration {
|
||||
|
||||
@Bean
|
||||
HttpHandler httpHandler() {
|
||||
return mock(HttpHandler.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class InitializationTestConfig {
|
||||
|
||||
private static boolean addedListener;
|
||||
|
||||
@Bean
|
||||
ReactiveWebServerFactory webServerFactory() {
|
||||
return new MockReactiveWebServerFactory();
|
||||
}
|
||||
|
||||
@Bean
|
||||
HttpHandler httpHandler() {
|
||||
if (!addedListener) {
|
||||
throw new RuntimeException(
|
||||
"Handlers should be added after listeners, we're being initialized too early!");
|
||||
}
|
||||
return mock(HttpHandler.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
Listener listener() {
|
||||
return new Listener();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ApplicationEventMulticaster applicationEventMulticaster() {
|
||||
return new SimpleApplicationEventMulticaster() {
|
||||
|
||||
@Override
|
||||
public void addApplicationListenerBean(String listenerBeanName) {
|
||||
super.addApplicationListenerBean(listenerBeanName);
|
||||
if ("listener".equals(listenerBeanName)) {
|
||||
addedListener = true;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
static class Listener implements ApplicationListener<ContextRefreshedEvent> {
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.reactive.context;
|
||||
|
||||
import org.springframework.boot.AbstractApplicationEnvironmentTests;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
/**
|
||||
* Tests for {@link ApplicationReactiveWebEnvironment}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ApplicationReactiveWebEnvironmentTests extends AbstractApplicationEnvironmentTests {
|
||||
|
||||
@Override
|
||||
protected StandardEnvironment createEnvironment() {
|
||||
return new ApplicationReactiveWebEnvironment();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.reactive.context;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.boot.availability.AvailabilityChangeEvent;
|
||||
import org.springframework.boot.availability.ReadinessState;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.server.context.ServerPortInfoApplicationContextInitializer;
|
||||
import org.springframework.boot.web.server.reactive.MockReactiveWebServerFactory;
|
||||
import org.springframework.context.ApplicationContextException;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextClosedEvent;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReactiveWebServerApplicationContext}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class ReactiveWebServerApplicationContextTests {
|
||||
|
||||
private final ReactiveWebServerApplicationContext context = new ReactiveWebServerApplicationContext();
|
||||
|
||||
@AfterEach
|
||||
void cleanUp() {
|
||||
this.context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenThereIsNoWebServerFactoryBeanThenContextRefreshWillFail() {
|
||||
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(this.context::refresh)
|
||||
.havingRootCause()
|
||||
.withMessageContaining(
|
||||
"Unable to start ReactiveWebServerApplicationContext due to missing ReactiveWebServerFactory bean");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenThereIsNoHttpHandlerBeanThenContextRefreshWillFail() {
|
||||
addWebServerFactoryBean();
|
||||
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(this.context::refresh)
|
||||
.havingRootCause()
|
||||
.withMessageContaining("Unable to start ReactiveWebApplicationContext due to missing HttpHandler bean");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenThereAreMultipleWebServerFactoryBeansThenContextRefreshWillFail() {
|
||||
addWebServerFactoryBean();
|
||||
addWebServerFactoryBean("anotherWebServerFactory");
|
||||
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(this.context::refresh)
|
||||
.havingRootCause()
|
||||
.withMessageContaining(
|
||||
"Unable to start ReactiveWebApplicationContext due to multiple ReactiveWebServerFactory beans");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenThereAreMultipleHttpHandlerBeansThenContextRefreshWillFail() {
|
||||
addWebServerFactoryBean();
|
||||
addHttpHandlerBean("httpHandler1");
|
||||
addHttpHandlerBean("httpHandler2");
|
||||
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(this.context::refresh)
|
||||
.havingRootCause()
|
||||
.withMessageContaining("Unable to start ReactiveWebApplicationContext due to multiple HttpHandler beans");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenContextIsRefreshedThenReactiveWebServerInitializedEventIsPublished() {
|
||||
addWebServerFactoryBean();
|
||||
addHttpHandlerBean();
|
||||
TestApplicationListener listener = new TestApplicationListener();
|
||||
this.context.addApplicationListener(listener);
|
||||
this.context.refresh();
|
||||
List<ApplicationEvent> events = listener.receivedEvents();
|
||||
assertThat(events).hasSize(2)
|
||||
.extracting("class")
|
||||
.containsExactly(ReactiveWebServerInitializedEvent.class, ContextRefreshedEvent.class);
|
||||
ReactiveWebServerInitializedEvent initializedEvent = (ReactiveWebServerInitializedEvent) events.get(0);
|
||||
assertThat(initializedEvent.getSource().getPort()).isGreaterThanOrEqualTo(0);
|
||||
assertThat(initializedEvent.getApplicationContext()).isEqualTo(this.context);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenContextIsRefreshedThenLocalServerPortIsAvailableFromTheEnvironment() {
|
||||
addWebServerFactoryBean();
|
||||
addHttpHandlerBean();
|
||||
new ServerPortInfoApplicationContextInitializer().initialize(this.context);
|
||||
this.context.refresh();
|
||||
ConfigurableEnvironment environment = this.context.getEnvironment();
|
||||
assertThat(environment.containsProperty("local.server.port")).isTrue();
|
||||
assertThat(environment.getProperty("local.server.port")).isEqualTo("8080");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenContextRefreshFailedThenWebServerIsStoppedAndDestroyed() {
|
||||
addWebServerFactoryBean();
|
||||
addHttpHandlerBean();
|
||||
this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class));
|
||||
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh);
|
||||
WebServer webServer = this.context.getWebServer();
|
||||
then(webServer).should(times(2)).stop();
|
||||
then(webServer).should().destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenContextRefreshFailedThenWebServerStopFailedCatchStopException() {
|
||||
addWebServerFactoryBean();
|
||||
addHttpHandlerBean();
|
||||
this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class, () -> {
|
||||
willThrow(new RuntimeException("WebServer has failed to stop")).willCallRealMethod()
|
||||
.given(this.context.getWebServer())
|
||||
.stop();
|
||||
return new RefreshFailure();
|
||||
}));
|
||||
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh)
|
||||
.withStackTraceContaining("WebServer has failed to stop");
|
||||
WebServer webServer = this.context.getWebServer();
|
||||
then(webServer).should().stop();
|
||||
then(webServer).should(never()).destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenContextRefreshFailedThenWebServerIsStoppedAndDestroyFailedCatchDestroyException() {
|
||||
addWebServerFactoryBean();
|
||||
addHttpHandlerBean();
|
||||
this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class, () -> {
|
||||
willThrow(new RuntimeException("WebServer has failed to destroy")).willCallRealMethod()
|
||||
.given(this.context.getWebServer())
|
||||
.destroy();
|
||||
return new RefreshFailure();
|
||||
}));
|
||||
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh)
|
||||
.withStackTraceContaining("WebServer has failed to destroy");
|
||||
WebServer webServer = this.context.getWebServer();
|
||||
then(webServer).should().stop();
|
||||
then(webServer).should().destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenContextIsClosedThenWebServerIsStoppedAndDestroyed() {
|
||||
addWebServerFactoryBean();
|
||||
addHttpHandlerBean();
|
||||
this.context.refresh();
|
||||
MockReactiveWebServerFactory factory = this.context.getBean(MockReactiveWebServerFactory.class);
|
||||
this.context.close();
|
||||
then(factory.getWebServer()).should(times(2)).stop();
|
||||
then(factory.getWebServer()).should().destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void whenContextIsClosedThenApplicationAvailabilityChangesToRefusingTraffic() {
|
||||
addWebServerFactoryBean();
|
||||
addHttpHandlerBean();
|
||||
TestApplicationListener listener = new TestApplicationListener();
|
||||
this.context.refresh();
|
||||
this.context.addApplicationListener(listener);
|
||||
this.context.close();
|
||||
List<ApplicationEvent> events = listener.receivedEvents();
|
||||
assertThat(events).hasSize(2)
|
||||
.extracting("class")
|
||||
.contains(AvailabilityChangeEvent.class, ContextClosedEvent.class);
|
||||
assertThat(((AvailabilityChangeEvent<ReadinessState>) events.get(0)).getState())
|
||||
.isEqualTo(ReadinessState.REFUSING_TRAFFIC);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenContextIsNotActiveThenCloseDoesNotChangeTheApplicationAvailability() {
|
||||
addWebServerFactoryBean();
|
||||
addHttpHandlerBean();
|
||||
TestApplicationListener listener = new TestApplicationListener();
|
||||
this.context.addApplicationListener(listener);
|
||||
this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class));
|
||||
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh);
|
||||
this.context.close();
|
||||
assertThat(listener.receivedEvents()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenTheContextIsRefreshedThenASubsequentRefreshAttemptWillFail() {
|
||||
addWebServerFactoryBean();
|
||||
addHttpHandlerBean();
|
||||
this.context.refresh();
|
||||
assertThatIllegalStateException().isThrownBy(this.context::refresh)
|
||||
.withMessageContaining("multiple refresh attempts");
|
||||
}
|
||||
|
||||
private void addHttpHandlerBean() {
|
||||
addHttpHandlerBean("httpHandler");
|
||||
}
|
||||
|
||||
private void addHttpHandlerBean(String beanName) {
|
||||
this.context.registerBeanDefinition(beanName,
|
||||
new RootBeanDefinition(HttpHandler.class, () -> (request, response) -> null));
|
||||
}
|
||||
|
||||
private void addWebServerFactoryBean() {
|
||||
addWebServerFactoryBean("webServerFactory");
|
||||
}
|
||||
|
||||
private void addWebServerFactoryBean(String beanName) {
|
||||
this.context.registerBeanDefinition(beanName, new RootBeanDefinition(MockReactiveWebServerFactory.class));
|
||||
}
|
||||
|
||||
static class TestApplicationListener implements ApplicationListener<ApplicationEvent> {
|
||||
|
||||
private final Deque<ApplicationEvent> events = new ArrayDeque<>();
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationEvent event) {
|
||||
this.events.add(event);
|
||||
}
|
||||
|
||||
List<ApplicationEvent> receivedEvents() {
|
||||
List<ApplicationEvent> receivedEvents = new ArrayList<>();
|
||||
while (!this.events.isEmpty()) {
|
||||
receivedEvents.add(this.events.pollFirst());
|
||||
}
|
||||
return receivedEvents;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class RefreshFailure {
|
||||
|
||||
RefreshFailure() {
|
||||
throw new RuntimeException("Fail refresh");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.reactive.context.config;
|
||||
|
||||
import org.springframework.boot.web.server.reactive.MockReactiveWebServerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Example {@code @Configuration} for use with
|
||||
* {@code AnnotationConfigReactiveWebServerApplicationContextTests}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class ExampleReactiveWebServerApplicationConfiguration {
|
||||
|
||||
@Bean
|
||||
public MockReactiveWebServerFactory webServerFactory() {
|
||||
return new MockReactiveWebServerFactory();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HttpHandler httpHandler() {
|
||||
return mock(HttpHandler.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.web.server.Cookie.SameSite;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* Tests for {@link CookieSameSiteSupplier}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class CookieSameSiteSupplierTests {
|
||||
|
||||
@Test
|
||||
void whenHasNameWhenNameIsNullThrowsException() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasName((String) null))
|
||||
.withMessage("'name' must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameWhenNameIsEmptyThrowsException() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasName(""))
|
||||
.withMessage("'name' must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameWhenNameMatchesCallsGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThat(supplier.whenHasName("test").getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.LAX);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameWhenNameDoesNotMatchDoesNotCallGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called");
|
||||
assertThat(supplier.whenHasName("test").getSameSite(new Cookie("tset", "x"))).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasSuppliedNameWhenNameIsNullThrowsException() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasName((Supplier<String>) null))
|
||||
.withMessage("'nameSupplier' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasSuppliedNameWhenNameMatchesCallsGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThat(supplier.whenHasName(() -> "test").getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.LAX);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasSuppliedNameWhenNameDoesNotMatchDoesNotCallGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called");
|
||||
assertThat(supplier.whenHasName(() -> "test").getSameSite(new Cookie("tset", "x"))).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameMatchingRegexWhenRegexIsNullThrowsException() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasNameMatching((String) null))
|
||||
.withMessage("'regex' must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameMatchingRegexWhenRegexIsEmptyThrowsException() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasNameMatching(""))
|
||||
.withMessage("'regex' must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameMatchingRegexWhenNameMatchesCallsGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThat(supplier.whenHasNameMatching("te.*").getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.LAX);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameMatchingRegexWhenNameDoesNotMatchDoesNotCallGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called");
|
||||
assertThat(supplier.whenHasNameMatching("te.*").getSameSite(new Cookie("tset", "x"))).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameMatchingPatternWhenPatternIsNullThrowsException() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> supplier.whenHasNameMatching((Pattern) null))
|
||||
.withMessage("'pattern' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameMatchingPatternWhenNameMatchesCallsGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThat(supplier.whenHasNameMatching(Pattern.compile("te.*")).getSameSite(new Cookie("test", "x")))
|
||||
.isEqualTo(SameSite.LAX);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNameMatchingPatternWhenNameDoesNotMatchDoesNotCallGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called");
|
||||
assertThat(supplier.whenHasNameMatching(Pattern.compile("te.*")).getSameSite(new Cookie("tset", "x"))).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenWhenPredicateIsNullThrowsException() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> supplier.when(null))
|
||||
.withMessage("'predicate' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenWhenPredicateMatchesCallsGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> SameSite.LAX;
|
||||
assertThat(supplier.when((cookie) -> cookie.getName().equals("test")).getSameSite(new Cookie("test", "x")))
|
||||
.isEqualTo(SameSite.LAX);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenWhenPredicateDoesNotMatchDoesNotCallGetSameSite() {
|
||||
CookieSameSiteSupplier supplier = (cookie) -> fail("Supplier Called");
|
||||
assertThat(supplier.when((cookie) -> cookie.getName().equals("test")).getSameSite(new Cookie("tset", "x")))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofNoneSuppliesNone() {
|
||||
assertThat(CookieSameSiteSupplier.ofNone().getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofLaxSuppliesLax() {
|
||||
assertThat(CookieSameSiteSupplier.ofLax().getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.LAX);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofStrictSuppliesStrict() {
|
||||
assertThat(CookieSameSiteSupplier.ofStrict().getSameSite(new Cookie("test", "x"))).isEqualTo(SameSite.STRICT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> CookieSameSiteSupplier.of(null))
|
||||
.withMessage("'sameSite' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofSuppliesValue() {
|
||||
assertThat(CookieSameSiteSupplier.of(SameSite.STRICT).getSameSite(new Cookie("test", "x")))
|
||||
.isEqualTo(SameSite.STRICT);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.security.CodeSource;
|
||||
import java.security.cert.Certificate;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link DocumentRoot}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class DocumentRootTests {
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
private final DocumentRoot documentRoot = new DocumentRoot(LogFactory.getLog(getClass()));
|
||||
|
||||
@Test
|
||||
void explodedWarFileDocumentRootWhenRunningFromExplodedWar() throws Exception {
|
||||
File codeSourceFile = new File(this.tempDir, "test.war/WEB-INF/lib/spring-boot.jar");
|
||||
codeSourceFile.getParentFile().mkdirs();
|
||||
codeSourceFile.createNewFile();
|
||||
File directory = this.documentRoot.getExplodedWarFileDocumentRoot(codeSourceFile);
|
||||
assertThat(directory).isEqualTo(codeSourceFile.getParentFile().getParentFile().getParentFile());
|
||||
}
|
||||
|
||||
@Test
|
||||
void explodedWarFileDocumentRootWhenRunningFromPackagedWar() {
|
||||
File codeSourceFile = new File(this.tempDir, "test.war");
|
||||
File directory = this.documentRoot.getExplodedWarFileDocumentRoot(codeSourceFile);
|
||||
assertThat(directory).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void codeSourceArchivePath() throws Exception {
|
||||
CodeSource codeSource = new CodeSource(new URL("file", "", "/some/test/path/"), (Certificate[]) null);
|
||||
File codeSourceArchive = this.documentRoot.getCodeSourceArchive(codeSource);
|
||||
assertThat(codeSourceArchive).isEqualTo(new File("/some/test/path/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void codeSourceArchivePathContainingSpaces() throws Exception {
|
||||
CodeSource codeSource = new CodeSource(new URL("file", "", "/test/path/with%20space/"), (Certificate[]) null);
|
||||
File codeSourceArchive = this.documentRoot.getCodeSourceArchive(codeSource);
|
||||
assertThat(codeSourceArchive).isEqualTo(new File("/test/path/with space/"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.JarURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarOutputStream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link StaticResourceJars}.
|
||||
*
|
||||
* @author Rupert Madden-Abbott
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class StaticResourceJarsTests {
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
@Test
|
||||
void includeJarWithStaticResources() throws Exception {
|
||||
File jarFile = createResourcesJar("test-resources.jar");
|
||||
List<URL> staticResourceJarUrls = new StaticResourceJars().getUrlsFrom(jarFile.toURI().toURL());
|
||||
assertThat(staticResourceJarUrls).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void includeJarWithStaticResourcesWithUrlEncodedSpaces() throws Exception {
|
||||
File jarFile = createResourcesJar("test resources.jar");
|
||||
List<URL> staticResourceJarUrls = new StaticResourceJars().getUrlsFrom(jarFile.toURI().toURL());
|
||||
assertThat(staticResourceJarUrls).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void includeJarWithStaticResourcesWithPlusInItsPath() throws Exception {
|
||||
File jarFile = createResourcesJar("test + resources.jar");
|
||||
List<URL> staticResourceJarUrls = new StaticResourceJars().getUrlsFrom(jarFile.toURI().toURL());
|
||||
assertThat(staticResourceJarUrls).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void excludeJarWithoutStaticResources() throws Exception {
|
||||
File jarFile = createJar("dependency.jar");
|
||||
List<URL> staticResourceJarUrls = new StaticResourceJars().getUrlsFrom(jarFile.toURI().toURL());
|
||||
assertThat(staticResourceJarUrls).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void uncPathsAreTolerated() throws Exception {
|
||||
File jarFile = createResourcesJar("test-resources.jar");
|
||||
List<URL> staticResourceJarUrls = new StaticResourceJars().getUrlsFrom(jarFile.toURI().toURL(),
|
||||
new URL("file://unc.example.com/test.jar"));
|
||||
assertThat(staticResourceJarUrls).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoreWildcardUrls() throws Exception {
|
||||
File jarFile = createResourcesJar("test-resources.jar");
|
||||
URL folderUrl = jarFile.getParentFile().toURI().toURL();
|
||||
URL wildcardUrl = new URL(folderUrl + "*.jar");
|
||||
List<URL> staticResourceJarUrls = new StaticResourceJars().getUrlsFrom(wildcardUrl);
|
||||
assertThat(staticResourceJarUrls).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotCloseJarFromCachedConnection() throws Exception {
|
||||
File jarFile = createResourcesJar("test-resources.jar");
|
||||
TrackedURLStreamHandler handler = new TrackedURLStreamHandler(true);
|
||||
URL url = new URL("jar", null, 0, jarFile.toURI().toURL() + "!/", handler);
|
||||
try {
|
||||
new StaticResourceJars().getUrlsFrom(url);
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> ((JarURLConnection) handler.getConnection()).getJarFile().getComment());
|
||||
}
|
||||
finally {
|
||||
((JarURLConnection) handler.getConnection()).getJarFile().close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void closesJarFromNonCachedConnection() throws Exception {
|
||||
File jarFile = createResourcesJar("test-resources.jar");
|
||||
TrackedURLStreamHandler handler = new TrackedURLStreamHandler(false);
|
||||
URL url = new URL("jar", null, 0, jarFile.toURI().toURL() + "!/", handler);
|
||||
new StaticResourceJars().getUrlsFrom(url);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> ((JarURLConnection) handler.getConnection()).getJarFile().getComment())
|
||||
.withMessageContaining("closed");
|
||||
}
|
||||
|
||||
private File createResourcesJar(String name) throws IOException {
|
||||
return createJar(name, (output) -> {
|
||||
JarEntry jarEntry = new JarEntry("META-INF/resources");
|
||||
try {
|
||||
output.putNextEntry(jarEntry);
|
||||
output.closeEntry();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private File createJar(String name) throws IOException {
|
||||
return createJar(name, null);
|
||||
}
|
||||
|
||||
private File createJar(String name, Consumer<JarOutputStream> customizer) throws IOException {
|
||||
File jarFile = new File(this.tempDir, name);
|
||||
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(jarFile));
|
||||
if (customizer != null) {
|
||||
customizer.accept(jarOutputStream);
|
||||
}
|
||||
jarOutputStream.close();
|
||||
return jarFile;
|
||||
}
|
||||
|
||||
private static class TrackedURLStreamHandler extends URLStreamHandler {
|
||||
|
||||
private final boolean useCaches;
|
||||
|
||||
private URLConnection connection;
|
||||
|
||||
TrackedURLStreamHandler(boolean useCaches) {
|
||||
this.useCaches = useCaches;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected URLConnection openConnection(URL u) throws IOException {
|
||||
this.connection = new URL(u.toExternalForm()).openConnection();
|
||||
this.connection.setUseCaches(this.useCaches);
|
||||
return this.connection;
|
||||
}
|
||||
|
||||
URLConnection getConnection() {
|
||||
return this.connection;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet.context;
|
||||
|
||||
import jakarta.servlet.GenericServlet;
|
||||
import jakarta.servlet.Servlet;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.web.server.servlet.MockServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.context.config.ExampleServletWebServerApplicationConfiguration;
|
||||
import org.springframework.boot.web.servlet.mock.MockServlet;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.context.annotation.ScopedProxyMode;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.ServletContextAware;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
|
||||
/**
|
||||
* Tests for {@link AnnotationConfigServletWebServerApplicationContext}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class AnnotationConfigServletWebServerApplicationContextTests {
|
||||
|
||||
private AnnotationConfigServletWebServerApplicationContext context;
|
||||
|
||||
@AfterEach
|
||||
void close() {
|
||||
if (this.context != null) {
|
||||
this.context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromScan() {
|
||||
this.context = new AnnotationConfigServletWebServerApplicationContext(
|
||||
ExampleServletWebServerApplicationConfiguration.class.getPackage().getName());
|
||||
verifyContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionScopeAvailable() {
|
||||
this.context = new AnnotationConfigServletWebServerApplicationContext(
|
||||
ExampleServletWebServerApplicationConfiguration.class, SessionScopedComponent.class);
|
||||
verifyContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionScopeAvailableToServlet() {
|
||||
this.context = new AnnotationConfigServletWebServerApplicationContext(
|
||||
ExampleServletWebServerApplicationConfiguration.class, ExampleServletWithAutowired.class,
|
||||
SessionScopedComponent.class);
|
||||
Servlet servlet = this.context.getBean(ExampleServletWithAutowired.class);
|
||||
assertThat(servlet).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromConfigClass() {
|
||||
this.context = new AnnotationConfigServletWebServerApplicationContext(
|
||||
ExampleServletWebServerApplicationConfiguration.class);
|
||||
verifyContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerAndRefresh() {
|
||||
this.context = new AnnotationConfigServletWebServerApplicationContext();
|
||||
this.context.register(ExampleServletWebServerApplicationConfiguration.class);
|
||||
this.context.refresh();
|
||||
verifyContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleRegistersAndRefresh() {
|
||||
this.context = new AnnotationConfigServletWebServerApplicationContext();
|
||||
this.context.register(WebServerConfiguration.class);
|
||||
this.context.register(ServletContextAwareConfiguration.class);
|
||||
this.context.refresh();
|
||||
assertThat(this.context.getBeansOfType(Servlet.class)).hasSize(1);
|
||||
assertThat(this.context.getBeansOfType(ServletWebServerFactory.class)).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scanAndRefresh() {
|
||||
this.context = new AnnotationConfigServletWebServerApplicationContext();
|
||||
this.context.scan(ExampleServletWebServerApplicationConfiguration.class.getPackage().getName());
|
||||
this.context.refresh();
|
||||
verifyContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createAndInitializeCyclic() {
|
||||
this.context = new AnnotationConfigServletWebServerApplicationContext(
|
||||
ServletContextAwareEmbeddedConfiguration.class);
|
||||
verifyContext();
|
||||
// You can't initialize the application context and inject the servlet context
|
||||
// because of a cycle - we'd like this to be not null, but it never will be
|
||||
assertThat(this.context.getBean(ServletContextAwareEmbeddedConfiguration.class).getServletContext()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createAndInitializeWithParent() {
|
||||
AnnotationConfigServletWebServerApplicationContext parent = new AnnotationConfigServletWebServerApplicationContext(
|
||||
WebServerConfiguration.class);
|
||||
this.context = new AnnotationConfigServletWebServerApplicationContext();
|
||||
this.context.register(WebServerConfiguration.class, ServletContextAwareConfiguration.class);
|
||||
this.context.setParent(parent);
|
||||
this.context.refresh();
|
||||
verifyContext();
|
||||
assertThat(this.context.getBean(ServletContextAwareConfiguration.class).getServletContext()).isNotNull();
|
||||
}
|
||||
|
||||
private void verifyContext() {
|
||||
MockServletWebServerFactory factory = this.context.getBean(MockServletWebServerFactory.class);
|
||||
Servlet servlet = this.context.getBean(Servlet.class);
|
||||
then(factory.getServletContext()).should().addServlet("servlet", servlet);
|
||||
}
|
||||
|
||||
@Component
|
||||
static class ExampleServletWithAutowired extends GenericServlet {
|
||||
|
||||
@Autowired
|
||||
private SessionScopedComponent component;
|
||||
|
||||
@Override
|
||||
public void service(ServletRequest req, ServletResponse res) {
|
||||
assertThat(this.component).isNotNull();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Component
|
||||
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
|
||||
static class SessionScopedComponent {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebMvc
|
||||
static class ServletContextAwareEmbeddedConfiguration implements ServletContextAware {
|
||||
|
||||
private ServletContext servletContext;
|
||||
|
||||
@Bean
|
||||
ServletWebServerFactory webServerFactory() {
|
||||
return new MockServletWebServerFactory();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Servlet servlet() {
|
||||
return new MockServlet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setServletContext(ServletContext servletContext) {
|
||||
this.servletContext = servletContext;
|
||||
}
|
||||
|
||||
ServletContext getServletContext() {
|
||||
return this.servletContext;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class WebServerConfiguration {
|
||||
|
||||
@Bean
|
||||
ServletWebServerFactory webServerFactory() {
|
||||
return new MockServletWebServerFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebMvc
|
||||
static class ServletContextAwareConfiguration implements ServletContextAware {
|
||||
|
||||
private ServletContext servletContext;
|
||||
|
||||
@Bean
|
||||
Servlet servlet() {
|
||||
return new MockServlet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setServletContext(ServletContext servletContext) {
|
||||
this.servletContext = servletContext;
|
||||
}
|
||||
|
||||
ServletContext getServletContext() {
|
||||
return this.servletContext;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet.context;
|
||||
|
||||
import org.springframework.boot.AbstractApplicationEnvironmentTests;
|
||||
import org.springframework.boot.web.context.servlet.ApplicationServletEnvironment;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
/**
|
||||
* Tests for {@link ApplicationServletEnvironment}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ApplicationServletEnvironmentTests extends AbstractApplicationEnvironmentTests {
|
||||
|
||||
@Override
|
||||
protected StandardEnvironment createEnvironment() {
|
||||
return new ApplicationServletEnvironment();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet.context;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import jakarta.servlet.MultipartConfigElement;
|
||||
import jakarta.servlet.annotation.WebFilter;
|
||||
import jakarta.servlet.annotation.WebListener;
|
||||
import jakarta.servlet.annotation.WebServlet;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.testsupport.classpath.ForkedClassPath;
|
||||
import org.springframework.boot.web.context.servlet.AnnotationConfigServletWebApplicationContext;
|
||||
import org.springframework.boot.web.server.servlet.WebListenerRegistrar;
|
||||
import org.springframework.boot.web.server.servlet.WebListenerRegistry;
|
||||
import org.springframework.boot.web.server.servlet.context.testcomponents.filter.TestFilter;
|
||||
import org.springframework.boot.web.server.servlet.context.testcomponents.listener.TestListener;
|
||||
import org.springframework.boot.web.server.servlet.context.testcomponents.servlet.TestMultipartServlet;
|
||||
import org.springframework.boot.web.server.servlet.context.testcomponents.servlet.TestServlet;
|
||||
import org.springframework.boot.web.servlet.RegistrationBean;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link ServletComponentScan @ServletComponentScan} with a mock
|
||||
* web environment.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class MockWebEnvironmentServletComponentScanIntegrationTests {
|
||||
|
||||
private AnnotationConfigServletWebApplicationContext context;
|
||||
|
||||
@TempDir
|
||||
File temp;
|
||||
|
||||
@AfterEach
|
||||
void cleanUp() {
|
||||
if (this.context != null) {
|
||||
this.context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ForkedClassPath
|
||||
void componentsAreRegistered() {
|
||||
prepareContext();
|
||||
this.context.refresh();
|
||||
Map<String, RegistrationBean> registrationBeans = this.context.getBeansOfType(RegistrationBean.class);
|
||||
assertThat(registrationBeans).hasSize(3);
|
||||
assertThat(registrationBeans.keySet()).containsExactlyInAnyOrder(TestServlet.class.getName(),
|
||||
TestFilter.class.getName(), TestMultipartServlet.class.getName());
|
||||
WebListenerRegistry registry = mock(WebListenerRegistry.class);
|
||||
this.context.getBean(WebListenerRegistrar.class).register(registry);
|
||||
then(registry).should().addWebListeners(TestListener.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ForkedClassPath
|
||||
void indexedComponentsAreRegistered() throws IOException {
|
||||
writeIndex(this.temp);
|
||||
prepareContext();
|
||||
try (URLClassLoader classLoader = new URLClassLoader(new URL[] { this.temp.toURI().toURL() },
|
||||
getClass().getClassLoader())) {
|
||||
this.context.setClassLoader(classLoader);
|
||||
this.context.refresh();
|
||||
Map<String, RegistrationBean> registrationBeans = this.context.getBeansOfType(RegistrationBean.class);
|
||||
assertThat(registrationBeans).hasSize(2);
|
||||
assertThat(registrationBeans.keySet()).containsExactlyInAnyOrder(TestServlet.class.getName(),
|
||||
TestFilter.class.getName());
|
||||
WebListenerRegistry registry = mock(WebListenerRegistry.class);
|
||||
this.context.getBean(WebListenerRegistrar.class).register(registry);
|
||||
then(registry).should().addWebListeners(TestListener.class.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ForkedClassPath
|
||||
void multipartConfigIsHonoured() {
|
||||
prepareContext();
|
||||
this.context.refresh();
|
||||
@SuppressWarnings("rawtypes")
|
||||
Map<String, ServletRegistrationBean> beans = this.context.getBeansOfType(ServletRegistrationBean.class);
|
||||
ServletRegistrationBean<?> servletRegistrationBean = beans.get(TestMultipartServlet.class.getName());
|
||||
assertThat(servletRegistrationBean).isNotNull();
|
||||
MultipartConfigElement multipartConfig = servletRegistrationBean.getMultipartConfig();
|
||||
assertThat(multipartConfig).isNotNull();
|
||||
assertThat(multipartConfig.getLocation()).isEqualTo("test");
|
||||
assertThat(multipartConfig.getMaxRequestSize()).isEqualTo(2048);
|
||||
assertThat(multipartConfig.getMaxFileSize()).isEqualTo(1024);
|
||||
assertThat(multipartConfig.getFileSizeThreshold()).isEqualTo(512);
|
||||
}
|
||||
|
||||
private void writeIndex(File temp) throws IOException {
|
||||
File metaInf = new File(temp, "META-INF");
|
||||
metaInf.mkdirs();
|
||||
Properties index = new Properties();
|
||||
index.setProperty(TestFilter.class.getName(), WebFilter.class.getName());
|
||||
index.setProperty(TestListener.class.getName(), WebListener.class.getName());
|
||||
index.setProperty(TestServlet.class.getName(), WebServlet.class.getName());
|
||||
try (FileWriter writer = new FileWriter(new File(metaInf, "spring.components"))) {
|
||||
index.store(writer, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareContext() {
|
||||
this.context = new AnnotationConfigServletWebApplicationContext();
|
||||
this.context.register(ScanningConfiguration.class);
|
||||
this.context.setServletContext(new MockServletContext());
|
||||
}
|
||||
|
||||
@ServletComponentScan(basePackages = "org.springframework.boot.web.server.servlet.context.testcomponents")
|
||||
static class ScanningConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet.context;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import jakarta.servlet.MultipartConfigElement;
|
||||
import jakarta.servlet.annotation.WebFilter;
|
||||
import jakarta.servlet.annotation.WebListener;
|
||||
import jakarta.servlet.annotation.WebServlet;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.MockServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.WebListenerRegistrar;
|
||||
import org.springframework.boot.web.server.servlet.context.testcomponents.filter.TestFilter;
|
||||
import org.springframework.boot.web.server.servlet.context.testcomponents.listener.TestListener;
|
||||
import org.springframework.boot.web.server.servlet.context.testcomponents.servlet.TestMultipartServlet;
|
||||
import org.springframework.boot.web.server.servlet.context.testcomponents.servlet.TestServlet;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link ServletComponentScan @ServletComponentScan}
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class ServletComponentScanIntegrationTests {
|
||||
|
||||
private AnnotationConfigServletWebServerApplicationContext context;
|
||||
|
||||
@TempDir
|
||||
File temp;
|
||||
|
||||
@AfterEach
|
||||
void cleanUp() {
|
||||
if (this.context != null) {
|
||||
this.context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void componentsAreRegistered() {
|
||||
this.context = new AnnotationConfigServletWebServerApplicationContext();
|
||||
this.context.register(TestConfiguration.class);
|
||||
this.context.refresh();
|
||||
assertThat(this.context.getServletContext().getFilterRegistrations()).hasSize(1)
|
||||
.containsKey(TestFilter.class.getName());
|
||||
assertThat(this.context.getServletContext().getServletRegistrations()).hasSize(2)
|
||||
.containsKeys(TestServlet.class.getName(), TestMultipartServlet.class.getName());
|
||||
assertThat(this.context.getBean(MockServletWebServerFactory.class).getSettings().getWebListenerClassNames())
|
||||
.containsExactly(TestListener.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void indexedComponentsAreRegistered() throws IOException {
|
||||
writeIndex(this.temp);
|
||||
this.context = new AnnotationConfigServletWebServerApplicationContext();
|
||||
try (URLClassLoader classLoader = new URLClassLoader(new URL[] { this.temp.toURI().toURL() },
|
||||
getClass().getClassLoader())) {
|
||||
this.context.setClassLoader(classLoader);
|
||||
this.context.register(TestConfiguration.class);
|
||||
this.context.refresh();
|
||||
assertThat(this.context.getServletContext().getFilterRegistrations()).hasSize(1)
|
||||
.containsKey(TestFilter.class.getName());
|
||||
assertThat(this.context.getServletContext().getServletRegistrations()).hasSize(1)
|
||||
.containsKeys(TestServlet.class.getName());
|
||||
assertThat(this.context.getBean(MockServletWebServerFactory.class).getSettings().getWebListenerClassNames())
|
||||
.containsExactly(TestListener.class.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipartConfigIsHonoured() {
|
||||
this.context = new AnnotationConfigServletWebServerApplicationContext();
|
||||
this.context.register(TestConfiguration.class);
|
||||
this.context.refresh();
|
||||
@SuppressWarnings("rawtypes")
|
||||
Map<String, ServletRegistrationBean> beans = this.context.getBeansOfType(ServletRegistrationBean.class);
|
||||
ServletRegistrationBean<?> servletRegistrationBean = beans.get(TestMultipartServlet.class.getName());
|
||||
assertThat(servletRegistrationBean).isNotNull();
|
||||
MultipartConfigElement multipartConfig = servletRegistrationBean.getMultipartConfig();
|
||||
assertThat(multipartConfig).isNotNull();
|
||||
assertThat(multipartConfig.getLocation()).isEqualTo("test");
|
||||
assertThat(multipartConfig.getMaxRequestSize()).isEqualTo(2048);
|
||||
assertThat(multipartConfig.getMaxFileSize()).isEqualTo(1024);
|
||||
assertThat(multipartConfig.getFileSizeThreshold()).isEqualTo(512);
|
||||
}
|
||||
|
||||
private void writeIndex(File temp) throws IOException {
|
||||
File metaInf = new File(temp, "META-INF");
|
||||
metaInf.mkdirs();
|
||||
Properties index = new Properties();
|
||||
index.setProperty(TestFilter.class.getName(), WebFilter.class.getName());
|
||||
index.setProperty(TestListener.class.getName(), WebListener.class.getName());
|
||||
index.setProperty(TestServlet.class.getName(), WebServlet.class.getName());
|
||||
try (FileWriter writer = new FileWriter(new File(metaInf, "spring.components"))) {
|
||||
index.store(writer, null);
|
||||
}
|
||||
}
|
||||
|
||||
@ServletComponentScan(basePackages = "org.springframework.boot.web.server.servlet.context.testcomponents")
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
protected ServletWebServerFactory webServerFactory(ObjectProvider<WebListenerRegistrar> webListenerRegistrars) {
|
||||
ConfigurableServletWebServerFactory factory = new MockServletWebServerFactory();
|
||||
webListenerRegistrars.orderedStream().forEach((registrar) -> registrar.register(factory));
|
||||
return factory;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet.context;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
|
||||
import org.springframework.aot.test.generate.TestGenerationContext;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.boot.web.server.servlet.context.testcomponents.listener.TestListener;
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.aot.ApplicationContextAotGenerator;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotationConfigurationException;
|
||||
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
|
||||
import org.springframework.core.test.tools.TestCompiler;
|
||||
import org.springframework.javapoet.ClassName;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link ServletComponentScanRegistrar}
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class ServletComponentScanRegistrarTests {
|
||||
|
||||
private AnnotationConfigApplicationContext context;
|
||||
|
||||
@AfterEach
|
||||
void after() {
|
||||
if (this.context != null) {
|
||||
this.context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void packagesConfiguredWithValue() {
|
||||
this.context = new AnnotationConfigApplicationContext(ValuePackages.class);
|
||||
ServletComponentRegisteringPostProcessor postProcessor = this.context
|
||||
.getBean(ServletComponentRegisteringPostProcessor.class);
|
||||
assertThat(postProcessor.getPackagesToScan()).contains("com.example.foo", "com.example.bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void packagesConfiguredWithValueAsm() {
|
||||
this.context = new AnnotationConfigApplicationContext();
|
||||
this.context.registerBeanDefinition("valuePackages", new RootBeanDefinition(ValuePackages.class.getName()));
|
||||
this.context.refresh();
|
||||
ServletComponentRegisteringPostProcessor postProcessor = this.context
|
||||
.getBean(ServletComponentRegisteringPostProcessor.class);
|
||||
assertThat(postProcessor.getPackagesToScan()).contains("com.example.foo", "com.example.bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void packagesConfiguredWithBackPackages() {
|
||||
this.context = new AnnotationConfigApplicationContext(BasePackages.class);
|
||||
ServletComponentRegisteringPostProcessor postProcessor = this.context
|
||||
.getBean(ServletComponentRegisteringPostProcessor.class);
|
||||
assertThat(postProcessor.getPackagesToScan()).contains("com.example.foo", "com.example.bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void packagesConfiguredWithBasePackageClasses() {
|
||||
this.context = new AnnotationConfigApplicationContext(BasePackageClasses.class);
|
||||
ServletComponentRegisteringPostProcessor postProcessor = this.context
|
||||
.getBean(ServletComponentRegisteringPostProcessor.class);
|
||||
assertThat(postProcessor.getPackagesToScan()).contains(getClass().getPackage().getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void packagesConfiguredWithBothValueAndBasePackages() {
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class)
|
||||
.isThrownBy(() -> this.context = new AnnotationConfigApplicationContext(ValueAndBasePackages.class))
|
||||
.withMessageContaining("'value'")
|
||||
.withMessageContaining("'basePackages'")
|
||||
.withMessageContaining("com.example.foo")
|
||||
.withMessageContaining("com.example.bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void packagesFromMultipleAnnotationsAreMerged() {
|
||||
this.context = new AnnotationConfigApplicationContext(BasePackages.class, AdditionalPackages.class);
|
||||
ServletComponentRegisteringPostProcessor postProcessor = this.context
|
||||
.getBean(ServletComponentRegisteringPostProcessor.class);
|
||||
assertThat(postProcessor.getPackagesToScan()).contains("com.example.foo", "com.example.bar", "com.example.baz");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withNoBasePackagesScanningUsesBasePackageOfAnnotatedClass() {
|
||||
this.context = new AnnotationConfigApplicationContext(NoBasePackages.class);
|
||||
ServletComponentRegisteringPostProcessor postProcessor = this.context
|
||||
.getBean(ServletComponentRegisteringPostProcessor.class);
|
||||
assertThat(postProcessor.getPackagesToScan())
|
||||
.containsExactly("org.springframework.boot.web.server.servlet.context");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noBasePackageAndBasePackageAreCombinedCorrectly() {
|
||||
this.context = new AnnotationConfigApplicationContext(NoBasePackages.class, BasePackages.class);
|
||||
ServletComponentRegisteringPostProcessor postProcessor = this.context
|
||||
.getBean(ServletComponentRegisteringPostProcessor.class);
|
||||
assertThat(postProcessor.getPackagesToScan()).containsExactlyInAnyOrder(
|
||||
"org.springframework.boot.web.server.servlet.context", "com.example.foo", "com.example.bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void basePackageAndNoBasePackageAreCombinedCorrectly() {
|
||||
this.context = new AnnotationConfigApplicationContext(BasePackages.class, NoBasePackages.class);
|
||||
ServletComponentRegisteringPostProcessor postProcessor = this.context
|
||||
.getBean(ServletComponentRegisteringPostProcessor.class);
|
||||
assertThat(postProcessor.getPackagesToScan()).containsExactlyInAnyOrder(
|
||||
"org.springframework.boot.web.server.servlet.context", "com.example.foo", "com.example.bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
@CompileWithForkedClassLoader
|
||||
void processAheadOfTimeDoesNotRegisterServletComponentRegisteringPostProcessor() {
|
||||
GenericApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
context.registerBean(BasePackages.class);
|
||||
compile(context, (freshContext) -> {
|
||||
freshContext.refresh();
|
||||
assertThat(freshContext.getBeansOfType(ServletComponentRegisteringPostProcessor.class)).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void processAheadOfTimeRegistersReflectionHintsForWebListeners() {
|
||||
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext();
|
||||
context.registerBean(ScanListenerPackage.class);
|
||||
TestGenerationContext generationContext = new TestGenerationContext(
|
||||
ClassName.get(getClass().getPackageName(), "TestTarget"));
|
||||
new ApplicationContextAotGenerator().processAheadOfTime(context, generationContext);
|
||||
assertThat(RuntimeHintsPredicates.reflection()
|
||||
.onType(TestListener.class)
|
||||
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS))
|
||||
.accepts(generationContext.getRuntimeHints());
|
||||
}
|
||||
|
||||
@Test
|
||||
void processAheadOfTimeSucceedsForWebServletWithMultipartConfig() {
|
||||
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext();
|
||||
context.registerBean(ScanServletPackage.class);
|
||||
TestGenerationContext generationContext = new TestGenerationContext(
|
||||
ClassName.get(getClass().getPackageName(), "TestTarget"));
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> new ApplicationContextAotGenerator().processAheadOfTime(context, generationContext));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void compile(GenericApplicationContext context, Consumer<GenericApplicationContext> freshContext) {
|
||||
TestGenerationContext generationContext = new TestGenerationContext(
|
||||
ClassName.get(getClass().getPackageName(), "TestTarget"));
|
||||
ClassName className = new ApplicationContextAotGenerator().processAheadOfTime(context, generationContext);
|
||||
generationContext.writeGeneratedContent();
|
||||
TestCompiler.forSystem().with(generationContext).compile((compiled) -> {
|
||||
GenericApplicationContext freshApplicationContext = new GenericApplicationContext();
|
||||
ApplicationContextInitializer<GenericApplicationContext> initializer = compiled
|
||||
.getInstance(ApplicationContextInitializer.class, className.toString());
|
||||
initializer.initialize(freshApplicationContext);
|
||||
freshContext.accept(freshApplicationContext);
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ServletComponentScan({ "com.example.foo", "com.example.bar" })
|
||||
static class ValuePackages {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ServletComponentScan(basePackages = { "com.example.foo", "com.example.bar" })
|
||||
static class BasePackages {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ServletComponentScan(basePackages = "com.example.baz")
|
||||
static class AdditionalPackages {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ServletComponentScan(basePackageClasses = ServletComponentScanRegistrarTests.class)
|
||||
static class BasePackageClasses {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ServletComponentScan(value = "com.example.foo", basePackages = "com.example.bar")
|
||||
static class ValueAndBasePackages {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ServletComponentScan
|
||||
static class NoBasePackages {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ServletComponentScan("org.springframework.boot.web.server.servlet.context.testcomponents.listener")
|
||||
static class ScanListenerPackage {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ServletComponentScan("org.springframework.boot.web.server.servlet.context.testcomponents.servlet")
|
||||
static class ScanServletPackage {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet.context;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.Servlet;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.ServletContextListener;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.config.ConstructorArgumentValues;
|
||||
import org.springframework.beans.factory.config.Scope;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.boot.availability.AvailabilityChangeEvent;
|
||||
import org.springframework.boot.testsupport.system.CapturedOutput;
|
||||
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.server.context.ServerPortInfoApplicationContextInitializer;
|
||||
import org.springframework.boot.web.server.servlet.MockServletWebServerFactory;
|
||||
import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.boot.web.servlet.ServletContextInitializer;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.context.ApplicationContextException;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextClosedEvent;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockFilterConfig;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.web.context.ServletContextAware;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.context.request.SessionScope;
|
||||
import org.springframework.web.filter.GenericFilterBean;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.atMost;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
|
||||
/**
|
||||
* Tests for {@link ServletWebServerApplicationContext}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
@ExtendWith({ OutputCaptureExtension.class, MockitoExtension.class })
|
||||
class ServletWebServerApplicationContextTests {
|
||||
|
||||
private final ServletWebServerApplicationContext context = new ServletWebServerApplicationContext();
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Filter> filterCaptor;
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
this.context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void startRegistrations() {
|
||||
addWebServerFactoryBean();
|
||||
this.context.refresh();
|
||||
MockServletWebServerFactory factory = getWebServerFactory();
|
||||
// Ensure that the context has been set up
|
||||
assertThat(this.context.getServletContext()).isEqualTo(factory.getServletContext());
|
||||
then(factory.getServletContext()).should()
|
||||
.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
|
||||
// Ensure WebApplicationContextUtils.registerWebApplicationScopes was called
|
||||
assertThat(this.context.getBeanFactory().getRegisteredScope(WebApplicationContext.SCOPE_SESSION))
|
||||
.isInstanceOf(SessionScope.class);
|
||||
// Ensure WebApplicationContextUtils.registerEnvironmentBeans was called
|
||||
assertThat(this.context.containsBean(WebApplicationContext.SERVLET_CONTEXT_BEAN_NAME)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotRegistersShutdownHook() {
|
||||
// See gh-314 for background. We no longer register the shutdown hook
|
||||
// since it is really the caller's responsibility. The shutdown hook could
|
||||
// also be problematic in a classic WAR deployment.
|
||||
addWebServerFactoryBean();
|
||||
this.context.refresh();
|
||||
assertThat(this.context).hasFieldOrPropertyWithValue("shutdownHook", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ServletWebServerInitializedEventPublished() {
|
||||
addWebServerFactoryBean();
|
||||
this.context.registerBeanDefinition("listener", new RootBeanDefinition(TestApplicationListener.class));
|
||||
this.context.refresh();
|
||||
List<ApplicationEvent> events = this.context.getBean(TestApplicationListener.class).receivedEvents();
|
||||
assertThat(events).hasSize(2)
|
||||
.extracting("class")
|
||||
.containsExactly(ServletWebServerInitializedEvent.class, ContextRefreshedEvent.class);
|
||||
ServletWebServerInitializedEvent initializedEvent = (ServletWebServerInitializedEvent) events.get(0);
|
||||
assertThat(initializedEvent.getSource().getPort()).isGreaterThanOrEqualTo(0);
|
||||
assertThat(initializedEvent.getApplicationContext()).isEqualTo(this.context);
|
||||
}
|
||||
|
||||
@Test
|
||||
void localPortIsAvailable() {
|
||||
addWebServerFactoryBean();
|
||||
new ServerPortInfoApplicationContextInitializer().initialize(this.context);
|
||||
this.context.refresh();
|
||||
ConfigurableEnvironment environment = this.context.getEnvironment();
|
||||
assertThat(environment.containsProperty("local.server.port")).isTrue();
|
||||
assertThat(environment.getProperty("local.server.port")).isEqualTo("8080");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stopOnStop() {
|
||||
addWebServerFactoryBean();
|
||||
this.context.refresh();
|
||||
MockServletWebServerFactory factory = getWebServerFactory();
|
||||
then(factory.getWebServer()).should().start();
|
||||
this.context.stop();
|
||||
then(factory.getWebServer()).should().stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
void startOnStartAfterStop() {
|
||||
addWebServerFactoryBean();
|
||||
this.context.refresh();
|
||||
MockServletWebServerFactory factory = getWebServerFactory();
|
||||
then(factory.getWebServer()).should().start();
|
||||
this.context.stop();
|
||||
then(factory.getWebServer()).should().stop();
|
||||
this.context.start();
|
||||
then(factory.getWebServer()).should(times(2)).start();
|
||||
}
|
||||
|
||||
@Test
|
||||
void stopAndDestroyOnClose() {
|
||||
addWebServerFactoryBean();
|
||||
this.context.refresh();
|
||||
MockServletWebServerFactory factory = getWebServerFactory();
|
||||
this.context.close();
|
||||
then(factory.getWebServer()).should(times(2)).stop();
|
||||
then(factory.getWebServer()).should().destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
void applicationIsUnreadyDuringShutdown() {
|
||||
TestApplicationListener listener = new TestApplicationListener();
|
||||
addWebServerFactoryBean();
|
||||
this.context.refresh();
|
||||
this.context.addApplicationListener(listener);
|
||||
this.context.close();
|
||||
assertThat(listener.receivedEvents()).hasSize(2)
|
||||
.extracting("class")
|
||||
.contains(AvailabilityChangeEvent.class, ContextClosedEvent.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenContextIsNotActiveThenCloseDoesNotChangeTheApplicationAvailability() {
|
||||
addWebServerFactoryBean();
|
||||
TestApplicationListener listener = new TestApplicationListener();
|
||||
this.context.addApplicationListener(listener);
|
||||
this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class));
|
||||
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh);
|
||||
this.context.close();
|
||||
assertThat(listener.receivedEvents()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenContextRefreshFailedThenWebServerIsStoppedAndDestroyed() {
|
||||
addWebServerFactoryBean();
|
||||
this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class));
|
||||
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh);
|
||||
WebServer webServer = this.context.getWebServer();
|
||||
then(webServer).should(times(2)).stop();
|
||||
then(webServer).should().destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenContextRefreshFailedThenWebServerStopFailedCatchStopException() {
|
||||
addWebServerFactoryBean();
|
||||
this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class, () -> {
|
||||
willThrow(new RuntimeException("WebServer has failed to stop")).willCallRealMethod()
|
||||
.given(this.context.getWebServer())
|
||||
.stop();
|
||||
return new RefreshFailure();
|
||||
}));
|
||||
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh)
|
||||
.withStackTraceContaining("WebServer has failed to stop");
|
||||
WebServer webServer = this.context.getWebServer();
|
||||
then(webServer).should().stop();
|
||||
then(webServer).should(never()).destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenContextRefreshFailedThenWebServerIsStoppedAndDestroyFailedCatchDestroyException() {
|
||||
addWebServerFactoryBean();
|
||||
this.context.registerBeanDefinition("refreshFailure", new RootBeanDefinition(RefreshFailure.class, () -> {
|
||||
willThrow(new RuntimeException("WebServer has failed to destroy")).willCallRealMethod()
|
||||
.given(this.context.getWebServer())
|
||||
.destroy();
|
||||
return new RefreshFailure();
|
||||
}));
|
||||
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh)
|
||||
.withStackTraceContaining("WebServer has failed to destroy");
|
||||
WebServer webServer = this.context.getWebServer();
|
||||
then(webServer).should().stop();
|
||||
then(webServer).should().destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
void cannotSecondRefresh() {
|
||||
addWebServerFactoryBean();
|
||||
this.context.refresh();
|
||||
assertThatIllegalStateException().isThrownBy(this.context::refresh);
|
||||
}
|
||||
|
||||
@Test
|
||||
void servletContextAwareBeansAreInjected() {
|
||||
addWebServerFactoryBean();
|
||||
ServletContextAware bean = mock(ServletContextAware.class);
|
||||
this.context.registerBeanDefinition("bean", beanDefinition(bean));
|
||||
this.context.refresh();
|
||||
then(bean).should().setServletContext(getWebServerFactory().getServletContext());
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingServletWebServerFactory() {
|
||||
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(this.context::refresh)
|
||||
.havingRootCause()
|
||||
.withMessageContaining("Unable to start ServletWebServerApplicationContext due to missing "
|
||||
+ "ServletWebServerFactory bean");
|
||||
}
|
||||
|
||||
@Test
|
||||
void tooManyWebServerFactories() {
|
||||
addWebServerFactoryBean();
|
||||
this.context.registerBeanDefinition("webServerFactory2",
|
||||
new RootBeanDefinition(MockServletWebServerFactory.class));
|
||||
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(this.context::refresh)
|
||||
.havingRootCause()
|
||||
.withMessageContaining("Unable to start ServletWebServerApplicationContext due to "
|
||||
+ "multiple ServletWebServerFactory beans");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleServletBean() {
|
||||
addWebServerFactoryBean();
|
||||
Servlet servlet = mock(Servlet.class);
|
||||
this.context.registerBeanDefinition("servletBean", beanDefinition(servlet));
|
||||
this.context.refresh();
|
||||
MockServletWebServerFactory factory = getWebServerFactory();
|
||||
then(factory.getServletContext()).should().addServlet("servletBean", servlet);
|
||||
then(factory.getRegisteredServlet(0).getRegistration()).should().addMapping("/");
|
||||
}
|
||||
|
||||
@Test
|
||||
void orderedBeanInsertedCorrectly() {
|
||||
addWebServerFactoryBean();
|
||||
OrderedFilter filter = new OrderedFilter();
|
||||
this.context.registerBeanDefinition("filterBean", beanDefinition(filter));
|
||||
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
|
||||
registration.setName("filterBeanRegistration");
|
||||
registration.setFilter(mock(Filter.class));
|
||||
registration.setOrder(100);
|
||||
this.context.registerBeanDefinition("filterRegistrationBean", beanDefinition(registration));
|
||||
this.context.refresh();
|
||||
MockServletWebServerFactory factory = getWebServerFactory();
|
||||
then(factory.getServletContext()).should().addFilter("filterBean", filter);
|
||||
then(factory.getServletContext()).should().addFilter("filterBeanRegistration", registration.getFilter());
|
||||
assertThat(factory.getRegisteredFilter(0).getFilter()).isEqualTo(filter);
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleServletBeans() {
|
||||
addWebServerFactoryBean();
|
||||
Servlet servlet1 = mock(Servlet.class, withSettings().extraInterfaces(Ordered.class));
|
||||
given(((Ordered) servlet1).getOrder()).willReturn(1);
|
||||
Servlet servlet2 = mock(Servlet.class, withSettings().extraInterfaces(Ordered.class));
|
||||
given(((Ordered) servlet2).getOrder()).willReturn(2);
|
||||
this.context.registerBeanDefinition("servletBean2", beanDefinition(servlet2));
|
||||
this.context.registerBeanDefinition("servletBean1", beanDefinition(servlet1));
|
||||
this.context.refresh();
|
||||
MockServletWebServerFactory factory = getWebServerFactory();
|
||||
ServletContext servletContext = factory.getServletContext();
|
||||
InOrder ordered = inOrder(servletContext);
|
||||
then(servletContext).should(ordered).addServlet("servletBean1", servlet1);
|
||||
then(servletContext).should(ordered).addServlet("servletBean2", servlet2);
|
||||
then(factory.getRegisteredServlet(0).getRegistration()).should().addMapping("/servletBean1/");
|
||||
then(factory.getRegisteredServlet(1).getRegistration()).should().addMapping("/servletBean2/");
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleServletBeansWithMainDispatcher() {
|
||||
addWebServerFactoryBean();
|
||||
Servlet servlet1 = mock(Servlet.class, withSettings().extraInterfaces(Ordered.class));
|
||||
given(((Ordered) servlet1).getOrder()).willReturn(1);
|
||||
Servlet servlet2 = mock(Servlet.class, withSettings().extraInterfaces(Ordered.class));
|
||||
given(((Ordered) servlet2).getOrder()).willReturn(2);
|
||||
this.context.registerBeanDefinition("servletBean2", beanDefinition(servlet2));
|
||||
this.context.registerBeanDefinition("dispatcherServlet", beanDefinition(servlet1));
|
||||
this.context.refresh();
|
||||
MockServletWebServerFactory factory = getWebServerFactory();
|
||||
ServletContext servletContext = factory.getServletContext();
|
||||
InOrder ordered = inOrder(servletContext);
|
||||
then(servletContext).should(ordered).addServlet("dispatcherServlet", servlet1);
|
||||
then(servletContext).should(ordered).addServlet("servletBean2", servlet2);
|
||||
then(factory.getRegisteredServlet(0).getRegistration()).should().addMapping("/");
|
||||
then(factory.getRegisteredServlet(1).getRegistration()).should().addMapping("/servletBean2/");
|
||||
}
|
||||
|
||||
@Test
|
||||
void servletAndFilterBeans() {
|
||||
addWebServerFactoryBean();
|
||||
Servlet servlet = mock(Servlet.class);
|
||||
Filter filter1 = mock(Filter.class, withSettings().extraInterfaces(Ordered.class));
|
||||
given(((Ordered) filter1).getOrder()).willReturn(1);
|
||||
Filter filter2 = mock(Filter.class, withSettings().extraInterfaces(Ordered.class));
|
||||
given(((Ordered) filter2).getOrder()).willReturn(2);
|
||||
this.context.registerBeanDefinition("servletBean", beanDefinition(servlet));
|
||||
this.context.registerBeanDefinition("filterBean2", beanDefinition(filter2));
|
||||
this.context.registerBeanDefinition("filterBean1", beanDefinition(filter1));
|
||||
this.context.refresh();
|
||||
MockServletWebServerFactory factory = getWebServerFactory();
|
||||
ServletContext servletContext = factory.getServletContext();
|
||||
InOrder ordered = inOrder(servletContext);
|
||||
then(factory.getServletContext()).should().addServlet("servletBean", servlet);
|
||||
then(factory.getRegisteredServlet(0).getRegistration()).should().addMapping("/");
|
||||
then(factory.getServletContext()).should(ordered).addFilter("filterBean1", filter1);
|
||||
then(factory.getServletContext()).should(ordered).addFilter("filterBean2", filter2);
|
||||
then(factory.getRegisteredFilter(0).getRegistration()).should()
|
||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
|
||||
then(factory.getRegisteredFilter(1).getRegistration()).should()
|
||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
|
||||
}
|
||||
|
||||
@Test
|
||||
void servletContextInitializerBeans() throws Exception {
|
||||
addWebServerFactoryBean();
|
||||
ServletContextInitializer initializer1 = mock(ServletContextInitializer.class,
|
||||
withSettings().extraInterfaces(Ordered.class));
|
||||
given(((Ordered) initializer1).getOrder()).willReturn(1);
|
||||
ServletContextInitializer initializer2 = mock(ServletContextInitializer.class,
|
||||
withSettings().extraInterfaces(Ordered.class));
|
||||
given(((Ordered) initializer2).getOrder()).willReturn(2);
|
||||
this.context.registerBeanDefinition("initializerBean2", beanDefinition(initializer2));
|
||||
this.context.registerBeanDefinition("initializerBean1", beanDefinition(initializer1));
|
||||
this.context.refresh();
|
||||
ServletContext servletContext = getWebServerFactory().getServletContext();
|
||||
InOrder ordered = inOrder(initializer1, initializer2);
|
||||
then(initializer1).should(ordered).onStartup(servletContext);
|
||||
then(initializer2).should(ordered).onStartup(servletContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
void servletContextListenerBeans() {
|
||||
addWebServerFactoryBean();
|
||||
ServletContextListener initializer = mock(ServletContextListener.class);
|
||||
this.context.registerBeanDefinition("initializerBean", beanDefinition(initializer));
|
||||
this.context.refresh();
|
||||
ServletContext servletContext = getWebServerFactory().getServletContext();
|
||||
then(servletContext).should().addListener(initializer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unorderedServletContextInitializerBeans() throws Exception {
|
||||
addWebServerFactoryBean();
|
||||
ServletContextInitializer initializer1 = mock(ServletContextInitializer.class);
|
||||
ServletContextInitializer initializer2 = mock(ServletContextInitializer.class);
|
||||
this.context.registerBeanDefinition("initializerBean2", beanDefinition(initializer2));
|
||||
this.context.registerBeanDefinition("initializerBean1", beanDefinition(initializer1));
|
||||
this.context.refresh();
|
||||
ServletContext servletContext = getWebServerFactory().getServletContext();
|
||||
then(initializer1).should().onStartup(servletContext);
|
||||
then(initializer2).should().onStartup(servletContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
void servletContextInitializerBeansDoesNotSkipServletsAndFilters() throws Exception {
|
||||
addWebServerFactoryBean();
|
||||
ServletContextInitializer initializer = mock(ServletContextInitializer.class);
|
||||
Servlet servlet = mock(Servlet.class);
|
||||
Filter filter = mock(Filter.class);
|
||||
this.context.registerBeanDefinition("initializerBean", beanDefinition(initializer));
|
||||
this.context.registerBeanDefinition("servletBean", beanDefinition(servlet));
|
||||
this.context.registerBeanDefinition("filterBean", beanDefinition(filter));
|
||||
this.context.refresh();
|
||||
ServletContext servletContext = getWebServerFactory().getServletContext();
|
||||
then(initializer).should().onStartup(servletContext);
|
||||
then(servletContext).should().addServlet(anyString(), any(Servlet.class));
|
||||
then(servletContext).should().addFilter(anyString(), any(Filter.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void servletContextInitializerBeansSkipsRegisteredServletsAndFilters() {
|
||||
addWebServerFactoryBean();
|
||||
Servlet servlet = mock(Servlet.class);
|
||||
Filter filter = mock(Filter.class);
|
||||
ServletRegistrationBean<Servlet> initializer = new ServletRegistrationBean<>(servlet, "/foo");
|
||||
this.context.registerBeanDefinition("initializerBean", beanDefinition(initializer));
|
||||
this.context.registerBeanDefinition("servletBean", beanDefinition(servlet));
|
||||
this.context.registerBeanDefinition("filterBean", beanDefinition(filter));
|
||||
this.context.refresh();
|
||||
ServletContext servletContext = getWebServerFactory().getServletContext();
|
||||
then(servletContext).should(atMost(1)).addServlet(anyString(), any(Servlet.class));
|
||||
then(servletContext).should(atMost(1)).addFilter(anyString(), any(Filter.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterRegistrationBeansSkipsRegisteredFilters() {
|
||||
addWebServerFactoryBean();
|
||||
Filter filter = mock(Filter.class);
|
||||
FilterRegistrationBean<Filter> initializer = new FilterRegistrationBean<>(filter);
|
||||
this.context.registerBeanDefinition("initializerBean", beanDefinition(initializer));
|
||||
this.context.registerBeanDefinition("filterBean", beanDefinition(filter));
|
||||
this.context.refresh();
|
||||
ServletContext servletContext = getWebServerFactory().getServletContext();
|
||||
then(servletContext).should(atMost(1)).addFilter(anyString(), any(Filter.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void delegatingFilterProxyRegistrationBeansSkipsTargetBeanNames() {
|
||||
addWebServerFactoryBean();
|
||||
DelegatingFilterProxyRegistrationBean initializer = new DelegatingFilterProxyRegistrationBean("filterBean");
|
||||
this.context.registerBeanDefinition("initializerBean", beanDefinition(initializer));
|
||||
BeanDefinition filterBeanDefinition = beanDefinition(new IllegalStateException("Create FilterBean Failure"));
|
||||
filterBeanDefinition.setLazyInit(true);
|
||||
this.context.registerBeanDefinition("filterBean", filterBeanDefinition);
|
||||
this.context.refresh();
|
||||
ServletContext servletContext = getWebServerFactory().getServletContext();
|
||||
then(servletContext).should(atMost(1)).addFilter(anyString(), this.filterCaptor.capture());
|
||||
// Up to this point the filterBean should not have been created, calling
|
||||
// the delegate proxy will trigger creation and an exception
|
||||
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> {
|
||||
this.filterCaptor.getValue().init(new MockFilterConfig());
|
||||
this.filterCaptor.getValue()
|
||||
.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse(), new MockFilterChain());
|
||||
}).withMessageContaining("Create FilterBean Failure");
|
||||
}
|
||||
|
||||
@Test
|
||||
void postProcessWebServerFactory() {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(MockServletWebServerFactory.class);
|
||||
MutablePropertyValues pv = new MutablePropertyValues();
|
||||
pv.add("port", "${port}");
|
||||
beanDefinition.setPropertyValues(pv);
|
||||
this.context.registerBeanDefinition("webServerFactory", beanDefinition);
|
||||
PropertySourcesPlaceholderConfigurer propertySupport = new PropertySourcesPlaceholderConfigurer();
|
||||
Properties properties = new Properties();
|
||||
properties.put("port", 8080);
|
||||
propertySupport.setProperties(properties);
|
||||
this.context.registerBeanDefinition("propertySupport", beanDefinition(propertySupport));
|
||||
this.context.refresh();
|
||||
assertThat(getWebServerFactory().getWebServer().getPort()).isEqualTo(8080);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotReplaceExistingScopes() {
|
||||
// gh-2082
|
||||
Scope scope = mock(Scope.class);
|
||||
ConfigurableListableBeanFactory factory = this.context.getBeanFactory();
|
||||
factory.registerScope(WebApplicationContext.SCOPE_REQUEST, scope);
|
||||
factory.registerScope(WebApplicationContext.SCOPE_SESSION, scope);
|
||||
addWebServerFactoryBean();
|
||||
this.context.refresh();
|
||||
assertThat(factory.getRegisteredScope(WebApplicationContext.SCOPE_REQUEST)).isSameAs(scope);
|
||||
assertThat(factory.getRegisteredScope(WebApplicationContext.SCOPE_SESSION)).isSameAs(scope);
|
||||
}
|
||||
|
||||
@Test
|
||||
void servletRequestCanBeInjectedEarly(CapturedOutput output) {
|
||||
// gh-14990
|
||||
int initialOutputLength = output.length();
|
||||
addWebServerFactoryBean();
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(WithAutowiredServletRequest.class);
|
||||
beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);
|
||||
this.context.registerBeanDefinition("withAutowiredServletRequest", beanDefinition);
|
||||
this.context.addBeanFactoryPostProcessor((beanFactory) -> {
|
||||
WithAutowiredServletRequest bean = beanFactory.getBean(WithAutowiredServletRequest.class);
|
||||
assertThat(bean.getRequest()).isNotNull();
|
||||
});
|
||||
this.context.refresh();
|
||||
assertThat(output.toString().substring(initialOutputLength)).doesNotContain("Replacing scope");
|
||||
}
|
||||
|
||||
@Test
|
||||
void webApplicationScopeIsRegistered() {
|
||||
addWebServerFactoryBean();
|
||||
this.context.refresh();
|
||||
assertThat(this.context.getBeanFactory().getRegisteredScope(WebApplicationContext.SCOPE_APPLICATION))
|
||||
.isNotNull();
|
||||
}
|
||||
|
||||
private void addWebServerFactoryBean() {
|
||||
this.context.registerBeanDefinition("webServerFactory",
|
||||
new RootBeanDefinition(MockServletWebServerFactory.class));
|
||||
}
|
||||
|
||||
MockServletWebServerFactory getWebServerFactory() {
|
||||
return this.context.getBean(MockServletWebServerFactory.class);
|
||||
}
|
||||
|
||||
private BeanDefinition beanDefinition(Object bean) {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition();
|
||||
beanDefinition.setBeanClass(getClass());
|
||||
beanDefinition.setFactoryMethodName("getBean");
|
||||
ConstructorArgumentValues constructorArguments = new ConstructorArgumentValues();
|
||||
constructorArguments.addGenericArgumentValue(bean);
|
||||
beanDefinition.setConstructorArgumentValues(constructorArguments);
|
||||
return beanDefinition;
|
||||
}
|
||||
|
||||
static <T> T getBean(T object) {
|
||||
if (object instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
static class TestApplicationListener implements ApplicationListener<ApplicationEvent> {
|
||||
|
||||
private final Deque<ApplicationEvent> events = new ArrayDeque<>();
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationEvent event) {
|
||||
this.events.add(event);
|
||||
}
|
||||
|
||||
List<ApplicationEvent> receivedEvents() {
|
||||
List<ApplicationEvent> receivedEvents = new ArrayList<>();
|
||||
while (!this.events.isEmpty()) {
|
||||
receivedEvents.add(this.events.pollFirst());
|
||||
}
|
||||
return receivedEvents;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Order(10)
|
||||
static class OrderedFilter extends GenericFilterBean {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class WithAutowiredServletRequest {
|
||||
|
||||
private final ServletRequest request;
|
||||
|
||||
WithAutowiredServletRequest(ServletRequest request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
ServletRequest getRequest() {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class RefreshFailure {
|
||||
|
||||
RefreshFailure() {
|
||||
throw new RuntimeException("Fail refresh");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet.context;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.FilterConfig;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.annotation.WebFilter;
|
||||
import jakarta.servlet.annotation.WebInitParam;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry;
|
||||
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebFilterHandler}
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class WebFilterHandlerTests {
|
||||
|
||||
private final WebFilterHandler handler = new WebFilterHandler();
|
||||
|
||||
private final SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void defaultFilterConfiguration() throws IOException {
|
||||
AnnotatedBeanDefinition definition = createBeanDefinition(DefaultConfigurationFilter.class);
|
||||
this.handler.handle(definition, this.registry);
|
||||
BeanDefinition filterRegistrationBean = this.registry
|
||||
.getBeanDefinition(DefaultConfigurationFilter.class.getName());
|
||||
MutablePropertyValues propertyValues = filterRegistrationBean.getPropertyValues();
|
||||
assertThat(propertyValues.get("asyncSupported")).isEqualTo(false);
|
||||
assertThat((EnumSet<DispatcherType>) propertyValues.get("dispatcherTypes"))
|
||||
.containsExactly(DispatcherType.REQUEST);
|
||||
assertThat(((Map<String, String>) propertyValues.get("initParameters"))).isEmpty();
|
||||
assertThat((String[]) propertyValues.get("servletNames")).isEmpty();
|
||||
assertThat((String[]) propertyValues.get("urlPatterns")).isEmpty();
|
||||
assertThat(propertyValues.get("name")).isEqualTo(DefaultConfigurationFilter.class.getName());
|
||||
assertThat(propertyValues.get("filter")).isEqualTo(definition);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterWithCustomName() throws IOException {
|
||||
AnnotatedBeanDefinition definition = createBeanDefinition(CustomNameFilter.class);
|
||||
this.handler.handle(definition, this.registry);
|
||||
BeanDefinition filterRegistrationBean = this.registry.getBeanDefinition("custom");
|
||||
MutablePropertyValues propertyValues = filterRegistrationBean.getPropertyValues();
|
||||
assertThat(propertyValues.get("name")).isEqualTo("custom");
|
||||
}
|
||||
|
||||
@Test
|
||||
void asyncSupported() throws IOException {
|
||||
BeanDefinition filterRegistrationBean = handleBeanDefinitionForClass(AsyncSupportedFilter.class);
|
||||
MutablePropertyValues propertyValues = filterRegistrationBean.getPropertyValues();
|
||||
assertThat(propertyValues.get("asyncSupported")).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void dispatcherTypes() throws IOException {
|
||||
BeanDefinition filterRegistrationBean = handleBeanDefinitionForClass(DispatcherTypesFilter.class);
|
||||
MutablePropertyValues propertyValues = filterRegistrationBean.getPropertyValues();
|
||||
assertThat((Set<DispatcherType>) propertyValues.get("dispatcherTypes")).containsExactly(DispatcherType.FORWARD,
|
||||
DispatcherType.INCLUDE, DispatcherType.REQUEST);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void initParameters() throws IOException {
|
||||
BeanDefinition filterRegistrationBean = handleBeanDefinitionForClass(InitParametersFilter.class);
|
||||
MutablePropertyValues propertyValues = filterRegistrationBean.getPropertyValues();
|
||||
assertThat((Map<String, String>) propertyValues.get("initParameters")).containsEntry("a", "alpha")
|
||||
.containsEntry("b", "bravo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void servletNames() throws IOException {
|
||||
BeanDefinition filterRegistrationBean = handleBeanDefinitionForClass(ServletNamesFilter.class);
|
||||
MutablePropertyValues propertyValues = filterRegistrationBean.getPropertyValues();
|
||||
assertThat((String[]) propertyValues.get("servletNames")).contains("alpha", "bravo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void urlPatterns() throws IOException {
|
||||
BeanDefinition filterRegistrationBean = handleBeanDefinitionForClass(UrlPatternsFilter.class);
|
||||
MutablePropertyValues propertyValues = filterRegistrationBean.getPropertyValues();
|
||||
assertThat((String[]) propertyValues.get("urlPatterns")).contains("alpha", "bravo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void urlPatternsFromValue() throws IOException {
|
||||
BeanDefinition filterRegistrationBean = handleBeanDefinitionForClass(UrlPatternsFromValueFilter.class);
|
||||
MutablePropertyValues propertyValues = filterRegistrationBean.getPropertyValues();
|
||||
assertThat((String[]) propertyValues.get("urlPatterns")).contains("alpha", "bravo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void urlPatternsDeclaredTwice() {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> handleBeanDefinitionForClass(UrlPatternsDeclaredTwiceFilter.class))
|
||||
.withMessageContaining("The urlPatterns and value attributes are mutually exclusive");
|
||||
}
|
||||
|
||||
private AnnotatedBeanDefinition createBeanDefinition(Class<?> filterClass) throws IOException {
|
||||
AnnotatedBeanDefinition definition = mock(AnnotatedBeanDefinition.class);
|
||||
given(definition.getBeanClassName()).willReturn(filterClass.getName());
|
||||
given(definition.getMetadata()).willReturn(
|
||||
new SimpleMetadataReaderFactory().getMetadataReader(filterClass.getName()).getAnnotationMetadata());
|
||||
return definition;
|
||||
}
|
||||
|
||||
private BeanDefinition handleBeanDefinitionForClass(Class<?> filterClass) throws IOException {
|
||||
this.handler.handle(createBeanDefinition(filterClass), this.registry);
|
||||
return this.registry.getBeanDefinition(filterClass.getName());
|
||||
}
|
||||
|
||||
@WebFilter
|
||||
class DefaultConfigurationFilter extends BaseFilter {
|
||||
|
||||
}
|
||||
|
||||
@WebFilter(asyncSupported = true)
|
||||
class AsyncSupportedFilter extends BaseFilter {
|
||||
|
||||
}
|
||||
|
||||
@WebFilter(dispatcherTypes = { DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE })
|
||||
class DispatcherTypesFilter extends BaseFilter {
|
||||
|
||||
}
|
||||
|
||||
@WebFilter(initParams = { @WebInitParam(name = "a", value = "alpha"), @WebInitParam(name = "b", value = "bravo") })
|
||||
class InitParametersFilter extends BaseFilter {
|
||||
|
||||
}
|
||||
|
||||
@WebFilter(servletNames = { "alpha", "bravo" })
|
||||
class ServletNamesFilter extends BaseFilter {
|
||||
|
||||
}
|
||||
|
||||
@WebFilter(urlPatterns = { "alpha", "bravo" })
|
||||
class UrlPatternsFilter extends BaseFilter {
|
||||
|
||||
}
|
||||
|
||||
@WebFilter({ "alpha", "bravo" })
|
||||
class UrlPatternsFromValueFilter extends BaseFilter {
|
||||
|
||||
}
|
||||
|
||||
@WebFilter(value = { "alpha", "bravo" }, urlPatterns = { "alpha", "bravo" })
|
||||
class UrlPatternsDeclaredTwiceFilter extends BaseFilter {
|
||||
|
||||
}
|
||||
|
||||
@WebFilter(filterName = "custom")
|
||||
class CustomNameFilter extends BaseFilter {
|
||||
|
||||
}
|
||||
|
||||
class BaseFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet.context;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.ServletContextAttributeEvent;
|
||||
import jakarta.servlet.ServletContextAttributeListener;
|
||||
import jakarta.servlet.annotation.WebListener;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
|
||||
import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry;
|
||||
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
|
||||
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebListenerHandler}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class WebListenerHandlerTests {
|
||||
|
||||
private final WebListenerHandler handler = new WebListenerHandler();
|
||||
|
||||
private final SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
|
||||
@Test
|
||||
void listener() throws IOException {
|
||||
AnnotatedBeanDefinition definition = mock(AnnotatedBeanDefinition.class);
|
||||
given(definition.getBeanClassName()).willReturn(TestListener.class.getName());
|
||||
given(definition.getMetadata())
|
||||
.willReturn(new SimpleMetadataReaderFactory().getMetadataReader(TestListener.class.getName())
|
||||
.getAnnotationMetadata());
|
||||
this.handler.handle(definition, this.registry);
|
||||
this.registry.getBeanDefinition(TestListener.class.getName() + "Registrar");
|
||||
}
|
||||
|
||||
@WebListener
|
||||
static class TestListener implements ServletContextAttributeListener {
|
||||
|
||||
@Override
|
||||
public void attributeAdded(ServletContextAttributeEvent event) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attributeRemoved(ServletContextAttributeEvent event) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attributeReplaced(ServletContextAttributeEvent event) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet.context;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.annotation.WebInitParam;
|
||||
import jakarta.servlet.annotation.WebServlet;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry;
|
||||
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebServletHandler}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class WebServletHandlerTests {
|
||||
|
||||
private final WebServletHandler handler = new WebServletHandler();
|
||||
|
||||
private final SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void defaultServletConfiguration() throws IOException {
|
||||
AnnotatedBeanDefinition servletDefinition = createBeanDefinition(DefaultConfigurationServlet.class);
|
||||
this.handler.handle(servletDefinition, this.registry);
|
||||
BeanDefinition servletRegistrationBean = this.registry
|
||||
.getBeanDefinition(DefaultConfigurationServlet.class.getName());
|
||||
MutablePropertyValues propertyValues = servletRegistrationBean.getPropertyValues();
|
||||
assertThat(propertyValues.get("asyncSupported")).isEqualTo(false);
|
||||
assertThat(((Map<String, String>) propertyValues.get("initParameters"))).isEmpty();
|
||||
assertThat((Integer) propertyValues.get("loadOnStartup")).isEqualTo(-1);
|
||||
assertThat(propertyValues.get("name")).isEqualTo(DefaultConfigurationServlet.class.getName());
|
||||
assertThat((String[]) propertyValues.get("urlMappings")).isEmpty();
|
||||
assertThat(propertyValues.get("servlet")).isEqualTo(servletDefinition);
|
||||
}
|
||||
|
||||
@Test
|
||||
void servletWithCustomName() throws IOException {
|
||||
AnnotatedBeanDefinition definition = createBeanDefinition(CustomNameServlet.class);
|
||||
this.handler.handle(definition, this.registry);
|
||||
BeanDefinition servletRegistrationBean = this.registry.getBeanDefinition("custom");
|
||||
MutablePropertyValues propertyValues = servletRegistrationBean.getPropertyValues();
|
||||
assertThat(propertyValues.get("name")).isEqualTo("custom");
|
||||
}
|
||||
|
||||
@Test
|
||||
void asyncSupported() throws IOException {
|
||||
BeanDefinition servletRegistrationBean = handleBeanDefinitionForClass(AsyncSupportedServlet.class);
|
||||
MutablePropertyValues propertyValues = servletRegistrationBean.getPropertyValues();
|
||||
assertThat(propertyValues.get("asyncSupported")).isEqualTo(true);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void initParameters() throws IOException {
|
||||
BeanDefinition servletRegistrationBean = handleBeanDefinitionForClass(InitParametersServlet.class);
|
||||
MutablePropertyValues propertyValues = servletRegistrationBean.getPropertyValues();
|
||||
assertThat((Map<String, String>) propertyValues.get("initParameters")).containsEntry("a", "alpha")
|
||||
.containsEntry("b", "bravo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void urlMappings() throws IOException {
|
||||
BeanDefinition servletRegistrationBean = handleBeanDefinitionForClass(UrlPatternsServlet.class);
|
||||
MutablePropertyValues propertyValues = servletRegistrationBean.getPropertyValues();
|
||||
assertThat((String[]) propertyValues.get("urlMappings")).contains("alpha", "bravo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void urlMappingsFromValue() throws IOException {
|
||||
BeanDefinition servletRegistrationBean = handleBeanDefinitionForClass(UrlPatternsFromValueServlet.class);
|
||||
MutablePropertyValues propertyValues = servletRegistrationBean.getPropertyValues();
|
||||
assertThat((String[]) propertyValues.get("urlMappings")).contains("alpha", "bravo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void urlPatternsDeclaredTwice() {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> handleBeanDefinitionForClass(UrlPatternsDeclaredTwiceServlet.class))
|
||||
.withMessageContaining("The urlPatterns and value attributes are mutually exclusive");
|
||||
}
|
||||
|
||||
private AnnotatedBeanDefinition createBeanDefinition(Class<?> servletClass) throws IOException {
|
||||
AnnotatedBeanDefinition definition = mock(AnnotatedBeanDefinition.class);
|
||||
given(definition.getBeanClassName()).willReturn(servletClass.getName());
|
||||
given(definition.getMetadata()).willReturn(
|
||||
new SimpleMetadataReaderFactory().getMetadataReader(servletClass.getName()).getAnnotationMetadata());
|
||||
return definition;
|
||||
}
|
||||
|
||||
private BeanDefinition handleBeanDefinitionForClass(Class<?> filterClass) throws IOException {
|
||||
this.handler.handle(createBeanDefinition(filterClass), this.registry);
|
||||
return this.registry.getBeanDefinition(filterClass.getName());
|
||||
}
|
||||
|
||||
@WebServlet
|
||||
class DefaultConfigurationServlet extends HttpServlet {
|
||||
|
||||
}
|
||||
|
||||
@WebServlet(asyncSupported = true)
|
||||
class AsyncSupportedServlet extends HttpServlet {
|
||||
|
||||
}
|
||||
|
||||
@WebServlet(initParams = { @WebInitParam(name = "a", value = "alpha"), @WebInitParam(name = "b", value = "bravo") })
|
||||
class InitParametersServlet extends HttpServlet {
|
||||
|
||||
}
|
||||
|
||||
@WebServlet(urlPatterns = { "alpha", "bravo" })
|
||||
class UrlPatternsServlet extends HttpServlet {
|
||||
|
||||
}
|
||||
|
||||
@WebServlet({ "alpha", "bravo" })
|
||||
class UrlPatternsFromValueServlet extends HttpServlet {
|
||||
|
||||
}
|
||||
|
||||
@WebServlet(value = { "alpha", "bravo" }, urlPatterns = { "alpha", "bravo" })
|
||||
class UrlPatternsDeclaredTwiceServlet extends HttpServlet {
|
||||
|
||||
}
|
||||
|
||||
@WebServlet(name = "custom")
|
||||
class CustomNameServlet extends HttpServlet {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet.context;
|
||||
|
||||
import jakarta.servlet.Servlet;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.web.server.servlet.MockServletWebServerFactory;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import static org.mockito.BDDMockito.then;
|
||||
|
||||
/**
|
||||
* Tests for {@link XmlServletWebServerApplicationContext}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class XmlServletWebServerApplicationContextTests {
|
||||
|
||||
private static final String PATH = XmlServletWebServerApplicationContextTests.class.getPackage()
|
||||
.getName()
|
||||
.replace('.', '/') + "/";
|
||||
|
||||
private static final String FILE = "exampleEmbeddedWebApplicationConfiguration.xml";
|
||||
|
||||
private XmlServletWebServerApplicationContext context;
|
||||
|
||||
@Test
|
||||
void createFromResource() {
|
||||
this.context = new XmlServletWebServerApplicationContext(new ClassPathResource(FILE, getClass()));
|
||||
verifyContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromResourceLocation() {
|
||||
this.context = new XmlServletWebServerApplicationContext(PATH + FILE);
|
||||
verifyContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromRelativeResourceLocation() {
|
||||
this.context = new XmlServletWebServerApplicationContext(getClass(), FILE);
|
||||
verifyContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadAndRefreshFromResource() {
|
||||
this.context = new XmlServletWebServerApplicationContext();
|
||||
this.context.load(new ClassPathResource(FILE, getClass()));
|
||||
this.context.refresh();
|
||||
verifyContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadAndRefreshFromResourceLocation() {
|
||||
this.context = new XmlServletWebServerApplicationContext();
|
||||
this.context.load(PATH + FILE);
|
||||
this.context.refresh();
|
||||
verifyContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadAndRefreshFromRelativeResourceLocation() {
|
||||
this.context = new XmlServletWebServerApplicationContext();
|
||||
this.context.load(getClass(), FILE);
|
||||
this.context.refresh();
|
||||
verifyContext();
|
||||
}
|
||||
|
||||
private void verifyContext() {
|
||||
MockServletWebServerFactory factory = this.context.getBean(MockServletWebServerFactory.class);
|
||||
Servlet servlet = this.context.getBean(Servlet.class);
|
||||
then(factory.getServletContext()).should().addServlet("servlet", servlet);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet.context.config;
|
||||
|
||||
import jakarta.servlet.Servlet;
|
||||
|
||||
import org.springframework.boot.web.server.servlet.MockServletWebServerFactory;
|
||||
import org.springframework.boot.web.servlet.mock.MockServlet;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Example {@code @Configuration} for use with
|
||||
* {@code AnnotationConfigServletWebServerApplicationContextTests}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class ExampleServletWebServerApplicationConfiguration {
|
||||
|
||||
@Bean
|
||||
public MockServletWebServerFactory webServerFactory() {
|
||||
return new MockServletWebServerFactory();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Servlet servlet() {
|
||||
return new MockServlet();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet.context.testcomponents.filter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.FilterConfig;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.annotation.WebFilter;
|
||||
|
||||
@WebFilter("/*")
|
||||
public class TestFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
request.setAttribute("filterAttribute", "bravo");
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet.context.testcomponents.listener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletContextEvent;
|
||||
import jakarta.servlet.ServletContextListener;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.annotation.WebListener;
|
||||
|
||||
@WebListener
|
||||
public class TestListener implements ServletContextListener {
|
||||
|
||||
@Override
|
||||
public void contextInitialized(ServletContextEvent sce) {
|
||||
sce.getServletContext()
|
||||
.addFilter("listenerAddedFilter", new ListenerAddedFilter())
|
||||
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
|
||||
sce.getServletContext().setAttribute("listenerAttribute", "alpha");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contextDestroyed(ServletContextEvent sce) {
|
||||
|
||||
}
|
||||
|
||||
static class ListenerAddedFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
request.setAttribute("listenerAddedFilterAttribute", "charlie");
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.servlet.context.testcomponents.servlet;
|
||||
|
||||
import jakarta.servlet.annotation.MultipartConfig;
|
||||
import jakarta.servlet.annotation.WebServlet;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
|
||||
@WebServlet("/test-multipart")
|
||||
@MultipartConfig(location = "test", maxFileSize = 1024, maxRequestSize = 2048, fileSizeThreshold = 512)
|
||||
public class TestMultipartServlet extends HttpServlet {
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user