Create beginnings of spring-boot-tomcat module
This commit is contained in:
committed by
Phillip Webb
parent
0337830615
commit
349f296d26
@@ -1,134 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.Host;
|
||||
import org.apache.catalina.WebResourceRoot;
|
||||
import org.apache.catalina.loader.WebappLoader;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.catalina.webresources.StandardRoot;
|
||||
import org.apache.tomcat.util.scan.StandardJarScanFilter;
|
||||
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.server.reactive.ConfigurableReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.reactive.ReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.tomcat.ConfigurableTomcatWebServerFactory;
|
||||
import org.springframework.boot.web.server.tomcat.DisableReferenceClearingContextCustomizer;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatEmbeddedContext;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatEmbeddedWebappClassLoader;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatWebServer;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatWebServerFactory;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.http.server.reactive.TomcatHttpHandlerAdapter;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* {@link ReactiveWebServerFactory} that can be used to create a {@link TomcatWebServer}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author HaiTao Zhang
|
||||
* @author Moritz Halbritter
|
||||
* @author Scott Frederick
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class TomcatReactiveWebServerFactory extends TomcatWebServerFactory
|
||||
implements ConfigurableTomcatWebServerFactory, ConfigurableReactiveWebServerFactory {
|
||||
|
||||
/**
|
||||
* Create a new {@link TomcatReactiveWebServerFactory} instance.
|
||||
*/
|
||||
public TomcatReactiveWebServerFactory() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link TomcatReactiveWebServerFactory} that listens for requests using
|
||||
* the specified port.
|
||||
* @param port the port to listen on
|
||||
*/
|
||||
public TomcatReactiveWebServerFactory(int port) {
|
||||
super(port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebServer getWebServer(HttpHandler httpHandler) {
|
||||
Tomcat tomcat = createTomcat();
|
||||
TomcatHttpHandlerAdapter servlet = new TomcatHttpHandlerAdapter(httpHandler);
|
||||
prepareContext(tomcat.getHost(), servlet);
|
||||
return getTomcatWebServer(tomcat);
|
||||
}
|
||||
|
||||
protected void prepareContext(Host host, TomcatHttpHandlerAdapter servlet) {
|
||||
File docBase = createTempDir("tomcat-docbase");
|
||||
TomcatEmbeddedContext context = new TomcatEmbeddedContext();
|
||||
WebResourceRoot resourceRoot = new StandardRoot(context);
|
||||
ignoringNoSuchMethodError(() -> resourceRoot.setReadOnly(true));
|
||||
context.setResources(resourceRoot);
|
||||
context.setPath("");
|
||||
context.setDocBase(docBase.getAbsolutePath());
|
||||
context.addLifecycleListener(new Tomcat.FixContextListener());
|
||||
ClassLoader parentClassLoader = ClassUtils.getDefaultClassLoader();
|
||||
context.setParentClassLoader(parentClassLoader);
|
||||
skipAllTldScanning(context);
|
||||
WebappLoader loader = new WebappLoader();
|
||||
loader.setLoaderInstance(new TomcatEmbeddedWebappClassLoader(parentClassLoader));
|
||||
loader.setDelegate(true);
|
||||
context.setLoader(loader);
|
||||
Tomcat.addServlet(context, "httpHandlerServlet", servlet).setAsyncSupported(true);
|
||||
context.addServletMappingDecoded("/", "httpHandlerServlet");
|
||||
host.addChild(context);
|
||||
configureContext(context);
|
||||
}
|
||||
|
||||
private void ignoringNoSuchMethodError(Runnable method) {
|
||||
try {
|
||||
method.run();
|
||||
}
|
||||
catch (NoSuchMethodError ex) {
|
||||
}
|
||||
}
|
||||
|
||||
private void skipAllTldScanning(TomcatEmbeddedContext context) {
|
||||
StandardJarScanFilter filter = new StandardJarScanFilter();
|
||||
filter.setTldSkip("*.jar");
|
||||
context.getJarScanner().setJarScanFilter(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the Tomcat {@link Context}.
|
||||
* @param context the Tomcat context
|
||||
*/
|
||||
protected void configureContext(Context context) {
|
||||
this.getContextLifecycleListeners().forEach(context::addLifecycleListener);
|
||||
new DisableReferenceClearingContextCustomizer().customize(context);
|
||||
this.getContextCustomizers().forEach((customizer) -> customizer.customize(context));
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method called to create the {@link TomcatWebServer}. Subclasses can
|
||||
* override this method to return a different {@link TomcatWebServer} or apply
|
||||
* additional processing to the Tomcat server.
|
||||
* @param tomcat the Tomcat server.
|
||||
* @return a new {@link TomcatWebServer} instance
|
||||
*/
|
||||
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
|
||||
return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
* 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 implementation backed by Tomcat.
|
||||
*/
|
||||
package org.springframework.boot.web.server.reactive.tomcat;
|
||||
@@ -1,161 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.JarURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Attributes.Name;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.apache.catalina.LifecycleException;
|
||||
import org.apache.catalina.WebResource;
|
||||
import org.apache.catalina.WebResourceRoot;
|
||||
import org.apache.catalina.WebResourceSet;
|
||||
import org.apache.catalina.webresources.AbstractSingleArchiveResourceSet;
|
||||
import org.apache.catalina.webresources.JarResource;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
|
||||
/**
|
||||
* A {@link WebResourceSet} for a resource in a nested JAR.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class NestedJarResourceSet extends AbstractSingleArchiveResourceSet {
|
||||
|
||||
private static final Name MULTI_RELEASE = new Name("Multi-Release");
|
||||
|
||||
private final URL url;
|
||||
|
||||
private JarFile archive = null;
|
||||
|
||||
private long archiveUseCount = 0;
|
||||
|
||||
private boolean useCaches;
|
||||
|
||||
private volatile Boolean multiRelease;
|
||||
|
||||
NestedJarResourceSet(URL url, WebResourceRoot root, String webAppMount, String internalPath)
|
||||
throws IllegalArgumentException {
|
||||
this.url = url;
|
||||
setRoot(root);
|
||||
setWebAppMount(webAppMount);
|
||||
setInternalPath(internalPath);
|
||||
setStaticOnly(true);
|
||||
if (getRoot().getState().isAvailable()) {
|
||||
try {
|
||||
start();
|
||||
}
|
||||
catch (LifecycleException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected WebResource createArchiveResource(JarEntry jarEntry, String webAppPath, Manifest manifest) {
|
||||
return new JarResource(this, webAppPath, getBaseUrlString(), jarEntry);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initInternal() throws LifecycleException {
|
||||
try {
|
||||
JarURLConnection connection = connect();
|
||||
try {
|
||||
setManifest(connection.getManifest());
|
||||
setBaseUrl(connection.getJarFileURL());
|
||||
}
|
||||
finally {
|
||||
if (!connection.getUseCaches()) {
|
||||
connection.getJarFile().close();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JarFile openJarFile() throws IOException {
|
||||
synchronized (this.archiveLock) {
|
||||
if (this.archive == null) {
|
||||
JarURLConnection connection = connect();
|
||||
this.useCaches = connection.getUseCaches();
|
||||
this.archive = connection.getJarFile();
|
||||
}
|
||||
this.archiveUseCount++;
|
||||
return this.archive;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closeJarFile() {
|
||||
synchronized (this.archiveLock) {
|
||||
this.archiveUseCount--;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isMultiRelease() {
|
||||
if (this.multiRelease == null) {
|
||||
synchronized (this.archiveLock) {
|
||||
if (this.multiRelease == null) {
|
||||
// JarFile.isMultiRelease() is final so we must go to the manifest
|
||||
Manifest manifest = getManifest();
|
||||
Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null;
|
||||
this.multiRelease = (attributes != null) && attributes.containsKey(MULTI_RELEASE);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.multiRelease;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void gc() {
|
||||
synchronized (this.archiveLock) {
|
||||
if (this.archive != null && this.archiveUseCount == 0) {
|
||||
try {
|
||||
if (!this.useCaches) {
|
||||
this.archive.close();
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// Ignore
|
||||
}
|
||||
this.archive = null;
|
||||
this.archiveEntries = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private JarURLConnection connect() throws IOException {
|
||||
URLConnection connection = this.url.openConnection();
|
||||
ResourceUtils.useCachesIfNecessary(connection);
|
||||
Assert.state(connection instanceof JarURLConnection,
|
||||
() -> "URL '%s' did not return a JAR connection".formatted(this.url));
|
||||
connection.connect();
|
||||
return (JarURLConnection) connection;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* TLD skip and scan patterns used by Spring Boot.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
final class TldPatterns {
|
||||
|
||||
static final Set<String> TOMCAT_SKIP;
|
||||
|
||||
static {
|
||||
// Same as Tomcat
|
||||
Set<String> skipPatterns = new LinkedHashSet<>();
|
||||
skipPatterns.add("annotations-api.jar");
|
||||
skipPatterns.add("ant-junit*.jar");
|
||||
skipPatterns.add("ant-launcher*.jar");
|
||||
skipPatterns.add("ant*.jar");
|
||||
skipPatterns.add("asm-*.jar");
|
||||
skipPatterns.add("aspectj*.jar");
|
||||
skipPatterns.add("bcel*.jar");
|
||||
skipPatterns.add("biz.aQute.bnd*.jar");
|
||||
skipPatterns.add("bootstrap.jar");
|
||||
skipPatterns.add("catalina-ant.jar");
|
||||
skipPatterns.add("catalina-ha.jar");
|
||||
skipPatterns.add("catalina-ssi.jar");
|
||||
skipPatterns.add("catalina-storeconfig.jar");
|
||||
skipPatterns.add("catalina-tribes.jar");
|
||||
skipPatterns.add("catalina.jar");
|
||||
skipPatterns.add("cglib-*.jar");
|
||||
skipPatterns.add("cobertura-*.jar");
|
||||
skipPatterns.add("commons-beanutils*.jar");
|
||||
skipPatterns.add("commons-codec*.jar");
|
||||
skipPatterns.add("commons-collections*.jar");
|
||||
skipPatterns.add("commons-compress*.jar");
|
||||
skipPatterns.add("commons-daemon.jar");
|
||||
skipPatterns.add("commons-dbcp*.jar");
|
||||
skipPatterns.add("commons-digester*.jar");
|
||||
skipPatterns.add("commons-fileupload*.jar");
|
||||
skipPatterns.add("commons-httpclient*.jar");
|
||||
skipPatterns.add("commons-io*.jar");
|
||||
skipPatterns.add("commons-lang*.jar");
|
||||
skipPatterns.add("commons-logging*.jar");
|
||||
skipPatterns.add("commons-math*.jar");
|
||||
skipPatterns.add("commons-pool*.jar");
|
||||
skipPatterns.add("derby-*.jar");
|
||||
skipPatterns.add("dom4j-*.jar");
|
||||
skipPatterns.add("easymock-*.jar");
|
||||
skipPatterns.add("ecj-*.jar");
|
||||
skipPatterns.add("el-api.jar");
|
||||
skipPatterns.add("geronimo-spec-jaxrpc*.jar");
|
||||
skipPatterns.add("h2*.jar");
|
||||
skipPatterns.add("ha-api-*.jar");
|
||||
skipPatterns.add("hamcrest-*.jar");
|
||||
skipPatterns.add("hibernate*.jar");
|
||||
skipPatterns.add("httpclient*.jar");
|
||||
skipPatterns.add("icu4j-*.jar");
|
||||
skipPatterns.add("jakartaee-migration-*.jar");
|
||||
skipPatterns.add("jasper-el.jar");
|
||||
skipPatterns.add("jasper.jar");
|
||||
skipPatterns.add("jaspic-api.jar");
|
||||
skipPatterns.add("jaxb-*.jar");
|
||||
skipPatterns.add("jaxen-*.jar");
|
||||
skipPatterns.add("jaxws-rt-*.jar");
|
||||
skipPatterns.add("jdom-*.jar");
|
||||
skipPatterns.add("jetty-*.jar");
|
||||
skipPatterns.add("jmx-tools.jar");
|
||||
skipPatterns.add("jmx.jar");
|
||||
skipPatterns.add("jsp-api.jar");
|
||||
skipPatterns.add("jstl.jar");
|
||||
skipPatterns.add("jta*.jar");
|
||||
skipPatterns.add("junit-*.jar");
|
||||
skipPatterns.add("junit.jar");
|
||||
skipPatterns.add("log4j*.jar");
|
||||
skipPatterns.add("mail*.jar");
|
||||
skipPatterns.add("objenesis-*.jar");
|
||||
skipPatterns.add("oraclepki.jar");
|
||||
skipPatterns.add("org.hamcrest.core_*.jar");
|
||||
skipPatterns.add("org.junit_*.jar");
|
||||
skipPatterns.add("oro-*.jar");
|
||||
skipPatterns.add("servlet-api-*.jar");
|
||||
skipPatterns.add("servlet-api.jar");
|
||||
skipPatterns.add("slf4j*.jar");
|
||||
skipPatterns.add("taglibs-standard-spec-*.jar");
|
||||
skipPatterns.add("tagsoup-*.jar");
|
||||
skipPatterns.add("tomcat-api.jar");
|
||||
skipPatterns.add("tomcat-coyote.jar");
|
||||
skipPatterns.add("tomcat-coyote-ffm.jar");
|
||||
skipPatterns.add("tomcat-dbcp.jar");
|
||||
skipPatterns.add("tomcat-i18n-*.jar");
|
||||
skipPatterns.add("tomcat-jdbc.jar");
|
||||
skipPatterns.add("tomcat-jni.jar");
|
||||
skipPatterns.add("tomcat-juli-adapters.jar");
|
||||
skipPatterns.add("tomcat-juli.jar");
|
||||
skipPatterns.add("tomcat-util-scan.jar");
|
||||
skipPatterns.add("tomcat-util.jar");
|
||||
skipPatterns.add("tomcat-websocket.jar");
|
||||
skipPatterns.add("tools.jar");
|
||||
skipPatterns.add("unboundid-ldapsdk-*.jar");
|
||||
skipPatterns.add("websocket-api.jar");
|
||||
skipPatterns.add("websocket-client-api.jar");
|
||||
skipPatterns.add("wsdl4j*.jar");
|
||||
skipPatterns.add("xercesImpl.jar");
|
||||
skipPatterns.add("xml-apis.jar");
|
||||
skipPatterns.add("xmlParserAPIs-*.jar");
|
||||
skipPatterns.add("xmlParserAPIs.jar");
|
||||
skipPatterns.add("xom-*.jar");
|
||||
TOMCAT_SKIP = Collections.unmodifiableSet(skipPatterns);
|
||||
}
|
||||
|
||||
private static final Set<String> ADDITIONAL_SKIP;
|
||||
|
||||
static {
|
||||
// Additional typical for Spring Boot applications
|
||||
Set<String> skipPatterns = new LinkedHashSet<>();
|
||||
skipPatterns.add("antlr-*.jar");
|
||||
skipPatterns.add("aopalliance-*.jar");
|
||||
skipPatterns.add("aspectjweaver-*.jar");
|
||||
skipPatterns.add("classmate-*.jar");
|
||||
skipPatterns.add("ehcache-core-*.jar");
|
||||
skipPatterns.add("hsqldb-*.jar");
|
||||
skipPatterns.add("jackson-annotations-*.jar");
|
||||
skipPatterns.add("jackson-core-*.jar");
|
||||
skipPatterns.add("jackson-databind-*.jar");
|
||||
skipPatterns.add("jandex-*.jar");
|
||||
skipPatterns.add("javassist-*.jar");
|
||||
skipPatterns.add("jboss-logging-*.jar");
|
||||
skipPatterns.add("jboss-transaction-api_*.jar");
|
||||
skipPatterns.add("jcl-over-slf4j-*.jar");
|
||||
skipPatterns.add("jdom-*.jar");
|
||||
skipPatterns.add("jul-to-slf4j-*.jar");
|
||||
skipPatterns.add("logback-classic-*.jar");
|
||||
skipPatterns.add("logback-core-*.jar");
|
||||
skipPatterns.add("rome-*.jar");
|
||||
skipPatterns.add("spring-aop-*.jar");
|
||||
skipPatterns.add("spring-aspects-*.jar");
|
||||
skipPatterns.add("spring-beans-*.jar");
|
||||
skipPatterns.add("spring-boot-*.jar");
|
||||
skipPatterns.add("spring-core-*.jar");
|
||||
skipPatterns.add("spring-context-*.jar");
|
||||
skipPatterns.add("spring-data-*.jar");
|
||||
skipPatterns.add("spring-expression-*.jar");
|
||||
skipPatterns.add("spring-jdbc-*.jar,");
|
||||
skipPatterns.add("spring-orm-*.jar");
|
||||
skipPatterns.add("spring-oxm-*.jar");
|
||||
skipPatterns.add("spring-tx-*.jar");
|
||||
skipPatterns.add("snakeyaml-*.jar");
|
||||
skipPatterns.add("tomcat-embed-core-*.jar");
|
||||
skipPatterns.add("tomcat-embed-logging-*.jar");
|
||||
skipPatterns.add("tomcat-embed-el-*.jar");
|
||||
skipPatterns.add("validation-api-*.jar");
|
||||
ADDITIONAL_SKIP = Collections.unmodifiableSet(skipPatterns);
|
||||
}
|
||||
|
||||
static final Set<String> DEFAULT_SKIP;
|
||||
|
||||
static {
|
||||
Set<String> skipPatterns = new LinkedHashSet<>();
|
||||
skipPatterns.addAll(TOMCAT_SKIP);
|
||||
skipPatterns.addAll(ADDITIONAL_SKIP);
|
||||
DEFAULT_SKIP = Collections.unmodifiableSet(skipPatterns);
|
||||
}
|
||||
|
||||
static final Set<String> TOMCAT_SCAN;
|
||||
|
||||
static {
|
||||
Set<String> scanPatterns = new LinkedHashSet<>();
|
||||
scanPatterns.add("log4j-taglib*.jar");
|
||||
scanPatterns.add("log4j-jakarta-web*.jar");
|
||||
scanPatterns.add("log4javascript*.jar");
|
||||
scanPatterns.add("slf4j-taglib*.jar");
|
||||
TOMCAT_SCAN = Collections.unmodifiableSet(scanPatterns);
|
||||
}
|
||||
|
||||
static final Set<String> DEFAULT_SCAN;
|
||||
|
||||
static {
|
||||
Set<String> scanPatterns = new LinkedHashSet<>(TOMCAT_SCAN);
|
||||
DEFAULT_SCAN = Collections.unmodifiableSet(scanPatterns);
|
||||
}
|
||||
|
||||
private TldPatterns() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,683 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.servlet.ServletContainerInitializer;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.Host;
|
||||
import org.apache.catalina.Lifecycle;
|
||||
import org.apache.catalina.LifecycleEvent;
|
||||
import org.apache.catalina.LifecycleException;
|
||||
import org.apache.catalina.LifecycleListener;
|
||||
import org.apache.catalina.Manager;
|
||||
import org.apache.catalina.Valve;
|
||||
import org.apache.catalina.WebResource;
|
||||
import org.apache.catalina.WebResourceRoot;
|
||||
import org.apache.catalina.WebResourceRoot.ResourceSetType;
|
||||
import org.apache.catalina.WebResourceSet;
|
||||
import org.apache.catalina.Wrapper;
|
||||
import org.apache.catalina.loader.WebappLoader;
|
||||
import org.apache.catalina.session.StandardManager;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.catalina.startup.Tomcat.FixContextListener;
|
||||
import org.apache.catalina.util.LifecycleBase;
|
||||
import org.apache.catalina.util.SessionConfig;
|
||||
import org.apache.catalina.webresources.AbstractResourceSet;
|
||||
import org.apache.catalina.webresources.EmptyResource;
|
||||
import org.apache.catalina.webresources.StandardRoot;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.tomcat.util.http.Rfc6265CookieProcessor;
|
||||
import org.apache.tomcat.util.scan.StandardJarScanFilter;
|
||||
|
||||
import org.springframework.boot.web.server.Cookie.SameSite;
|
||||
import org.springframework.boot.web.server.ErrorPage;
|
||||
import org.springframework.boot.web.server.MimeMappings;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.ContextPath;
|
||||
import org.springframework.boot.web.server.servlet.CookieSameSiteSupplier;
|
||||
import org.springframework.boot.web.server.servlet.DocumentRoot;
|
||||
import org.springframework.boot.web.server.servlet.ServletContextInitializer;
|
||||
import org.springframework.boot.web.server.servlet.ServletContextInitializers;
|
||||
import org.springframework.boot.web.server.servlet.ServletWebServerSettings;
|
||||
import org.springframework.boot.web.server.tomcat.ConfigurableTomcatWebServerFactory;
|
||||
import org.springframework.boot.web.server.tomcat.DisableReferenceClearingContextCustomizer;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatContextCustomizer;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatEmbeddedContext;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatEmbeddedWebappClassLoader;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatStarter;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatWebServer;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatWebServerFactory;
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link ConfigurableServletWebServerFactory} that can be used to create
|
||||
* {@link TomcatWebServer}s. Can be initialized using Spring's
|
||||
* {@link ServletContextInitializer}s or Tomcat {@link LifecycleListener}s.
|
||||
* <p>
|
||||
* Unless explicitly configured otherwise this factory will create containers that listen
|
||||
* for HTTP requests on port 8080.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @author Brock Mills
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
* @author Eddú Meléndez
|
||||
* @author Christoffer Sawicki
|
||||
* @author Dawid Antecki
|
||||
* @author Moritz Halbritter
|
||||
* @author Scott Frederick
|
||||
* @since 4.0.0
|
||||
* @see #setPort(int)
|
||||
* @see #setContextLifecycleListeners(Collection)
|
||||
* @see TomcatWebServer
|
||||
*/
|
||||
public class TomcatServletWebServerFactory extends TomcatWebServerFactory
|
||||
implements ConfigurableTomcatWebServerFactory, ConfigurableServletWebServerFactory, ResourceLoaderAware {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(TomcatServletWebServerFactory.class);
|
||||
|
||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private static final Set<Class<?>> NO_CLASSES = Collections.emptySet();
|
||||
|
||||
private final ServletWebServerSettings settings = new ServletWebServerSettings();
|
||||
|
||||
private ResourceLoader resourceLoader;
|
||||
|
||||
private Set<String> tldSkipPatterns = new LinkedHashSet<>(TldPatterns.DEFAULT_SKIP);
|
||||
|
||||
private final Set<String> tldScanPatterns = new LinkedHashSet<>(TldPatterns.DEFAULT_SCAN);
|
||||
|
||||
/**
|
||||
* Create a new {@link TomcatServletWebServerFactory} instance.
|
||||
*/
|
||||
public TomcatServletWebServerFactory() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link TomcatServletWebServerFactory} that listens for requests using
|
||||
* the specified port.
|
||||
* @param port the port to listen on
|
||||
*/
|
||||
public TomcatServletWebServerFactory(int port) {
|
||||
super(port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link TomcatServletWebServerFactory} with the specified context path
|
||||
* and port.
|
||||
* @param contextPath the root context path
|
||||
* @param port the port to listen on
|
||||
*/
|
||||
public TomcatServletWebServerFactory(String contextPath, int port) {
|
||||
super(port);
|
||||
this.settings.setContextPath(ContextPath.of(contextPath));
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebServer getWebServer(ServletContextInitializer... initializers) {
|
||||
Tomcat tomcat = createTomcat();
|
||||
prepareContext(tomcat.getHost(), initializers);
|
||||
return getTomcatWebServer(tomcat);
|
||||
}
|
||||
|
||||
protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
|
||||
DocumentRoot documentRoot = new DocumentRoot(logger);
|
||||
documentRoot.setDirectory(this.settings.getDocumentRoot());
|
||||
File documentRootFile = documentRoot.getValidDirectory();
|
||||
TomcatEmbeddedContext context = new TomcatEmbeddedContext();
|
||||
WebResourceRoot resourceRoot = (documentRootFile != null) ? new LoaderHidingResourceRoot(context)
|
||||
: new StandardRoot(context);
|
||||
ignoringNoSuchMethodError(() -> resourceRoot.setReadOnly(true));
|
||||
context.setResources(resourceRoot);
|
||||
String contextPath = this.settings.getContextPath().toString();
|
||||
context.setName(contextPath);
|
||||
context.setDisplayName(this.settings.getDisplayName());
|
||||
context.setPath(contextPath);
|
||||
File docBase = (documentRootFile != null) ? documentRootFile : createTempDir("tomcat-docbase");
|
||||
context.setDocBase(docBase.getAbsolutePath());
|
||||
context.addLifecycleListener(new FixContextListener());
|
||||
ClassLoader parentClassLoader = (this.resourceLoader != null) ? this.resourceLoader.getClassLoader()
|
||||
: ClassUtils.getDefaultClassLoader();
|
||||
context.setParentClassLoader(parentClassLoader);
|
||||
resetDefaultLocaleMapping(context);
|
||||
addLocaleMappings(context);
|
||||
context.setCreateUploadTargets(true);
|
||||
configureTldPatterns(context);
|
||||
WebappLoader loader = new WebappLoader();
|
||||
loader.setLoaderInstance(new TomcatEmbeddedWebappClassLoader(parentClassLoader));
|
||||
loader.setDelegate(true);
|
||||
context.setLoader(loader);
|
||||
if (this.settings.isRegisterDefaultServlet()) {
|
||||
addDefaultServlet(context);
|
||||
}
|
||||
if (shouldRegisterJspServlet()) {
|
||||
addJspServlet(context);
|
||||
addJasperInitializer(context);
|
||||
}
|
||||
context.addLifecycleListener(new StaticResourceConfigurer(context));
|
||||
ServletContextInitializers initializersToUse = ServletContextInitializers.from(this.settings, initializers);
|
||||
host.addChild(context);
|
||||
configureContext(context, initializersToUse);
|
||||
postProcessContext(context);
|
||||
}
|
||||
|
||||
private void ignoringNoSuchMethodError(Runnable method) {
|
||||
try {
|
||||
method.run();
|
||||
}
|
||||
catch (NoSuchMethodError ex) {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldRegisterJspServlet() {
|
||||
return this.settings.getJsp() != null && this.settings.getJsp().getRegistered()
|
||||
&& ClassUtils.isPresent(this.settings.getJsp().getClassName(), getClass().getClassLoader());
|
||||
}
|
||||
|
||||
/**
|
||||
* Override Tomcat's default locale mappings to align with other servers. See
|
||||
* {@code org.apache.catalina.util.CharsetMapperDefault.properties}.
|
||||
* @param context the context to reset
|
||||
*/
|
||||
private void resetDefaultLocaleMapping(TomcatEmbeddedContext context) {
|
||||
context.addLocaleEncodingMappingParameter(Locale.ENGLISH.toString(), DEFAULT_CHARSET.displayName());
|
||||
context.addLocaleEncodingMappingParameter(Locale.FRENCH.toString(), DEFAULT_CHARSET.displayName());
|
||||
context.addLocaleEncodingMappingParameter(Locale.JAPANESE.toString(), DEFAULT_CHARSET.displayName());
|
||||
}
|
||||
|
||||
private void addLocaleMappings(TomcatEmbeddedContext context) {
|
||||
this.settings.getLocaleCharsetMappings()
|
||||
.forEach((locale, charset) -> context.addLocaleEncodingMappingParameter(locale.toString(),
|
||||
charset.toString()));
|
||||
}
|
||||
|
||||
private void configureTldPatterns(TomcatEmbeddedContext context) {
|
||||
StandardJarScanFilter filter = new StandardJarScanFilter();
|
||||
filter.setTldSkip(StringUtils.collectionToCommaDelimitedString(this.tldSkipPatterns));
|
||||
filter.setTldScan(StringUtils.collectionToCommaDelimitedString(this.tldScanPatterns));
|
||||
context.getJarScanner().setJarScanFilter(filter);
|
||||
}
|
||||
|
||||
private void addDefaultServlet(Context context) {
|
||||
Wrapper defaultServlet = context.createWrapper();
|
||||
defaultServlet.setName("default");
|
||||
defaultServlet.setServletClass("org.apache.catalina.servlets.DefaultServlet");
|
||||
defaultServlet.addInitParameter("debug", "0");
|
||||
defaultServlet.addInitParameter("listings", "false");
|
||||
defaultServlet.setLoadOnStartup(1);
|
||||
// Otherwise the default location of a Spring DispatcherServlet cannot be set
|
||||
defaultServlet.setOverridable(true);
|
||||
context.addChild(defaultServlet);
|
||||
context.addServletMappingDecoded("/", "default");
|
||||
}
|
||||
|
||||
private void addJspServlet(Context context) {
|
||||
Wrapper jspServlet = context.createWrapper();
|
||||
jspServlet.setName("jsp");
|
||||
jspServlet.setServletClass(this.settings.getJsp().getClassName());
|
||||
jspServlet.addInitParameter("fork", "false");
|
||||
this.settings.getJsp().getInitParameters().forEach(jspServlet::addInitParameter);
|
||||
jspServlet.setLoadOnStartup(3);
|
||||
context.addChild(jspServlet);
|
||||
context.addServletMappingDecoded("*.jsp", "jsp");
|
||||
context.addServletMappingDecoded("*.jspx", "jsp");
|
||||
}
|
||||
|
||||
private void addJasperInitializer(TomcatEmbeddedContext context) {
|
||||
try {
|
||||
ServletContainerInitializer initializer = (ServletContainerInitializer) ClassUtils
|
||||
.forName("org.apache.jasper.servlet.JasperInitializer", null)
|
||||
.getDeclaredConstructor()
|
||||
.newInstance();
|
||||
context.addServletContainerInitializer(initializer, null);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Probably not Tomcat 8
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the Tomcat {@link Context}.
|
||||
* @param context the Tomcat context
|
||||
* @param initializers initializers to apply
|
||||
*/
|
||||
protected void configureContext(Context context, Iterable<ServletContextInitializer> initializers) {
|
||||
TomcatStarter starter = new TomcatStarter(initializers);
|
||||
if (context instanceof TomcatEmbeddedContext embeddedContext) {
|
||||
embeddedContext.setStarter(starter);
|
||||
embeddedContext.setFailCtxIfServletStartFails(true);
|
||||
}
|
||||
context.addServletContainerInitializer(starter, NO_CLASSES);
|
||||
for (LifecycleListener lifecycleListener : this.getContextLifecycleListeners()) {
|
||||
context.addLifecycleListener(lifecycleListener);
|
||||
}
|
||||
for (Valve valve : this.getContextValves()) {
|
||||
context.getPipeline().addValve(valve);
|
||||
}
|
||||
for (ErrorPage errorPage : getErrorPages()) {
|
||||
org.apache.tomcat.util.descriptor.web.ErrorPage tomcatErrorPage = new org.apache.tomcat.util.descriptor.web.ErrorPage();
|
||||
tomcatErrorPage.setLocation(errorPage.getPath());
|
||||
tomcatErrorPage.setErrorCode(errorPage.getStatusCode());
|
||||
tomcatErrorPage.setExceptionType(errorPage.getExceptionName());
|
||||
context.addErrorPage(tomcatErrorPage);
|
||||
}
|
||||
setMimeMappings(context);
|
||||
configureSession(context);
|
||||
configureCookieProcessor(context);
|
||||
new DisableReferenceClearingContextCustomizer().customize(context);
|
||||
for (String webListenerClassName : getSettings().getWebListenerClassNames()) {
|
||||
context.addApplicationListener(webListenerClassName);
|
||||
}
|
||||
for (TomcatContextCustomizer customizer : this.getContextCustomizers()) {
|
||||
customizer.customize(context);
|
||||
}
|
||||
}
|
||||
|
||||
private void configureSession(Context context) {
|
||||
long sessionTimeout = getSessionTimeoutInMinutes();
|
||||
context.setSessionTimeout((int) sessionTimeout);
|
||||
Boolean httpOnly = this.settings.getSession().getCookie().getHttpOnly();
|
||||
if (httpOnly != null) {
|
||||
context.setUseHttpOnly(httpOnly);
|
||||
}
|
||||
if (this.settings.getSession().isPersistent()) {
|
||||
Manager manager = context.getManager();
|
||||
if (manager == null) {
|
||||
manager = new StandardManager();
|
||||
context.setManager(manager);
|
||||
}
|
||||
configurePersistSession(manager);
|
||||
}
|
||||
else {
|
||||
context.addLifecycleListener(new DisablePersistSessionListener());
|
||||
}
|
||||
}
|
||||
|
||||
private void setMimeMappings(Context context) {
|
||||
MimeMappings mimeMappings = this.settings.getMimeMappings();
|
||||
if (context instanceof TomcatEmbeddedContext embeddedContext) {
|
||||
embeddedContext.setMimeMappings(mimeMappings);
|
||||
return;
|
||||
}
|
||||
for (MimeMappings.Mapping mapping : mimeMappings) {
|
||||
context.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
|
||||
}
|
||||
}
|
||||
|
||||
private void configureCookieProcessor(Context context) {
|
||||
SameSite sessionSameSite = this.settings.getSession().getCookie().getSameSite();
|
||||
List<CookieSameSiteSupplier> suppliers = new ArrayList<>();
|
||||
if (sessionSameSite != null) {
|
||||
suppliers.add(CookieSameSiteSupplier.of(sessionSameSite)
|
||||
.whenHasName(() -> SessionConfig.getSessionCookieName(context)));
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(this.settings.getCookieSameSiteSuppliers())) {
|
||||
suppliers.addAll(this.settings.getCookieSameSiteSuppliers());
|
||||
}
|
||||
if (!suppliers.isEmpty()) {
|
||||
context.setCookieProcessor(new SuppliedSameSiteCookieProcessor(suppliers));
|
||||
}
|
||||
}
|
||||
|
||||
private void configurePersistSession(Manager manager) {
|
||||
Assert.state(manager instanceof StandardManager,
|
||||
() -> "Unable to persist HTTP session state using manager type " + manager.getClass().getName());
|
||||
File dir = this.settings.getSession().getSessionStoreDirectory().getValidDirectory(true);
|
||||
File file = new File(dir, "SESSIONS.ser");
|
||||
((StandardManager) manager).setPathname(file.getAbsolutePath());
|
||||
}
|
||||
|
||||
private long getSessionTimeoutInMinutes() {
|
||||
Duration sessionTimeout = this.settings.getSession().getTimeout();
|
||||
if (isZeroOrLess(sessionTimeout)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(sessionTimeout.toMinutes(), 1);
|
||||
}
|
||||
|
||||
private boolean isZeroOrLess(Duration sessionTimeout) {
|
||||
return sessionTimeout == null || sessionTimeout.isNegative() || sessionTimeout.isZero();
|
||||
}
|
||||
|
||||
/**
|
||||
* Post process the Tomcat {@link Context} before it's used with the Tomcat Server.
|
||||
* Subclasses can override this method to apply additional processing to the
|
||||
* {@link Context}.
|
||||
* @param context the Tomcat {@link Context}
|
||||
*/
|
||||
protected void postProcessContext(Context context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method called to create the {@link TomcatWebServer}. Subclasses can
|
||||
* override this method to return a different {@link TomcatWebServer} or apply
|
||||
* additional processing to the Tomcat server.
|
||||
* @param tomcat the Tomcat server.
|
||||
* @return a new {@link TomcatWebServer} instance
|
||||
*/
|
||||
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
|
||||
return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResourceLoader(ResourceLoader resourceLoader) {
|
||||
this.resourceLoader = resourceLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mutable set of the patterns that match jars to ignore for TLD scanning.
|
||||
* @return the set of jars to ignore for TLD scanning
|
||||
*/
|
||||
public Set<String> getTldSkipPatterns() {
|
||||
return this.tldSkipPatterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the patterns that match jars to ignore for TLD scanning. See Tomcat's
|
||||
* catalina.properties for typical values. Defaults to a list drawn from that source.
|
||||
* @param patterns the jar patterns to skip when scanning for TLDs etc
|
||||
*/
|
||||
public void setTldSkipPatterns(Collection<String> patterns) {
|
||||
Assert.notNull(patterns, "'patterns' must not be null");
|
||||
this.tldSkipPatterns = new LinkedHashSet<>(patterns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add patterns that match jars to ignore for TLD scanning. See Tomcat's
|
||||
* catalina.properties for typical values.
|
||||
* @param patterns the additional jar patterns to skip when scanning for TLDs etc
|
||||
*/
|
||||
public void addTldSkipPatterns(String... patterns) {
|
||||
Assert.notNull(patterns, "'patterns' must not be null");
|
||||
this.tldSkipPatterns.addAll(Arrays.asList(patterns));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletWebServerSettings getSettings() {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link LifecycleListener} to disable persistence in the {@link StandardManager}. A
|
||||
* {@link LifecycleListener} is used so not to interfere with Tomcat's default manager
|
||||
* creation logic.
|
||||
*/
|
||||
private static final class DisablePersistSessionListener implements LifecycleListener {
|
||||
|
||||
@Override
|
||||
public void lifecycleEvent(LifecycleEvent event) {
|
||||
if (event.getType().equals(Lifecycle.START_EVENT)) {
|
||||
Context context = (Context) event.getLifecycle();
|
||||
Manager manager = context.getManager();
|
||||
if (manager instanceof StandardManager standardManager) {
|
||||
standardManager.setPathname(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final class StaticResourceConfigurer implements LifecycleListener {
|
||||
|
||||
private static final String WEB_APP_MOUNT = "/";
|
||||
|
||||
private static final String INTERNAL_PATH = "/META-INF/resources";
|
||||
|
||||
private final Context context;
|
||||
|
||||
private StaticResourceConfigurer(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lifecycleEvent(LifecycleEvent event) {
|
||||
if (event.getType().equals(Lifecycle.BEFORE_INIT_EVENT)) {
|
||||
addResourceJars(TomcatServletWebServerFactory.this.getSettings().getStaticResourceUrls());
|
||||
}
|
||||
}
|
||||
|
||||
private void addResourceJars(List<URL> resourceJarUrls) {
|
||||
for (URL url : resourceJarUrls) {
|
||||
String path = url.getPath();
|
||||
if (path.endsWith(".jar") || path.endsWith(".jar!/")) {
|
||||
String jar = url.toString();
|
||||
if (!jar.startsWith("jar:")) {
|
||||
// A jar file in the file system. Convert to Jar URL.
|
||||
jar = "jar:" + jar + "!/";
|
||||
}
|
||||
addResourceSet(jar);
|
||||
}
|
||||
else {
|
||||
addResourceSet(url.toString());
|
||||
}
|
||||
}
|
||||
for (WebResourceSet resources : this.context.getResources().getJarResources()) {
|
||||
resources.setReadOnly(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void addResourceSet(String resource) {
|
||||
try {
|
||||
if (isInsideClassicNestedJar(resource)) {
|
||||
addClassicNestedResourceSet(resource);
|
||||
return;
|
||||
}
|
||||
WebResourceRoot root = this.context.getResources();
|
||||
URL url = new URL(resource);
|
||||
if (isInsideNestedJar(resource)) {
|
||||
root.addJarResources(new NestedJarResourceSet(url, root, WEB_APP_MOUNT, INTERNAL_PATH));
|
||||
}
|
||||
else {
|
||||
root.createWebResourceSet(ResourceSetType.RESOURCE_JAR, WEB_APP_MOUNT, url, INTERNAL_PATH);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Ignore (probably not a directory)
|
||||
}
|
||||
}
|
||||
|
||||
private void addClassicNestedResourceSet(String resource) throws MalformedURLException {
|
||||
// It's a nested jar but we now don't want the suffix because Tomcat
|
||||
// is going to try and locate it as a root URL (not the resource
|
||||
// inside it)
|
||||
URL url = new URL(resource.substring(0, resource.length() - 2));
|
||||
this.context.getResources()
|
||||
.createWebResourceSet(ResourceSetType.RESOURCE_JAR, WEB_APP_MOUNT, url, INTERNAL_PATH);
|
||||
}
|
||||
|
||||
private boolean isInsideClassicNestedJar(String resource) {
|
||||
return !isInsideNestedJar(resource) && resource.indexOf("!/") < resource.lastIndexOf("!/");
|
||||
}
|
||||
|
||||
private boolean isInsideNestedJar(String resource) {
|
||||
return resource.startsWith("jar:nested:");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class LoaderHidingResourceRoot extends StandardRoot {
|
||||
|
||||
private LoaderHidingResourceRoot(TomcatEmbeddedContext context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected WebResourceSet createMainResourceSet() {
|
||||
return new LoaderHidingWebResourceSet(super.createMainResourceSet());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class LoaderHidingWebResourceSet extends AbstractResourceSet {
|
||||
|
||||
private final WebResourceSet delegate;
|
||||
|
||||
private final Method initInternal;
|
||||
|
||||
private LoaderHidingWebResourceSet(WebResourceSet delegate) {
|
||||
this.delegate = delegate;
|
||||
try {
|
||||
this.initInternal = LifecycleBase.class.getDeclaredMethod("initInternal");
|
||||
this.initInternal.setAccessible(true);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebResource getResource(String path) {
|
||||
if (path.startsWith("/org/springframework/boot")) {
|
||||
return new EmptyResource(getRoot(), path);
|
||||
}
|
||||
return this.delegate.getResource(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] list(String path) {
|
||||
return this.delegate.list(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> listWebAppPaths(String path) {
|
||||
return this.delegate.listWebAppPaths(path)
|
||||
.stream()
|
||||
.filter((webAppPath) -> !webAppPath.startsWith("/org/springframework/boot"))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mkdir(String path) {
|
||||
return this.delegate.mkdir(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean write(String path, InputStream is, boolean overwrite) {
|
||||
return this.delegate.write(path, is, overwrite);
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getBaseUrl() {
|
||||
return this.delegate.getBaseUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadOnly(boolean readOnly) {
|
||||
this.delegate.setReadOnly(readOnly);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadOnly() {
|
||||
return this.delegate.isReadOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void gc() {
|
||||
this.delegate.gc();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAllowLinking(boolean allowLinking) {
|
||||
this.delegate.setAllowLinking(allowLinking);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getAllowLinking() {
|
||||
return this.delegate.getAllowLinking();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initInternal() throws LifecycleException {
|
||||
if (this.delegate instanceof LifecycleBase) {
|
||||
try {
|
||||
ReflectionUtils.invokeMethod(this.initInternal, this.delegate);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new LifecycleException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Rfc6265CookieProcessor} that supports {@link CookieSameSiteSupplier
|
||||
* supplied} {@link SameSite} values.
|
||||
*/
|
||||
private static class SuppliedSameSiteCookieProcessor extends Rfc6265CookieProcessor {
|
||||
|
||||
private final List<CookieSameSiteSupplier> suppliers;
|
||||
|
||||
SuppliedSameSiteCookieProcessor(List<CookieSameSiteSupplier> suppliers) {
|
||||
this.suppliers = suppliers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateHeader(Cookie cookie, HttpServletRequest request) {
|
||||
SameSite sameSite = getSameSite(cookie);
|
||||
String sameSiteValue = (sameSite != null) ? sameSite.attributeValue() : null;
|
||||
if (sameSiteValue == null) {
|
||||
return super.generateHeader(cookie, request);
|
||||
}
|
||||
Rfc6265CookieProcessor delegate = new Rfc6265CookieProcessor();
|
||||
delegate.setSameSiteCookies(sameSiteValue);
|
||||
return delegate.generateHeader(cookie, request);
|
||||
}
|
||||
|
||||
private SameSite getSameSite(Cookie cookie) {
|
||||
for (CookieSameSiteSupplier supplier : this.suppliers) {
|
||||
SameSite sameSite = supplier.getSameSite(cookie);
|
||||
if (sameSite != null) {
|
||||
return sameSite;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
* 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 implementation backed by Tomcat.
|
||||
*/
|
||||
package org.springframework.boot.web.server.servlet.tomcat;
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import org.apache.catalina.connector.Connector;
|
||||
import org.apache.coyote.ProtocolHandler;
|
||||
import org.apache.coyote.http11.AbstractHttp11Protocol;
|
||||
|
||||
import org.springframework.boot.web.server.Compression;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link TomcatConnectorCustomizer} that configures compression support on the given
|
||||
* Connector.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class CompressionConnectorCustomizer implements TomcatConnectorCustomizer {
|
||||
|
||||
private final Compression compression;
|
||||
|
||||
public CompressionConnectorCustomizer(Compression compression) {
|
||||
this.compression = compression;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(Connector connector) {
|
||||
if (this.compression != null && this.compression.getEnabled()) {
|
||||
ProtocolHandler handler = connector.getProtocolHandler();
|
||||
if (handler instanceof AbstractHttp11Protocol<?> abstractHttp11Protocol) {
|
||||
customize(abstractHttp11Protocol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void customize(AbstractHttp11Protocol<?> protocol) {
|
||||
Compression compression = this.compression;
|
||||
protocol.setCompression("on");
|
||||
protocol.setCompressionMinSize(getMinResponseSize(compression));
|
||||
protocol.setCompressibleMimeType(getMimeTypes(compression));
|
||||
if (this.compression.getExcludedUserAgents() != null) {
|
||||
protocol.setNoCompressionUserAgents(getExcludedUserAgents());
|
||||
}
|
||||
}
|
||||
|
||||
private int getMinResponseSize(Compression compression) {
|
||||
return (int) compression.getMinResponseSize().toBytes();
|
||||
}
|
||||
|
||||
private String getMimeTypes(Compression compression) {
|
||||
return StringUtils.arrayToCommaDelimitedString(compression.getMimeTypes());
|
||||
}
|
||||
|
||||
private String getExcludedUserAgents() {
|
||||
return StringUtils.arrayToCommaDelimitedString(this.compression.getExcludedUserAgents());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.Engine;
|
||||
import org.apache.catalina.Valve;
|
||||
import org.apache.catalina.connector.Connector;
|
||||
|
||||
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
|
||||
import org.springframework.boot.web.server.reactive.tomcat.TomcatReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.tomcat.TomcatServletWebServerFactory;
|
||||
|
||||
/**
|
||||
* {@link ConfigurableWebServerFactory} for Tomcat-specific features.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 4.0.0
|
||||
* @see TomcatServletWebServerFactory
|
||||
* @see TomcatReactiveWebServerFactory
|
||||
*/
|
||||
public interface ConfigurableTomcatWebServerFactory extends ConfigurableWebServerFactory {
|
||||
|
||||
/**
|
||||
* Set the Tomcat base directory. If not specified a temporary directory will be used.
|
||||
* @param baseDirectory the tomcat base directory
|
||||
*/
|
||||
void setBaseDirectory(File baseDirectory);
|
||||
|
||||
/**
|
||||
* Sets the background processor delay in seconds.
|
||||
* @param delay the delay in seconds
|
||||
*/
|
||||
void setBackgroundProcessorDelay(int delay);
|
||||
|
||||
/**
|
||||
* Add {@link Valve}s that should be applied to the Tomcat {@link Engine}.
|
||||
* @param engineValves the valves to add
|
||||
*/
|
||||
void addEngineValves(Valve... engineValves);
|
||||
|
||||
/**
|
||||
* Add {@link TomcatConnectorCustomizer}s that should be added to the Tomcat
|
||||
* {@link Connector}.
|
||||
* @param tomcatConnectorCustomizers the customizers to add
|
||||
*/
|
||||
void addConnectorCustomizers(TomcatConnectorCustomizer... tomcatConnectorCustomizers);
|
||||
|
||||
/**
|
||||
* Add {@link TomcatContextCustomizer}s that should be added to the Tomcat
|
||||
* {@link Context}.
|
||||
* @param tomcatContextCustomizers the customizers to add
|
||||
*/
|
||||
void addContextCustomizers(TomcatContextCustomizer... tomcatContextCustomizers);
|
||||
|
||||
/**
|
||||
* Add {@link TomcatProtocolHandlerCustomizer}s that should be added to the Tomcat
|
||||
* {@link Connector}.
|
||||
* @param tomcatProtocolHandlerCustomizers the customizers to add
|
||||
* @since 4.0.0
|
||||
*/
|
||||
void addProtocolHandlerCustomizers(TomcatProtocolHandlerCustomizer<?>... tomcatProtocolHandlerCustomizers);
|
||||
|
||||
/**
|
||||
* Set the character encoding to use for URL decoding. If not specified 'UTF-8' will
|
||||
* be used.
|
||||
* @param uriEncoding the uri encoding to set
|
||||
*/
|
||||
void setUriEncoding(Charset uriEncoding);
|
||||
|
||||
/**
|
||||
* Whether to use APR.
|
||||
* @param useApr whether to use APR
|
||||
*/
|
||||
void setUseApr(boolean useApr);
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import org.apache.catalina.connector.Connector;
|
||||
|
||||
import org.springframework.boot.web.server.WebServerException;
|
||||
|
||||
/**
|
||||
* A {@code ConnectorStartFailedException} is thrown when a Tomcat {@link Connector} fails
|
||||
* to start, for example due to a port clash or incorrect SSL configuration.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class ConnectorStartFailedException extends WebServerException {
|
||||
|
||||
private final int port;
|
||||
|
||||
/**
|
||||
* Creates a new {@code ConnectorStartFailedException} for a connector that's
|
||||
* configured to listen on the given {@code port}.
|
||||
* @param port the port
|
||||
*/
|
||||
public ConnectorStartFailedException(int port) {
|
||||
super("Connector configured to listen on port " + port + " failed to start", null);
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.tomcat;
|
||||
|
||||
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
|
||||
import org.springframework.boot.diagnostics.FailureAnalysis;
|
||||
|
||||
/**
|
||||
* An {@link AbstractFailureAnalyzer} for {@link ConnectorStartFailedException}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class ConnectorStartFailureAnalyzer extends AbstractFailureAnalyzer<ConnectorStartFailedException> {
|
||||
|
||||
@Override
|
||||
protected FailureAnalysis analyze(Throwable rootFailure, ConnectorStartFailedException cause) {
|
||||
return new FailureAnalysis(
|
||||
"The Tomcat connector configured to listen on port " + cause.getPort()
|
||||
+ " failed to start. The port may already be in use or the connector may be misconfigured.",
|
||||
"Verify the connector's configuration, identify and stop any process that's listening on port "
|
||||
+ cause.getPort() + ", or configure this application to listen on another port.",
|
||||
cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.core.StandardContext;
|
||||
|
||||
/**
|
||||
* A {@link TomcatContextCustomizer} that disables Tomcat's reflective reference clearing
|
||||
* to avoid reflective access warnings on Java 9 and later JVMs.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class DisableReferenceClearingContextCustomizer implements TomcatContextCustomizer {
|
||||
|
||||
@Override
|
||||
public void customize(Context context) {
|
||||
if (!(context instanceof StandardContext standardContext)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
standardContext.setClearReferencesRmiTargets(false);
|
||||
standardContext.setClearReferencesThreadLocals(false);
|
||||
}
|
||||
catch (NoSuchMethodError ex) {
|
||||
// Earlier version of Tomcat (probably without
|
||||
// setClearReferencesThreadLocals). Continue.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import org.apache.catalina.Container;
|
||||
import org.apache.catalina.Service;
|
||||
import org.apache.catalina.connector.Connector;
|
||||
import org.apache.catalina.core.StandardContext;
|
||||
import org.apache.catalina.core.StandardWrapper;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.boot.web.server.GracefulShutdownCallback;
|
||||
import org.springframework.boot.web.server.GracefulShutdownResult;
|
||||
|
||||
/**
|
||||
* Handles Tomcat graceful shutdown.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
final class GracefulShutdown {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(GracefulShutdown.class);
|
||||
|
||||
private final Tomcat tomcat;
|
||||
|
||||
private volatile boolean aborted = false;
|
||||
|
||||
GracefulShutdown(Tomcat tomcat) {
|
||||
this.tomcat = tomcat;
|
||||
}
|
||||
|
||||
void shutDownGracefully(GracefulShutdownCallback callback) {
|
||||
logger.info("Commencing graceful shutdown. Waiting for active requests to complete");
|
||||
CountDownLatch shutdownUnderway = new CountDownLatch(1);
|
||||
new Thread(() -> doShutdown(callback, shutdownUnderway), "tomcat-shutdown").start();
|
||||
try {
|
||||
shutdownUnderway.await();
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
private void doShutdown(GracefulShutdownCallback callback, CountDownLatch shutdownUnderway) {
|
||||
try {
|
||||
List<Connector> connectors = getConnectors();
|
||||
connectors.forEach(this::close);
|
||||
shutdownUnderway.countDown();
|
||||
awaitInactiveOrAborted();
|
||||
if (this.aborted) {
|
||||
logger.info("Graceful shutdown aborted with one or more requests still active");
|
||||
callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
|
||||
}
|
||||
else {
|
||||
logger.info("Graceful shutdown complete");
|
||||
callback.shutdownComplete(GracefulShutdownResult.IDLE);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
shutdownUnderway.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
private List<Connector> getConnectors() {
|
||||
List<Connector> connectors = new ArrayList<>();
|
||||
for (Service service : this.tomcat.getServer().findServices()) {
|
||||
Collections.addAll(connectors, service.findConnectors());
|
||||
}
|
||||
return connectors;
|
||||
}
|
||||
|
||||
private void close(Connector connector) {
|
||||
connector.pause();
|
||||
connector.getProtocolHandler().closeServerSocketGraceful();
|
||||
}
|
||||
|
||||
private void awaitInactiveOrAborted() {
|
||||
try {
|
||||
for (Container host : this.tomcat.getEngine().findChildren()) {
|
||||
for (Container context : host.findChildren()) {
|
||||
while (!this.aborted && isActive(context)) {
|
||||
Thread.sleep(50);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isActive(Container context) {
|
||||
try {
|
||||
if (((StandardContext) context).getInProgressAsyncCount() > 0) {
|
||||
return true;
|
||||
}
|
||||
for (Container wrapper : context.findChildren()) {
|
||||
if (((StandardWrapper) wrapper).getCountAllocated() > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
void abort() {
|
||||
this.aborted = true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import org.apache.catalina.LifecycleException;
|
||||
import org.apache.catalina.LifecycleState;
|
||||
import org.apache.catalina.util.StandardSessionIdGenerator;
|
||||
|
||||
/**
|
||||
* A specialization of {@link StandardSessionIdGenerator} that initializes
|
||||
* {@code SecureRandom} lazily.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class LazySessionIdGenerator extends StandardSessionIdGenerator {
|
||||
|
||||
@Override
|
||||
protected void startInternal() throws LifecycleException {
|
||||
setState(LifecycleState.STARTING);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.catalina.connector.Connector;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.coyote.ProtocolHandler;
|
||||
import org.apache.coyote.http11.AbstractHttp11Protocol;
|
||||
import org.apache.tomcat.util.net.SSLHostConfig;
|
||||
import org.apache.tomcat.util.net.SSLHostConfigCertificate;
|
||||
import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type;
|
||||
|
||||
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.web.server.Ssl.ClientAuth;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Utility that configures SSL support on the given connector.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @author Cyril Dangerville
|
||||
* @author Moritz Halbritter
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class SslConnectorCustomizer {
|
||||
|
||||
private final Log logger;
|
||||
|
||||
private final ClientAuth clientAuth;
|
||||
|
||||
private final Connector connector;
|
||||
|
||||
public SslConnectorCustomizer(Log logger, Connector connector, ClientAuth clientAuth) {
|
||||
this.logger = logger;
|
||||
this.clientAuth = clientAuth;
|
||||
this.connector = connector;
|
||||
}
|
||||
|
||||
public void update(String serverName, SslBundle updatedSslBundle) {
|
||||
AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) this.connector.getProtocolHandler();
|
||||
String host = (serverName != null) ? serverName : protocol.getDefaultSSLHostConfigName();
|
||||
this.logger.debug("SSL Bundle for host " + host + " has been updated, reloading SSL configuration");
|
||||
addSslHostConfig(protocol, host, updatedSslBundle);
|
||||
}
|
||||
|
||||
public void customize(SslBundle sslBundle, Map<String, SslBundle> serverNameSslBundles) {
|
||||
ProtocolHandler handler = this.connector.getProtocolHandler();
|
||||
Assert.state(handler instanceof AbstractHttp11Protocol,
|
||||
"To use SSL, the connector's protocol handler must be an AbstractHttp11Protocol subclass");
|
||||
configureSsl((AbstractHttp11Protocol<?>) handler, sslBundle, serverNameSslBundles);
|
||||
this.connector.setScheme("https");
|
||||
this.connector.setSecure(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Tomcat's {@link AbstractHttp11Protocol} for SSL.
|
||||
* @param protocol the protocol
|
||||
* @param sslBundle the SSL bundle
|
||||
* @param serverNameSslBundles the SSL bundles for specific SNI host names
|
||||
*/
|
||||
private void configureSsl(AbstractHttp11Protocol<?> protocol, SslBundle sslBundle,
|
||||
Map<String, SslBundle> serverNameSslBundles) {
|
||||
protocol.setSSLEnabled(true);
|
||||
if (sslBundle != null) {
|
||||
addSslHostConfig(protocol, protocol.getDefaultSSLHostConfigName(), sslBundle);
|
||||
}
|
||||
serverNameSslBundles.forEach((serverName, bundle) -> addSslHostConfig(protocol, serverName, bundle));
|
||||
}
|
||||
|
||||
private void addSslHostConfig(AbstractHttp11Protocol<?> protocol, String serverName, SslBundle sslBundle) {
|
||||
SSLHostConfig sslHostConfig = new SSLHostConfig();
|
||||
sslHostConfig.setHostName(serverName);
|
||||
configureSslClientAuth(sslHostConfig);
|
||||
applySslBundle(protocol, sslHostConfig, sslBundle);
|
||||
protocol.addSslHostConfig(sslHostConfig, true);
|
||||
}
|
||||
|
||||
private void applySslBundle(AbstractHttp11Protocol<?> protocol, SSLHostConfig sslHostConfig, SslBundle sslBundle) {
|
||||
SslBundleKey key = sslBundle.getKey();
|
||||
SslStoreBundle stores = sslBundle.getStores();
|
||||
SslOptions options = sslBundle.getOptions();
|
||||
sslHostConfig.setSslProtocol(sslBundle.getProtocol());
|
||||
SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED);
|
||||
String keystorePassword = (stores.getKeyStorePassword() != null) ? stores.getKeyStorePassword() : "";
|
||||
certificate.setCertificateKeystorePassword(keystorePassword);
|
||||
if (key.getPassword() != null) {
|
||||
certificate.setCertificateKeyPassword(key.getPassword());
|
||||
}
|
||||
if (key.getAlias() != null) {
|
||||
certificate.setCertificateKeyAlias(key.getAlias());
|
||||
}
|
||||
sslHostConfig.addCertificate(certificate);
|
||||
if (options.getCiphers() != null) {
|
||||
String ciphers = StringUtils.arrayToCommaDelimitedString(options.getCiphers());
|
||||
sslHostConfig.setCiphers(ciphers);
|
||||
}
|
||||
configureSslStores(sslHostConfig, certificate, stores);
|
||||
configureEnabledProtocols(sslHostConfig, options);
|
||||
}
|
||||
|
||||
private void configureEnabledProtocols(SSLHostConfig sslHostConfig, SslOptions options) {
|
||||
if (options.getEnabledProtocols() != null) {
|
||||
String enabledProtocols = StringUtils.arrayToDelimitedString(options.getEnabledProtocols(), "+");
|
||||
sslHostConfig.setProtocols(enabledProtocols);
|
||||
}
|
||||
}
|
||||
|
||||
private void configureSslClientAuth(SSLHostConfig config) {
|
||||
config.setCertificateVerification(ClientAuth.map(this.clientAuth, "none", "optional", "required"));
|
||||
}
|
||||
|
||||
private void configureSslStores(SSLHostConfig sslHostConfig, SSLHostConfigCertificate certificate,
|
||||
SslStoreBundle stores) {
|
||||
try {
|
||||
if (stores.getKeyStore() != null) {
|
||||
certificate.setCertificateKeystore(stores.getKeyStore());
|
||||
}
|
||||
if (stores.getTrustStore() != null) {
|
||||
sslHostConfig.setTrustStore(stores.getTrustStore());
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Could not load store: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import org.apache.catalina.connector.Connector;
|
||||
|
||||
/**
|
||||
* Callback interface that can be used to customize a Tomcat {@link Connector}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 4.0.0
|
||||
* @see ConfigurableTomcatWebServerFactory
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TomcatConnectorCustomizer {
|
||||
|
||||
/**
|
||||
* Customize the connector.
|
||||
* @param connector the connector to customize
|
||||
*/
|
||||
void customize(Connector connector);
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import org.apache.catalina.Context;
|
||||
|
||||
import org.springframework.boot.web.server.servlet.tomcat.TomcatServletWebServerFactory;
|
||||
|
||||
/**
|
||||
* Callback interface that can be used to customize a Tomcat {@link Context}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 4.0.0
|
||||
* @see TomcatServletWebServerFactory
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TomcatContextCustomizer {
|
||||
|
||||
/**
|
||||
* Customize the context.
|
||||
* @param context the context to customize
|
||||
*/
|
||||
void customize(Context context);
|
||||
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import org.apache.catalina.Container;
|
||||
import org.apache.catalina.Manager;
|
||||
import org.apache.catalina.Wrapper;
|
||||
import org.apache.catalina.core.StandardContext;
|
||||
import org.apache.catalina.core.StandardWrapper;
|
||||
import org.apache.catalina.session.ManagerBase;
|
||||
|
||||
import org.springframework.boot.web.server.MimeMappings;
|
||||
import org.springframework.boot.web.server.WebServerException;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Tomcat {@link StandardContext} used by {@link TomcatWebServer} to support deferred
|
||||
* initialization.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class TomcatEmbeddedContext extends StandardContext {
|
||||
|
||||
private TomcatStarter starter;
|
||||
|
||||
private MimeMappings mimeMappings;
|
||||
|
||||
@Override
|
||||
public boolean loadOnStartup(Container[] children) {
|
||||
// deferred until later (see deferredLoadOnStartup)
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setManager(Manager manager) {
|
||||
if (manager instanceof ManagerBase) {
|
||||
manager.setSessionIdGenerator(new LazySessionIdGenerator());
|
||||
}
|
||||
super.setManager(manager);
|
||||
}
|
||||
|
||||
void deferredLoadOnStartup() {
|
||||
doWithThreadContextClassLoader(getLoader().getClassLoader(),
|
||||
() -> getLoadOnStartupWrappers(findChildren()).forEach(this::load));
|
||||
}
|
||||
|
||||
private Stream<Wrapper> getLoadOnStartupWrappers(Container[] children) {
|
||||
Map<Integer, List<Wrapper>> grouped = new TreeMap<>();
|
||||
for (Container child : children) {
|
||||
Wrapper wrapper = (Wrapper) child;
|
||||
int order = wrapper.getLoadOnStartup();
|
||||
if (order >= 0) {
|
||||
grouped.computeIfAbsent(order, (o) -> new ArrayList<>()).add(wrapper);
|
||||
}
|
||||
}
|
||||
return grouped.values().stream().flatMap(List::stream);
|
||||
}
|
||||
|
||||
private void load(Wrapper wrapper) {
|
||||
try {
|
||||
wrapper.load();
|
||||
}
|
||||
catch (ServletException ex) {
|
||||
String message = sm.getString("standardContext.loadOnStartup.loadException", getName(), wrapper.getName());
|
||||
if (getComputedFailCtxIfServletStartFails()) {
|
||||
throw new WebServerException(message, ex);
|
||||
}
|
||||
getLogger().error(message, StandardWrapper.getRootCause(ex));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Some older Servlet frameworks (e.g. Struts, BIRT) use the Thread context class
|
||||
* loader to create servlet instances in this phase. If they do that and then try to
|
||||
* initialize them later the class loader may have changed, so wrap the call to
|
||||
* loadOnStartup in what we think is going to be the main webapp classloader at
|
||||
* runtime.
|
||||
* @param classLoader the class loader to use
|
||||
* @param code the code to run
|
||||
*/
|
||||
private void doWithThreadContextClassLoader(ClassLoader classLoader, Runnable code) {
|
||||
ClassLoader existingLoader = (classLoader != null) ? ClassUtils.overrideThreadContextClassLoader(classLoader)
|
||||
: null;
|
||||
try {
|
||||
code.run();
|
||||
}
|
||||
finally {
|
||||
if (existingLoader != null) {
|
||||
ClassUtils.overrideThreadContextClassLoader(existingLoader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setStarter(TomcatStarter starter) {
|
||||
this.starter = starter;
|
||||
}
|
||||
|
||||
TomcatStarter getStarter() {
|
||||
return this.starter;
|
||||
}
|
||||
|
||||
public void setMimeMappings(MimeMappings mimeMappings) {
|
||||
this.mimeMappings = mimeMappings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] findMimeMappings() {
|
||||
List<String> mappings = new ArrayList<>(Arrays.asList(super.findMimeMappings()));
|
||||
if (this.mimeMappings != null) {
|
||||
this.mimeMappings.forEach((mapping) -> mappings.add(mapping.getExtension()));
|
||||
}
|
||||
return mappings.toArray(String[]::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String findMimeMapping(String extension) {
|
||||
String mimeMapping = super.findMimeMapping(extension);
|
||||
if (mimeMapping != null) {
|
||||
return mimeMapping;
|
||||
}
|
||||
return (this.mimeMappings != null) ? this.mimeMappings.get(extension) : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
|
||||
import org.apache.catalina.loader.ParallelWebappClassLoader;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.tomcat.util.compat.JreCompat;
|
||||
|
||||
/**
|
||||
* Extension of Tomcat's {@link ParallelWebappClassLoader} that does not consider the
|
||||
* {@link ClassLoader#getSystemClassLoader() system classloader}. This is required to
|
||||
* ensure that any custom context class loader is always used (as is the case with some
|
||||
* executable archives).
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Clement
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class TomcatEmbeddedWebappClassLoader extends ParallelWebappClassLoader {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(TomcatEmbeddedWebappClassLoader.class);
|
||||
|
||||
static {
|
||||
if (!JreCompat.isGraalAvailable()) {
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
}
|
||||
|
||||
public TomcatEmbeddedWebappClassLoader() {
|
||||
}
|
||||
|
||||
public TomcatEmbeddedWebappClassLoader(ClassLoader parent) {
|
||||
super(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL findResource(String name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<URL> findResources(String name) throws IOException {
|
||||
return Collections.emptyEnumeration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) {
|
||||
Class<?> result = findExistingLoadedClass(name);
|
||||
result = (result != null) ? result : doLoadClass(name);
|
||||
if (result == null) {
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
return resolveIfNecessary(result, resolve);
|
||||
}
|
||||
}
|
||||
|
||||
private Class<?> findExistingLoadedClass(String name) {
|
||||
Class<?> resultClass = findLoadedClass0(name);
|
||||
resultClass = (resultClass != null || JreCompat.isGraalAvailable()) ? resultClass : findLoadedClass(name);
|
||||
return resultClass;
|
||||
}
|
||||
|
||||
private Class<?> doLoadClass(String name) {
|
||||
if ((this.delegate || filter(name, true))) {
|
||||
Class<?> result = loadFromParent(name);
|
||||
return (result != null) ? result : findClassIgnoringNotFound(name);
|
||||
}
|
||||
Class<?> result = findClassIgnoringNotFound(name);
|
||||
return (result != null) ? result : loadFromParent(name);
|
||||
}
|
||||
|
||||
private Class<?> resolveIfNecessary(Class<?> resultClass, boolean resolve) {
|
||||
if (resolve) {
|
||||
resolveClass(resultClass);
|
||||
}
|
||||
return (resultClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addURL(URL url) {
|
||||
// Ignore URLs added by the Tomcat 8 implementation (see gh-919)
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Ignoring request to add " + url + " to the tomcat classloader");
|
||||
}
|
||||
}
|
||||
|
||||
private Class<?> loadFromParent(String name) {
|
||||
if (this.parent == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Class.forName(name, false, this.parent);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Class<?> findClassIgnoringNotFound(String name) {
|
||||
try {
|
||||
return findClass(name);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import org.apache.catalina.connector.Connector;
|
||||
import org.apache.coyote.ProtocolHandler;
|
||||
|
||||
/**
|
||||
* Callback interface that can be used to customize the {@link ProtocolHandler} on the
|
||||
* {@link Connector}.
|
||||
*
|
||||
* @param <T> specified type for customization based on {@link ProtocolHandler}
|
||||
* @author Pascal Zwick
|
||||
* @since 4.0.0
|
||||
* @see ConfigurableTomcatWebServerFactory
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TomcatProtocolHandlerCustomizer<T extends ProtocolHandler> {
|
||||
|
||||
/**
|
||||
* Customize the protocol handler.
|
||||
* @param protocolHandler the protocol handler to customize
|
||||
*/
|
||||
void customize(T protocolHandler);
|
||||
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.servlet.ServletContainerInitializer;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.ServletException;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.boot.web.server.servlet.ServletContextInitializer;
|
||||
|
||||
/**
|
||||
* {@link ServletContainerInitializer} used to trigger {@link ServletContextInitializer
|
||||
* ServletContextInitializers} and track startup errors.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class TomcatStarter implements ServletContainerInitializer {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(TomcatStarter.class);
|
||||
|
||||
private final Iterable<ServletContextInitializer> initializers;
|
||||
|
||||
private volatile Exception startUpException;
|
||||
|
||||
public TomcatStarter(Iterable<ServletContextInitializer> initializers) {
|
||||
this.initializers = initializers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
|
||||
try {
|
||||
for (ServletContextInitializer initializer : this.initializers) {
|
||||
initializer.onStartup(servletContext);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
this.startUpException = ex;
|
||||
// Prevent Tomcat from logging and re-throwing when we know we can
|
||||
// deal with it in the main thread, but log for information here.
|
||||
if (logger.isErrorEnabled()) {
|
||||
logger.error("Error starting Tomcat context. Exception: " + ex.getClass().getName() + ". Message: "
|
||||
+ ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Exception getStartUpException() {
|
||||
return this.startUpException;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,463 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.naming.NamingException;
|
||||
|
||||
import org.apache.catalina.Container;
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.Engine;
|
||||
import org.apache.catalina.Lifecycle;
|
||||
import org.apache.catalina.LifecycleException;
|
||||
import org.apache.catalina.LifecycleState;
|
||||
import org.apache.catalina.Service;
|
||||
import org.apache.catalina.Wrapper;
|
||||
import org.apache.catalina.connector.Connector;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.naming.ContextBindings;
|
||||
|
||||
import org.springframework.boot.web.server.GracefulShutdownCallback;
|
||||
import org.springframework.boot.web.server.GracefulShutdownResult;
|
||||
import org.springframework.boot.web.server.PortInUseException;
|
||||
import org.springframework.boot.web.server.Shutdown;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.server.WebServerException;
|
||||
import org.springframework.boot.web.server.reactive.tomcat.TomcatReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.tomcat.TomcatServletWebServerFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link WebServer} that can be used to control a Tomcat web server. Usually this class
|
||||
* should be created using the {@link TomcatReactiveWebServerFactory} or
|
||||
* {@link TomcatServletWebServerFactory}, but not directly.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Kristine Jetzke
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class TomcatWebServer implements WebServer {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(TomcatWebServer.class);
|
||||
|
||||
private static final AtomicInteger containerCounter = new AtomicInteger(-1);
|
||||
|
||||
private final Object monitor = new Object();
|
||||
|
||||
private final Map<Service, Connector[]> serviceConnectors = new HashMap<>();
|
||||
|
||||
private final Tomcat tomcat;
|
||||
|
||||
private final boolean autoStart;
|
||||
|
||||
private final GracefulShutdown gracefulShutdown;
|
||||
|
||||
private volatile boolean started;
|
||||
|
||||
/**
|
||||
* Create a new {@link TomcatWebServer} instance.
|
||||
* @param tomcat the underlying Tomcat server
|
||||
*/
|
||||
public TomcatWebServer(Tomcat tomcat) {
|
||||
this(tomcat, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link TomcatWebServer} instance.
|
||||
* @param tomcat the underlying Tomcat server
|
||||
* @param autoStart if the server should be started
|
||||
*/
|
||||
public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
|
||||
this(tomcat, autoStart, Shutdown.IMMEDIATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link TomcatWebServer} instance.
|
||||
* @param tomcat the underlying Tomcat server
|
||||
* @param autoStart if the server should be started
|
||||
* @param shutdown type of shutdown supported by the server
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
|
||||
Assert.notNull(tomcat, "'tomcat' must not be null");
|
||||
this.tomcat = tomcat;
|
||||
this.autoStart = autoStart;
|
||||
this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() throws WebServerException {
|
||||
logger.info("Tomcat initialized with " + getPortsDescription(false));
|
||||
synchronized (this.monitor) {
|
||||
try {
|
||||
addInstanceIdToEngineName();
|
||||
|
||||
Context context = findContext();
|
||||
context.addLifecycleListener((event) -> {
|
||||
if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
|
||||
// Remove service connectors so that protocol binding doesn't
|
||||
// happen when the service is started.
|
||||
removeServiceConnectors();
|
||||
}
|
||||
});
|
||||
|
||||
disableBindOnInit();
|
||||
|
||||
// Start the server to trigger initialization listeners
|
||||
this.tomcat.start();
|
||||
|
||||
// We can re-throw failure exception directly in the main thread
|
||||
rethrowDeferredStartupExceptions();
|
||||
|
||||
try {
|
||||
ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
|
||||
}
|
||||
catch (NamingException ex) {
|
||||
// Naming is not enabled. Continue
|
||||
}
|
||||
|
||||
// Unlike Jetty, all Tomcat threads are daemon threads. We create a
|
||||
// blocking non-daemon to stop immediate shutdown
|
||||
startNonDaemonAwaitThread();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
stopSilently();
|
||||
destroySilently();
|
||||
throw new WebServerException("Unable to start embedded Tomcat", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Context findContext() {
|
||||
for (Container child : this.tomcat.getHost().findChildren()) {
|
||||
if (child instanceof Context context) {
|
||||
return context;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("The host does not contain a Context");
|
||||
}
|
||||
|
||||
private void addInstanceIdToEngineName() {
|
||||
int instanceId = containerCounter.incrementAndGet();
|
||||
if (instanceId > 0) {
|
||||
Engine engine = this.tomcat.getEngine();
|
||||
engine.setName(engine.getName() + "-" + instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeServiceConnectors() {
|
||||
doWithConnectors((service, connectors) -> {
|
||||
this.serviceConnectors.put(service, connectors);
|
||||
for (Connector connector : connectors) {
|
||||
service.removeConnector(connector);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void disableBindOnInit() {
|
||||
doWithConnectors((service, connectors) -> {
|
||||
for (Connector connector : connectors) {
|
||||
Object bindOnInit = connector.getProperty("bindOnInit");
|
||||
if (bindOnInit == null) {
|
||||
connector.setProperty("bindOnInit", "false");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void doWithConnectors(BiConsumer<Service, Connector[]> consumer) {
|
||||
for (Service service : this.tomcat.getServer().findServices()) {
|
||||
Connector[] connectors = service.findConnectors().clone();
|
||||
consumer.accept(service, connectors);
|
||||
}
|
||||
}
|
||||
|
||||
private void rethrowDeferredStartupExceptions() throws Exception {
|
||||
Container[] children = this.tomcat.getHost().findChildren();
|
||||
for (Container container : children) {
|
||||
if (container instanceof TomcatEmbeddedContext embeddedContext) {
|
||||
TomcatStarter tomcatStarter = embeddedContext.getStarter();
|
||||
if (tomcatStarter != null) {
|
||||
Exception exception = tomcatStarter.getStartUpException();
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!LifecycleState.STARTED.equals(container.getState())) {
|
||||
throw new IllegalStateException(container + " failed to start");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startNonDaemonAwaitThread() {
|
||||
Thread awaitThread = new Thread("container-" + (containerCounter.get())) {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
TomcatWebServer.this.tomcat.getServer().await();
|
||||
}
|
||||
|
||||
};
|
||||
awaitThread.setContextClassLoader(getClass().getClassLoader());
|
||||
awaitThread.setDaemon(false);
|
||||
awaitThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws WebServerException {
|
||||
synchronized (this.monitor) {
|
||||
if (this.started) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
addPreviouslyRemovedConnectors();
|
||||
Connector connector = this.tomcat.getConnector();
|
||||
if (connector != null && this.autoStart) {
|
||||
performDeferredLoadOnStartup();
|
||||
}
|
||||
checkThatConnectorsHaveStarted();
|
||||
this.started = true;
|
||||
logger.info(getStartedLogMessage());
|
||||
}
|
||||
catch (ConnectorStartFailedException ex) {
|
||||
stopSilently();
|
||||
throw ex;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
PortInUseException.throwIfPortBindingException(ex, () -> this.tomcat.getConnector().getPort());
|
||||
throw new WebServerException("Unable to start embedded Tomcat server", ex);
|
||||
}
|
||||
finally {
|
||||
Context context = findContext();
|
||||
ContextBindings.unbindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String getStartedLogMessage() {
|
||||
String contextPath = getContextPath();
|
||||
return "Tomcat started on " + getPortsDescription(true)
|
||||
+ ((contextPath != null) ? " with context path '" + contextPath + "'" : "");
|
||||
}
|
||||
|
||||
private void checkThatConnectorsHaveStarted() {
|
||||
checkConnectorHasStarted(this.tomcat.getConnector());
|
||||
for (Connector connector : this.tomcat.getService().findConnectors()) {
|
||||
checkConnectorHasStarted(connector);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkConnectorHasStarted(Connector connector) {
|
||||
if (LifecycleState.FAILED.equals(connector.getState())) {
|
||||
throw new ConnectorStartFailedException(connector.getPort());
|
||||
}
|
||||
}
|
||||
|
||||
private void stopSilently() {
|
||||
try {
|
||||
stopTomcat();
|
||||
}
|
||||
catch (LifecycleException ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
private void destroySilently() {
|
||||
try {
|
||||
this.tomcat.destroy();
|
||||
}
|
||||
catch (LifecycleException ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
private void stopTomcat() throws LifecycleException {
|
||||
if (Thread.currentThread().getContextClassLoader() instanceof TomcatEmbeddedWebappClassLoader) {
|
||||
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
|
||||
}
|
||||
this.tomcat.stop();
|
||||
}
|
||||
|
||||
private void addPreviouslyRemovedConnectors() {
|
||||
Service[] services = this.tomcat.getServer().findServices();
|
||||
for (Service service : services) {
|
||||
Connector[] connectors = this.serviceConnectors.get(service);
|
||||
if (connectors != null) {
|
||||
for (Connector connector : connectors) {
|
||||
service.addConnector(connector);
|
||||
if (!this.autoStart) {
|
||||
stopProtocolHandler(connector);
|
||||
}
|
||||
}
|
||||
this.serviceConnectors.remove(service);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void stopProtocolHandler(Connector connector) {
|
||||
try {
|
||||
connector.getProtocolHandler().stop();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
logger.error("Cannot pause connector: ", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void performDeferredLoadOnStartup() {
|
||||
try {
|
||||
for (Container child : this.tomcat.getHost().findChildren()) {
|
||||
if (child instanceof TomcatEmbeddedContext embeddedContext) {
|
||||
embeddedContext.deferredLoadOnStartup();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
if (ex instanceof WebServerException webServerException) {
|
||||
throw webServerException;
|
||||
}
|
||||
throw new WebServerException("Unable to start embedded Tomcat connectors", ex);
|
||||
}
|
||||
}
|
||||
|
||||
Map<Service, Connector[]> getServiceConnectors() {
|
||||
return this.serviceConnectors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws WebServerException {
|
||||
synchronized (this.monitor) {
|
||||
boolean wasStarted = this.started;
|
||||
try {
|
||||
this.started = false;
|
||||
if (this.gracefulShutdown != null) {
|
||||
this.gracefulShutdown.abort();
|
||||
}
|
||||
removeServiceConnectors();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new WebServerException("Unable to stop embedded Tomcat", ex);
|
||||
}
|
||||
finally {
|
||||
if (wasStarted) {
|
||||
containerCounter.decrementAndGet();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws WebServerException {
|
||||
try {
|
||||
stopTomcat();
|
||||
this.tomcat.destroy();
|
||||
}
|
||||
catch (LifecycleException ex) {
|
||||
// Swallow and continue
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new WebServerException("Unable to destroy embedded Tomcat", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String getPortsDescription(boolean localPort) {
|
||||
StringBuilder description = new StringBuilder();
|
||||
Connector[] connectors = this.tomcat.getService().findConnectors();
|
||||
description.append("port");
|
||||
if (connectors.length != 1) {
|
||||
description.append("s");
|
||||
}
|
||||
description.append(" ");
|
||||
for (int i = 0; i < connectors.length; i++) {
|
||||
if (i != 0) {
|
||||
description.append(", ");
|
||||
}
|
||||
Connector connector = connectors[i];
|
||||
int port = localPort ? connector.getLocalPort() : connector.getPort();
|
||||
description.append(port).append(" (").append(connector.getScheme()).append(')');
|
||||
}
|
||||
return description.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPort() {
|
||||
Connector connector = this.tomcat.getConnector();
|
||||
if (connector != null) {
|
||||
return connector.getLocalPort();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private String getContextPath() {
|
||||
String contextPath = Arrays.stream(this.tomcat.getHost().findChildren())
|
||||
.filter(TomcatEmbeddedContext.class::isInstance)
|
||||
.map(TomcatEmbeddedContext.class::cast)
|
||||
.filter(this::imperative)
|
||||
.map(TomcatEmbeddedContext::getPath)
|
||||
.map((path) -> path.isEmpty() ? "/" : path)
|
||||
.collect(Collectors.joining(" "));
|
||||
return StringUtils.hasText(contextPath) ? contextPath : null;
|
||||
}
|
||||
|
||||
private boolean imperative(TomcatEmbeddedContext context) {
|
||||
for (Container container : context.findChildren()) {
|
||||
if (container instanceof Wrapper wrapper) {
|
||||
if (wrapper.getServletClass()
|
||||
.equals("org.springframework.http.server.reactive.TomcatHttpHandlerAdapter")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns access to the underlying Tomcat server.
|
||||
* @return the Tomcat server
|
||||
*/
|
||||
public Tomcat getTomcat() {
|
||||
return this.tomcat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a graceful shutdown of the Tomcat 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}.
|
||||
* <p>
|
||||
* Once shutdown has been initiated Tomcat will reject any new connections. Requests
|
||||
* on existing idle connections will also be rejected.
|
||||
*/
|
||||
@Override
|
||||
public void shutDownGracefully(GracefulShutdownCallback callback) {
|
||||
if (this.gracefulShutdown == null) {
|
||||
callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
|
||||
return;
|
||||
}
|
||||
this.gracefulShutdown.shutDownGracefully(callback);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,461 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.Engine;
|
||||
import org.apache.catalina.Executor;
|
||||
import org.apache.catalina.LifecycleListener;
|
||||
import org.apache.catalina.Valve;
|
||||
import org.apache.catalina.connector.Connector;
|
||||
import org.apache.catalina.core.AprLifecycleListener;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.coyote.AbstractProtocol;
|
||||
import org.apache.coyote.ProtocolHandler;
|
||||
import org.apache.coyote.http2.Http2Protocol;
|
||||
import org.apache.tomcat.util.modeler.Registry;
|
||||
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.core.NativeDetector;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Base class for factories that produce a {@link TomcatWebServer}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class TomcatWebServerFactory extends AbstractConfigurableWebServerFactory
|
||||
implements ConfigurableTomcatWebServerFactory {
|
||||
|
||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
/**
|
||||
* The class name of default protocol used.
|
||||
*/
|
||||
public static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol";
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private File baseDirectory;
|
||||
|
||||
private int backgroundProcessorDelay;
|
||||
|
||||
private List<Valve> engineValves = new ArrayList<>();
|
||||
|
||||
private List<Valve> contextValves = new ArrayList<>();
|
||||
|
||||
private List<LifecycleListener> contextLifecycleListeners = new ArrayList<>();
|
||||
|
||||
private Set<TomcatContextCustomizer> contextCustomizers = new LinkedHashSet<>();
|
||||
|
||||
private Set<TomcatConnectorCustomizer> connectorCustomizers = new LinkedHashSet<>();
|
||||
|
||||
private Set<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers = new LinkedHashSet<>();
|
||||
|
||||
private List<Connector> additionalConnectors = new ArrayList<>();
|
||||
|
||||
private Charset uriEncoding = DEFAULT_CHARSET;
|
||||
|
||||
private String protocol = DEFAULT_PROTOCOL;
|
||||
|
||||
private boolean disableMBeanRegistry = true;
|
||||
|
||||
private boolean useApr;
|
||||
|
||||
protected TomcatWebServerFactory() {
|
||||
}
|
||||
|
||||
protected TomcatWebServerFactory(int port) {
|
||||
super(port);
|
||||
}
|
||||
|
||||
private List<LifecycleListener> getDefaultServerLifecycleListeners() {
|
||||
ArrayList<LifecycleListener> lifecycleListeners = new ArrayList<>();
|
||||
if (this.useApr && !NativeDetector.inNativeImage()) {
|
||||
lifecycleListeners.add(new AprLifecycleListener());
|
||||
}
|
||||
return lifecycleListeners;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBaseDirectory(File baseDirectory) {
|
||||
this.baseDirectory = baseDirectory;
|
||||
}
|
||||
|
||||
public File getBaseDirectory() {
|
||||
return this.baseDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mutable collection of the {@link Valve}s that will be applied to the
|
||||
* Tomcat {@link Engine}.
|
||||
* @return the engine valves that will be applied
|
||||
*/
|
||||
public Collection<Valve> getEngineValves() {
|
||||
return this.engineValves;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set {@link Valve}s that should be applied to the Tomcat {@link Engine}. Calling
|
||||
* this method will replace any existing valves.
|
||||
* @param engineValves the valves to set
|
||||
*/
|
||||
public void setEngineValves(Collection<? extends Valve> engineValves) {
|
||||
Assert.notNull(engineValves, "'engineValves' must not be null");
|
||||
this.engineValves = new ArrayList<>(engineValves);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addEngineValves(Valve... engineValves) {
|
||||
Assert.notNull(engineValves, "'engineValves' must not be null");
|
||||
this.engineValves.addAll(Arrays.asList(engineValves));
|
||||
}
|
||||
|
||||
public Charset getUriEncoding() {
|
||||
return this.uriEncoding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUriEncoding(Charset uriEncoding) {
|
||||
this.uriEncoding = uriEncoding;
|
||||
}
|
||||
|
||||
public int getBackgroundProcessorDelay() {
|
||||
return this.backgroundProcessorDelay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBackgroundProcessorDelay(int delay) {
|
||||
this.backgroundProcessorDelay = delay;
|
||||
}
|
||||
|
||||
public String getProtocol() {
|
||||
return this.protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Tomcat protocol to use when create the {@link Connector}.
|
||||
* @param protocol the protocol
|
||||
* @see Connector#Connector(String)
|
||||
*/
|
||||
public void setProtocol(String protocol) {
|
||||
Assert.hasLength(protocol, "'protocol' must not be empty");
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mutable collection of the {@link Valve}s that will be applied to the
|
||||
* Tomcat {@link Context}.
|
||||
* @return the context valves that will be applied
|
||||
* @see #getEngineValves()
|
||||
*/
|
||||
public Collection<Valve> getContextValves() {
|
||||
return this.contextValves;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set {@link Valve}s that should be applied to the Tomcat {@link Context}. Calling
|
||||
* this method will replace any existing valves.
|
||||
* @param contextValves the valves to set
|
||||
*/
|
||||
public void setContextValves(Collection<? extends Valve> contextValves) {
|
||||
Assert.notNull(contextValves, "'contextValves' must not be null");
|
||||
this.contextValves = new ArrayList<>(contextValves);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add {@link Valve}s that should be applied to the Tomcat {@link Context}.
|
||||
* @param contextValves the valves to add
|
||||
*/
|
||||
public void addContextValves(Valve... contextValves) {
|
||||
Assert.notNull(contextValves, "'contextValves' must not be null");
|
||||
this.contextValves.addAll(Arrays.asList(contextValves));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mutable collection of the {@link LifecycleListener}s that will be applied
|
||||
* to the Tomcat {@link Context}.
|
||||
* @return the context lifecycle listeners that will be applied
|
||||
*/
|
||||
public Collection<LifecycleListener> getContextLifecycleListeners() {
|
||||
return this.contextLifecycleListeners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set {@link LifecycleListener}s that should be applied to the Tomcat
|
||||
* {@link Context}. Calling this method will replace any existing listeners.
|
||||
* @param contextLifecycleListeners the listeners to set
|
||||
*/
|
||||
public void setContextLifecycleListeners(Collection<? extends LifecycleListener> contextLifecycleListeners) {
|
||||
Assert.notNull(contextLifecycleListeners, "'contextLifecycleListeners' must not be null");
|
||||
this.contextLifecycleListeners = new ArrayList<>(contextLifecycleListeners);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add {@link LifecycleListener}s that should be added to the Tomcat {@link Context}.
|
||||
* @param contextLifecycleListeners the listeners to add
|
||||
*/
|
||||
public void addContextLifecycleListeners(LifecycleListener... contextLifecycleListeners) {
|
||||
Assert.notNull(contextLifecycleListeners, "'contextLifecycleListeners' must not be null");
|
||||
this.contextLifecycleListeners.addAll(Arrays.asList(contextLifecycleListeners));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mutable collection of the {@link TomcatContextCustomizer}s that will be
|
||||
* applied to the Tomcat {@link Context}.
|
||||
* @return the listeners that will be applied
|
||||
*/
|
||||
public Collection<TomcatContextCustomizer> getContextCustomizers() {
|
||||
return this.contextCustomizers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set {@link TomcatContextCustomizer}s that should be applied to the Tomcat
|
||||
* {@link Context}. Calling this method will replace any existing customizers.
|
||||
* @param contextCustomizers the customizers to set
|
||||
*/
|
||||
public void setContextCustomizers(Collection<? extends TomcatContextCustomizer> contextCustomizers) {
|
||||
Assert.notNull(contextCustomizers, "'contextCustomizers' must not be null");
|
||||
this.contextCustomizers = new LinkedHashSet<>(contextCustomizers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addContextCustomizers(TomcatContextCustomizer... contextCustomizers) {
|
||||
Assert.notNull(contextCustomizers, "'contextCustomizers' must not be null");
|
||||
this.contextCustomizers.addAll(Arrays.asList(contextCustomizers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mutable collection of the {@link TomcatConnectorCustomizer}s that will be
|
||||
* applied to the Tomcat {@link Connector}.
|
||||
* @return the customizers that will be applied
|
||||
*/
|
||||
public Set<TomcatConnectorCustomizer> getConnectorCustomizers() {
|
||||
return this.connectorCustomizers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set {@link TomcatConnectorCustomizer}s that should be applied to the Tomcat
|
||||
* {@link Connector}. Calling this method will replace any existing customizers.
|
||||
* @param connectorCustomizers the customizers to set
|
||||
*/
|
||||
public void setConnectorCustomizers(Collection<? extends TomcatConnectorCustomizer> connectorCustomizers) {
|
||||
Assert.notNull(connectorCustomizers, "'connectorCustomizers' must not be null");
|
||||
this.connectorCustomizers = new LinkedHashSet<>(connectorCustomizers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addConnectorCustomizers(TomcatConnectorCustomizer... connectorCustomizers) {
|
||||
Assert.notNull(connectorCustomizers, "'connectorCustomizers' must not be null");
|
||||
this.connectorCustomizers.addAll(Arrays.asList(connectorCustomizers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mutable collection of the {@link TomcatProtocolHandlerCustomizer}s that
|
||||
* will be applied to the Tomcat {@link Connector}.
|
||||
* @return the customizers that will be applied
|
||||
*/
|
||||
public Set<TomcatProtocolHandlerCustomizer<?>> getProtocolHandlerCustomizers() {
|
||||
return this.protocolHandlerCustomizers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set {@link TomcatProtocolHandlerCustomizer}s that should be applied to the Tomcat
|
||||
* {@link Connector}. Calling this method will replace any existing customizers.
|
||||
* @param protocolHandlerCustomizers the customizers to set
|
||||
*/
|
||||
public void setProtocolHandlerCustomizers(
|
||||
Collection<? extends TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
|
||||
Assert.notNull(protocolHandlerCustomizers, "'protocolHandlerCustomizers' must not be null");
|
||||
this.protocolHandlerCustomizers = new LinkedHashSet<>(protocolHandlerCustomizers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addProtocolHandlerCustomizers(TomcatProtocolHandlerCustomizer<?>... protocolHandlerCustomizers) {
|
||||
Assert.notNull(protocolHandlerCustomizers, "'protocolHandlerCustomizers' must not be null");
|
||||
this.protocolHandlerCustomizers.addAll(Arrays.asList(protocolHandlerCustomizers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mutable collection of the {@link Connector}s that will be added to the
|
||||
* Tomcat server.
|
||||
* @return the additional connectors
|
||||
*/
|
||||
public List<Connector> getAdditionalConnectors() {
|
||||
return this.additionalConnectors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set additional {@link Connector}s that should be added to the Tomcat server .
|
||||
* Calling this method will replace any existing additional connectors.
|
||||
* @param additionalConnectors the additionalConnectors to set
|
||||
*/
|
||||
public void setAdditionalConnectors(Collection<? extends Connector> additionalConnectors) {
|
||||
Assert.notNull(additionalConnectors, "'additionalConnectors' must not be null");
|
||||
this.additionalConnectors = new ArrayList<>(additionalConnectors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add {@link Connector}s in addition to the default connector, e.g. for SSL or AJP.
|
||||
* <p>
|
||||
* {@link #getConnectorCustomizers Connector customizers} are not applied to
|
||||
* connectors added this way.
|
||||
* @param connectors the connectors to add
|
||||
*/
|
||||
public void addAdditionalConnectors(Connector... connectors) {
|
||||
Assert.notNull(connectors, "'connectors' must not be null");
|
||||
this.additionalConnectors.addAll(Arrays.asList(connectors));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the factory should disable Tomcat's MBean registry prior to
|
||||
* creating the server.
|
||||
* @return whether to disable Tomcat's MBean registry
|
||||
*/
|
||||
public boolean isDisableMBeanRegistry() {
|
||||
return this.disableMBeanRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the factory should disable Tomcat's MBean registry prior to creating
|
||||
* the server.
|
||||
* @param disableMBeanRegistry whether to disable the MBean registry
|
||||
*/
|
||||
public void setDisableMBeanRegistry(boolean disableMBeanRegistry) {
|
||||
this.disableMBeanRegistry = disableMBeanRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to use APR.
|
||||
* @param useApr whether to use APR
|
||||
*/
|
||||
@Override
|
||||
public void setUseApr(boolean useApr) {
|
||||
this.useApr = useApr;
|
||||
}
|
||||
|
||||
protected Tomcat createTomcat() {
|
||||
if (this.isDisableMBeanRegistry()) {
|
||||
Registry.disableRegistry();
|
||||
}
|
||||
Tomcat tomcat = new Tomcat();
|
||||
File baseDir = (getBaseDirectory() != null) ? getBaseDirectory() : createTempDir("tomcat");
|
||||
tomcat.setBaseDir(baseDir.getAbsolutePath());
|
||||
for (LifecycleListener listener : getDefaultServerLifecycleListeners()) {
|
||||
tomcat.getServer().addLifecycleListener(listener);
|
||||
}
|
||||
Connector connector = new Connector(getProtocol());
|
||||
connector.setThrowOnFailure(true);
|
||||
tomcat.getService().addConnector(connector);
|
||||
customizeConnector(connector);
|
||||
tomcat.setConnector(connector);
|
||||
registerConnectorExecutor(tomcat, connector);
|
||||
tomcat.getHost().setAutoDeploy(false);
|
||||
configureEngine(tomcat.getEngine());
|
||||
for (Connector additionalConnector : this.getAdditionalConnectors()) {
|
||||
tomcat.getService().addConnector(additionalConnector);
|
||||
registerConnectorExecutor(tomcat, additionalConnector);
|
||||
}
|
||||
return tomcat;
|
||||
}
|
||||
|
||||
protected void customizeConnector(Connector connector) {
|
||||
int port = Math.max(getPort(), 0);
|
||||
connector.setPort(port);
|
||||
if (StringUtils.hasText(getServerHeader())) {
|
||||
connector.setProperty("server", getServerHeader());
|
||||
}
|
||||
if (connector.getProtocolHandler() instanceof AbstractProtocol<?> abstractProtocol) {
|
||||
customizeProtocol(abstractProtocol);
|
||||
}
|
||||
invokeProtocolHandlerCustomizers(connector.getProtocolHandler());
|
||||
if (getUriEncoding() != null) {
|
||||
connector.setURIEncoding(getUriEncoding().name());
|
||||
}
|
||||
if (getHttp2() != null && getHttp2().isEnabled()) {
|
||||
connector.addUpgradeProtocol(new Http2Protocol());
|
||||
}
|
||||
if (Ssl.isEnabled(getSsl())) {
|
||||
customizeSsl(connector);
|
||||
}
|
||||
TomcatConnectorCustomizer compression = new CompressionConnectorCustomizer(getCompression());
|
||||
compression.customize(connector);
|
||||
for (TomcatConnectorCustomizer customizer : this.getConnectorCustomizers()) {
|
||||
customizer.customize(connector);
|
||||
}
|
||||
}
|
||||
|
||||
private void customizeProtocol(AbstractProtocol<?> protocol) {
|
||||
if (getAddress() != null) {
|
||||
protocol.setAddress(getAddress());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void invokeProtocolHandlerCustomizers(ProtocolHandler protocolHandler) {
|
||||
LambdaSafe
|
||||
.callbacks(TomcatProtocolHandlerCustomizer.class, this.getProtocolHandlerCustomizers(), protocolHandler)
|
||||
.invoke((customizer) -> customizer.customize(protocolHandler));
|
||||
}
|
||||
|
||||
private void customizeSsl(Connector connector) {
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector,
|
||||
getSsl().getClientAuth());
|
||||
customizer.customize(getSslBundle(), getServerNameSslBundles());
|
||||
addBundleUpdateHandler(null, getSsl().getBundle(), customizer);
|
||||
getSsl().getServerNameBundles()
|
||||
.forEach((serverNameSslBundle) -> addBundleUpdateHandler(serverNameSslBundle.serverName(),
|
||||
serverNameSslBundle.bundle(), customizer));
|
||||
}
|
||||
|
||||
private void addBundleUpdateHandler(String serverName, String sslBundleName, SslConnectorCustomizer customizer) {
|
||||
if (StringUtils.hasText(sslBundleName)) {
|
||||
getSslBundles().addBundleUpdateHandler(sslBundleName,
|
||||
(sslBundle) -> customizer.update(serverName, sslBundle));
|
||||
}
|
||||
}
|
||||
|
||||
private void registerConnectorExecutor(Tomcat tomcat, Connector connector) {
|
||||
if (connector.getProtocolHandler().getExecutor() instanceof Executor executor) {
|
||||
tomcat.getService().addExecutor(executor);
|
||||
}
|
||||
}
|
||||
|
||||
private void configureEngine(Engine engine) {
|
||||
engine.setBackgroundProcessorDelay(getBackgroundProcessorDelay());
|
||||
for (Valve valve : getEngineValves()) {
|
||||
engine.getPipeline().addValve(valve);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* 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 and servlet web server implementations backed by Tomcat.
|
||||
*
|
||||
* @see org.springframework.boot.web.server.servlet.tomcat.TomcatServletWebServerFactory
|
||||
* @see org.springframework.boot.web.server.reactive.tomcat.TomcatReactiveWebServerFactory
|
||||
*/
|
||||
package org.springframework.boot.web.server.tomcat;
|
||||
@@ -8,8 +8,7 @@ org.springframework.boot.reactor.ReactorEnvironmentPostProcessor
|
||||
|
||||
# Failure Analyzers
|
||||
org.springframework.boot.diagnostics.FailureAnalyzer=\
|
||||
org.springframework.boot.liquibase.LiquibaseChangelogMissingFailureAnalyzer,\
|
||||
org.springframework.boot.web.server.tomcat.ConnectorStartFailureAnalyzer
|
||||
org.springframework.boot.liquibase.LiquibaseChangelogMissingFailureAnalyzer
|
||||
|
||||
# Database Initializer Detectors
|
||||
org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\
|
||||
|
||||
@@ -41,10 +41,10 @@ import org.springframework.boot.ssl.jks.JksSslStoreBundle;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories;
|
||||
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.Ssl.ClientAuth;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.server.servlet.tomcat.TomcatServletWebServerFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
|
||||
@@ -41,10 +41,10 @@ import org.springframework.boot.ssl.jks.JksSslStoreBundle;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories;
|
||||
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.Ssl.ClientAuth;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.server.servlet.tomcat.TomcatServletWebServerFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
|
||||
@@ -38,10 +38,10 @@ import org.springframework.boot.ssl.jks.JksSslStoreBundle;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories;
|
||||
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.Ssl.ClientAuth;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.server.servlet.tomcat.TomcatServletWebServerFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.net.ConnectException;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.LifecycleEvent;
|
||||
import org.apache.catalina.LifecycleListener;
|
||||
import org.apache.catalina.Service;
|
||||
import org.apache.catalina.connector.Connector;
|
||||
import org.apache.catalina.core.AprLifecycleListener;
|
||||
import org.apache.catalina.core.StandardContext;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.catalina.valves.RemoteIpValve;
|
||||
import org.apache.coyote.ProtocolHandler;
|
||||
import org.apache.coyote.http11.AbstractHttp11Protocol;
|
||||
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.boot.web.server.PortInUseException;
|
||||
import org.springframework.boot.web.server.Shutdown;
|
||||
import org.springframework.boot.web.server.WebServerException;
|
||||
import org.springframework.boot.web.server.reactive.AbstractReactiveWebServerFactoryTests;
|
||||
import org.springframework.boot.web.server.reactive.ConfigurableReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatAccess;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatConnectorCustomizer;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatContextCustomizer;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatProtocolHandlerCustomizer;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatWebServer;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.assertArg;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link TomcatReactiveWebServerFactory}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Madhura Bhave
|
||||
* @author HaiTao Zhang
|
||||
*/
|
||||
class TomcatReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactoryTests {
|
||||
|
||||
@Override
|
||||
protected TomcatReactiveWebServerFactory getFactory() {
|
||||
return new TomcatReactiveWebServerFactory(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tomcatCustomizers() {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
TomcatContextCustomizer[] customizers = new TomcatContextCustomizer[4];
|
||||
Arrays.setAll(customizers, (i) -> mock(TomcatContextCustomizer.class));
|
||||
factory.setContextCustomizers(Arrays.asList(customizers[0], customizers[1]));
|
||||
factory.addContextCustomizers(customizers[2], customizers[3]);
|
||||
this.webServer = factory.getWebServer(mock(HttpHandler.class));
|
||||
InOrder ordered = inOrder((Object[]) customizers);
|
||||
for (TomcatContextCustomizer customizer : customizers) {
|
||||
then(customizer).should(ordered).customize(any(Context.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void contextIsAddedToHostBeforeCustomizersAreCalled() {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
TomcatContextCustomizer customizer = mock(TomcatContextCustomizer.class);
|
||||
factory.addContextCustomizers(customizer);
|
||||
this.webServer = factory.getWebServer(mock(HttpHandler.class));
|
||||
then(customizer).should().customize(assertArg((context) -> assertThat(context.getParent()).isNotNull()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultTomcatListeners() {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
assertThat(factory.getContextLifecycleListeners()).isEmpty();
|
||||
TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer(mock(HttpHandler.class));
|
||||
this.webServer = tomcatWebServer;
|
||||
assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void aprShouldBeOptIn() {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
factory.setUseApr(true);
|
||||
TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer(mock(HttpHandler.class));
|
||||
this.webServer = tomcatWebServer;
|
||||
assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).singleElement()
|
||||
.isInstanceOf(AprLifecycleListener.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tomcatListeners() {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
LifecycleListener[] listeners = new LifecycleListener[4];
|
||||
Arrays.setAll(listeners, (i) -> mock(LifecycleListener.class));
|
||||
factory.setContextLifecycleListeners(Arrays.asList(listeners[0], listeners[1]));
|
||||
factory.addContextLifecycleListeners(listeners[2], listeners[3]);
|
||||
this.webServer = factory.getWebServer(mock(HttpHandler.class));
|
||||
InOrder ordered = inOrder((Object[]) listeners);
|
||||
for (LifecycleListener listener : listeners) {
|
||||
then(listener).should(ordered).lifecycleEvent(any(LifecycleEvent.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void setNullConnectorCustomizersShouldThrowException() {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> factory.setConnectorCustomizers(null))
|
||||
.withMessageContaining("'connectorCustomizers' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void addNullAddConnectorCustomizersShouldThrowException() {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> factory.addConnectorCustomizers((TomcatConnectorCustomizer[]) null))
|
||||
.withMessageContaining("'connectorCustomizers' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setNullProtocolHandlerCustomizersShouldThrowException() {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> factory.setProtocolHandlerCustomizers(null))
|
||||
.withMessageContaining("'protocolHandlerCustomizers' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void addNullProtocolHandlerCustomizersShouldThrowException() {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> factory.addProtocolHandlerCustomizers((TomcatProtocolHandlerCustomizer[]) null))
|
||||
.withMessageContaining("'protocolHandlerCustomizers' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void tomcatConnectorCustomizersShouldBeInvoked() {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
HttpHandler handler = mock(HttpHandler.class);
|
||||
TomcatConnectorCustomizer[] customizers = new TomcatConnectorCustomizer[4];
|
||||
Arrays.setAll(customizers, (i) -> mock(TomcatConnectorCustomizer.class));
|
||||
factory.setConnectorCustomizers(Arrays.asList(customizers[0], customizers[1]));
|
||||
factory.addConnectorCustomizers(customizers[2], customizers[3]);
|
||||
this.webServer = factory.getWebServer(handler);
|
||||
InOrder ordered = inOrder((Object[]) customizers);
|
||||
for (TomcatConnectorCustomizer customizer : customizers) {
|
||||
then(customizer).should(ordered).customize(any(Connector.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
void tomcatProtocolHandlerCustomizersShouldBeInvoked() {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
HttpHandler handler = mock(HttpHandler.class);
|
||||
TomcatProtocolHandlerCustomizer<AbstractHttp11Protocol<?>>[] customizers = new TomcatProtocolHandlerCustomizer[4];
|
||||
Arrays.setAll(customizers, (i) -> mock(TomcatProtocolHandlerCustomizer.class));
|
||||
factory.setProtocolHandlerCustomizers(Arrays.asList(customizers[0], customizers[1]));
|
||||
factory.addProtocolHandlerCustomizers(customizers[2], customizers[3]);
|
||||
this.webServer = factory.getWebServer(handler);
|
||||
InOrder ordered = inOrder((Object[]) customizers);
|
||||
for (TomcatProtocolHandlerCustomizer customizer : customizers) {
|
||||
then(customizer).should(ordered).customize(any(ProtocolHandler.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void tomcatAdditionalConnectors() {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
Connector[] connectors = new Connector[4];
|
||||
Arrays.setAll(connectors, (i) -> new Connector());
|
||||
factory.addAdditionalConnectors(connectors);
|
||||
this.webServer = factory.getWebServer(mock(HttpHandler.class));
|
||||
Map<Service, Connector[]> connectorsByService = TomcatAccess
|
||||
.getServiceConnectors((TomcatWebServer) this.webServer);
|
||||
assertThat(connectorsByService.values().iterator().next()).hasSize(connectors.length + 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addNullAdditionalConnectorsThrows() {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> factory.addAdditionalConnectors((Connector[]) null))
|
||||
.withMessageContaining("'connectors' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void useForwardedHeaders() {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
RemoteIpValve valve = new RemoteIpValve();
|
||||
valve.setProtocolHeader("X-Forwarded-Proto");
|
||||
factory.addEngineValves(valve);
|
||||
assertForwardHeaderIsUsed(factory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void referenceClearingIsDisabled() {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
this.webServer = factory.getWebServer(mock(HttpHandler.class));
|
||||
this.webServer.start();
|
||||
Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat();
|
||||
StandardContext context = (StandardContext) tomcat.getHost().findChildren()[0];
|
||||
assertThat(context.getClearReferencesRmiTargets()).isFalse();
|
||||
assertThat(context.getClearReferencesThreadLocals()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void portClashOfPrimaryConnectorResultsInPortInUseException() throws Exception {
|
||||
doWithBlockedPort((port) -> assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
factory.setPort(port);
|
||||
this.webServer = factory.getWebServer(mock(HttpHandler.class));
|
||||
this.webServer.start();
|
||||
}).satisfies((ex) -> handleExceptionCausedByBlockedPortOnPrimaryConnector(ex, port)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void assertThatSslWithInvalidAliasCallFails(ThrowingCallable call) {
|
||||
assertThatExceptionOfType(WebServerException.class).isThrownBy(call);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() {
|
||||
TomcatReactiveWebServerFactory factory = getFactory();
|
||||
factory.setShutdown(Shutdown.GRACEFUL);
|
||||
BlockingHandler blockingHandler = new BlockingHandler();
|
||||
this.webServer = factory.getWebServer(blockingHandler);
|
||||
this.webServer.start();
|
||||
WebClient webClient = getWebClient(this.webServer.getPort()).build();
|
||||
this.webServer.shutDownGracefully((result) -> {
|
||||
});
|
||||
Awaitility.await().atMost(Duration.ofSeconds(30)).until(() -> {
|
||||
blockingHandler.stopBlocking();
|
||||
try {
|
||||
webClient.get().retrieve().toBodilessEntity().block();
|
||||
return false;
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
return ex.getCause() instanceof ConnectException;
|
||||
}
|
||||
});
|
||||
this.webServer.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenGetTomcatWebServerIsOverriddenThenWebServerCreationCanBeCustomized() {
|
||||
AtomicReference<TomcatWebServer> webServerReference = new AtomicReference<>();
|
||||
TomcatWebServer webServer = (TomcatWebServer) new TomcatReactiveWebServerFactory() {
|
||||
|
||||
@Override
|
||||
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
|
||||
webServerReference.set(new TomcatWebServer(tomcat));
|
||||
return webServerReference.get();
|
||||
}
|
||||
|
||||
}.getWebServer(new EchoHandler());
|
||||
assertThat(webServerReference).hasValue(webServer);
|
||||
}
|
||||
|
||||
private void handleExceptionCausedByBlockedPortOnPrimaryConnector(RuntimeException ex, int blockedPort) {
|
||||
assertThat(ex).isInstanceOf(PortInUseException.class);
|
||||
assertThat(((PortInUseException) ex).getPort()).isEqualTo(blockedPort);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String startedLogMessage() {
|
||||
return TomcatAccess.getStartedLogMessage((TomcatWebServer) this.webServer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addConnector(int port, ConfigurableReactiveWebServerFactory factory) {
|
||||
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
|
||||
connector.setPort(port);
|
||||
((TomcatReactiveWebServerFactory) factory).addAdditionalConnectors(connector);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link TldPatterns}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class TldPatternsTests {
|
||||
|
||||
@Test
|
||||
void tomcatSkipAlignsWithTomcatDefaults() throws IOException {
|
||||
assertThat(TldPatterns.TOMCAT_SKIP).containsExactlyInAnyOrderElementsOf(getTomcatDefaultJarsToSkip());
|
||||
}
|
||||
|
||||
@Test
|
||||
void tomcatScanAlignsWithTomcatDefaults() throws IOException {
|
||||
assertThat(TldPatterns.TOMCAT_SCAN).containsExactlyInAnyOrderElementsOf(getTomcatDefaultJarsToScan());
|
||||
}
|
||||
|
||||
private Set<String> getTomcatDefaultJarsToSkip() throws IOException {
|
||||
return getTomcatDefault("tomcat.util.scan.StandardJarScanFilter.jarsToSkip");
|
||||
}
|
||||
|
||||
private Set<String> getTomcatDefaultJarsToScan() throws IOException {
|
||||
return getTomcatDefault("tomcat.util.scan.StandardJarScanFilter.jarsToScan");
|
||||
}
|
||||
|
||||
private Set<String> getTomcatDefault(String key) throws IOException {
|
||||
ClassLoader classLoader = getClass().getClassLoader();
|
||||
try (InputStream inputStream = classLoader.getResource("catalina.properties").openStream()) {
|
||||
Properties properties = new Properties();
|
||||
properties.load(inputStream);
|
||||
String jarsToSkip = properties.getProperty(key);
|
||||
return StringUtils.commaDelimitedListToSet(jarsToSkip);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,776 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.naming.InitialContext;
|
||||
import javax.naming.NamingException;
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||
import javax.net.ssl.SSLSession;
|
||||
|
||||
import jakarta.servlet.MultipartConfigElement;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRegistration.Dynamic;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.catalina.Container;
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.LifecycleEvent;
|
||||
import org.apache.catalina.LifecycleListener;
|
||||
import org.apache.catalina.LifecycleState;
|
||||
import org.apache.catalina.Service;
|
||||
import org.apache.catalina.Valve;
|
||||
import org.apache.catalina.connector.Connector;
|
||||
import org.apache.catalina.core.AprLifecycleListener;
|
||||
import org.apache.catalina.core.StandardContext;
|
||||
import org.apache.catalina.core.StandardWrapper;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.catalina.util.CharsetMapper;
|
||||
import org.apache.catalina.valves.RemoteIpValve;
|
||||
import org.apache.coyote.ProtocolHandler;
|
||||
import org.apache.coyote.http11.AbstractHttp11Protocol;
|
||||
import org.apache.coyote.http11.Http11Nio2Protocol;
|
||||
import org.apache.hc.client5.http.HttpHostConnectException;
|
||||
import org.apache.hc.client5.http.classic.HttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
||||
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
|
||||
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
|
||||
import org.apache.hc.core5.http.HttpResponse;
|
||||
import org.apache.hc.core5.http.NoHttpResponseException;
|
||||
import org.apache.hc.core5.ssl.SSLContextBuilder;
|
||||
import org.apache.jasper.servlet.JspServlet;
|
||||
import org.apache.tomcat.JarScanFilter;
|
||||
import org.apache.tomcat.JarScanType;
|
||||
import org.apache.tomcat.util.scan.StandardJarScanFilter;
|
||||
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.boot.testsupport.system.CapturedOutput;
|
||||
import org.springframework.boot.web.server.PortInUseException;
|
||||
import org.springframework.boot.web.server.Shutdown;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.WebServerException;
|
||||
import org.springframework.boot.web.server.servlet.AbstractServletWebServerFactoryTests;
|
||||
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.tomcat.ConnectorStartFailedException;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatAccess;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatConnectorCustomizer;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatContextCustomizer;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatEmbeddedContext;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatProtocolHandlerCustomizer;
|
||||
import org.springframework.boot.web.server.tomcat.TomcatWebServer;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.support.PropertiesLoaderUtils;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.assertArg;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link TomcatServletWebServerFactory}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @author Stephane Nicoll
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactoryTests {
|
||||
|
||||
@Override
|
||||
protected TomcatServletWebServerFactory getFactory() {
|
||||
return new TomcatServletWebServerFactory(0);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void restoreTccl() {
|
||||
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
|
||||
}
|
||||
|
||||
// JMX MBean names clash if you get more than one Engine with the same name...
|
||||
@Test
|
||||
void tomcatEngineNames() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
this.webServer = factory.getWebServer();
|
||||
factory.setPort(0);
|
||||
TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer();
|
||||
// Make sure that the names are different
|
||||
String firstName = ((TomcatWebServer) this.webServer).getTomcat().getEngine().getName();
|
||||
String secondName = tomcatWebServer.getTomcat().getEngine().getName();
|
||||
assertThat(firstName).as("Tomcat engines must have different names").isNotEqualTo(secondName);
|
||||
tomcatWebServer.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultTomcatListeners() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
assertThat(factory.getContextLifecycleListeners()).isEmpty();
|
||||
TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer();
|
||||
this.webServer = tomcatWebServer;
|
||||
assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void aprShouldBeOptIn() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
factory.setUseApr(true);
|
||||
TomcatWebServer tomcatWebServer = (TomcatWebServer) factory.getWebServer();
|
||||
this.webServer = tomcatWebServer;
|
||||
assertThat(tomcatWebServer.getTomcat().getServer().findLifecycleListeners()).singleElement()
|
||||
.isInstanceOf(AprLifecycleListener.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tomcatListeners() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
LifecycleListener[] listeners = new LifecycleListener[4];
|
||||
Arrays.setAll(listeners, (i) -> mock(LifecycleListener.class));
|
||||
factory.setContextLifecycleListeners(Arrays.asList(listeners[0], listeners[1]));
|
||||
factory.addContextLifecycleListeners(listeners[2], listeners[3]);
|
||||
this.webServer = factory.getWebServer();
|
||||
InOrder ordered = inOrder((Object[]) listeners);
|
||||
for (LifecycleListener listener : listeners) {
|
||||
then(listener).should(ordered).lifecycleEvent(any(LifecycleEvent.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void tomcatCustomizers() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
TomcatContextCustomizer[] customizers = new TomcatContextCustomizer[4];
|
||||
Arrays.setAll(customizers, (i) -> mock(TomcatContextCustomizer.class));
|
||||
factory.setContextCustomizers(Arrays.asList(customizers[0], customizers[1]));
|
||||
factory.addContextCustomizers(customizers[2], customizers[3]);
|
||||
this.webServer = factory.getWebServer();
|
||||
InOrder ordered = inOrder((Object[]) customizers);
|
||||
for (TomcatContextCustomizer customizer : customizers) {
|
||||
then(customizer).should(ordered).customize(any(Context.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void contextIsAddedToHostBeforeCustomizersAreCalled() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
TomcatContextCustomizer customizer = mock(TomcatContextCustomizer.class);
|
||||
factory.addContextCustomizers(customizer);
|
||||
this.webServer = factory.getWebServer();
|
||||
then(customizer).should().customize(assertArg((context) -> assertThat(context.getParent()).isNotNull()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void tomcatConnectorCustomizers() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
TomcatConnectorCustomizer[] customizers = new TomcatConnectorCustomizer[4];
|
||||
Arrays.setAll(customizers, (i) -> mock(TomcatConnectorCustomizer.class));
|
||||
factory.setConnectorCustomizers(Arrays.asList(customizers[0], customizers[1]));
|
||||
factory.addConnectorCustomizers(customizers[2], customizers[3]);
|
||||
this.webServer = factory.getWebServer();
|
||||
InOrder ordered = inOrder((Object[]) customizers);
|
||||
for (TomcatConnectorCustomizer customizer : customizers) {
|
||||
then(customizer).should(ordered).customize(any(Connector.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
void tomcatProtocolHandlerCustomizersShouldBeInvoked() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
TomcatProtocolHandlerCustomizer<AbstractHttp11Protocol<?>>[] customizers = new TomcatProtocolHandlerCustomizer[4];
|
||||
Arrays.setAll(customizers, (i) -> mock(TomcatProtocolHandlerCustomizer.class));
|
||||
factory.setProtocolHandlerCustomizers(Arrays.asList(customizers[0], customizers[1]));
|
||||
factory.addProtocolHandlerCustomizers(customizers[2], customizers[3]);
|
||||
this.webServer = factory.getWebServer();
|
||||
InOrder ordered = inOrder((Object[]) customizers);
|
||||
for (TomcatProtocolHandlerCustomizer customizer : customizers) {
|
||||
then(customizer).should(ordered).customize(any(ProtocolHandler.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void tomcatProtocolHandlerCanBeCustomized() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
TomcatProtocolHandlerCustomizer<AbstractHttp11Protocol<?>> customizer = (protocolHandler) -> protocolHandler
|
||||
.setProcessorCache(250);
|
||||
factory.addProtocolHandlerCustomizers(customizer);
|
||||
Tomcat tomcat = getTomcat(factory);
|
||||
Connector connector = TomcatAccess.getServiceConnectors((TomcatWebServer) this.webServer)
|
||||
.get(tomcat.getService())[0];
|
||||
AbstractHttp11Protocol<?> protocolHandler = (AbstractHttp11Protocol<?>) connector.getProtocolHandler();
|
||||
assertThat(protocolHandler.getProcessorCache()).isEqualTo(250);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tomcatAdditionalConnectors() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
Connector[] connectors = new Connector[4];
|
||||
Arrays.setAll(connectors, (i) -> {
|
||||
Connector connector = new Connector();
|
||||
connector.setPort(0);
|
||||
return connector;
|
||||
});
|
||||
factory.addAdditionalConnectors(connectors);
|
||||
this.webServer = factory.getWebServer();
|
||||
Map<Service, Connector[]> connectorsByService = new HashMap<>(
|
||||
TomcatAccess.getServiceConnectors((TomcatWebServer) this.webServer));
|
||||
assertThat(connectorsByService.values().iterator().next()).hasSize(connectors.length + 1);
|
||||
this.webServer.start();
|
||||
this.webServer.stop();
|
||||
connectorsByService.forEach((service, serviceConnectors) -> {
|
||||
for (Connector connector : serviceConnectors) {
|
||||
assertThat(connector.getProtocolHandler()).extracting("endpoint.serverSock").isNull();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void addNullAdditionalConnectorThrows() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> factory.addAdditionalConnectors((Connector[]) null))
|
||||
.withMessageContaining("'connectors' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionTimeout() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
factory.getSettings().getSession().setTimeout(Duration.ofSeconds(10));
|
||||
assertTimeout(factory, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionTimeoutInMinutes() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
factory.getSettings().getSession().setTimeout(Duration.ofMinutes(1));
|
||||
assertTimeout(factory, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noSessionTimeout() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
factory.getSettings().getSession().setTimeout(null);
|
||||
assertTimeout(factory, -1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void valve() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
Valve valve = mock(Valve.class);
|
||||
factory.addContextValves(valve);
|
||||
this.webServer = factory.getWebServer();
|
||||
then(valve).should().setNext(any(Valve.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setNullTomcatContextCustomizersThrows() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> factory.setContextCustomizers(null))
|
||||
.withMessageContaining("'contextCustomizers' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void addNullContextCustomizersThrows() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> factory.addContextCustomizers((TomcatContextCustomizer[]) null))
|
||||
.withMessageContaining("'contextCustomizers' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setNullTomcatConnectorCustomizersThrows() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> factory.setConnectorCustomizers(null))
|
||||
.withMessageContaining("'connectorCustomizers' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void addNullConnectorCustomizersThrows() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> factory.addConnectorCustomizers((TomcatConnectorCustomizer[]) null))
|
||||
.withMessageContaining("'connectorCustomizers' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setNullTomcatProtocolHandlerCustomizersThrows() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> factory.setProtocolHandlerCustomizers(null))
|
||||
.withMessageContaining("'protocolHandlerCustomizers' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void addNullTomcatProtocolHandlerCustomizersThrows() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> factory.addProtocolHandlerCustomizers((TomcatProtocolHandlerCustomizer[]) null))
|
||||
.withMessageContaining("'protocolHandlerCustomizers' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void uriEncoding() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
factory.setUriEncoding(StandardCharsets.US_ASCII);
|
||||
Tomcat tomcat = getTomcat(factory);
|
||||
Connector connector = TomcatAccess.getServiceConnectors((TomcatWebServer) this.webServer)
|
||||
.get(tomcat.getService())[0];
|
||||
assertThat(connector.getURIEncoding()).isEqualTo("US-ASCII");
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultUriEncoding() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
Tomcat tomcat = getTomcat(factory);
|
||||
Connector connector = TomcatAccess.getServiceConnectors((TomcatWebServer) this.webServer)
|
||||
.get(tomcat.getService())[0];
|
||||
assertThat(connector.getURIEncoding()).isEqualTo("UTF-8");
|
||||
}
|
||||
|
||||
@Test
|
||||
void startupFailureDoesNotResultInUnstoppedThreadsBeingReported(CapturedOutput output) throws Exception {
|
||||
super.portClashOfPrimaryConnectorResultsInPortInUseException();
|
||||
assertThat(output).doesNotContain("appears to have started a thread named [main]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void destroyCalledWithoutStart() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
this.webServer = factory.getWebServer(exampleServletRegistration());
|
||||
this.webServer.destroy();
|
||||
Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat();
|
||||
assertThat(tomcat.getServer().getState()).isSameAs(LifecycleState.DESTROYED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addConnector(int port, ConfigurableServletWebServerFactory factory) {
|
||||
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
|
||||
connector.setPort(port);
|
||||
((TomcatServletWebServerFactory) factory).addAdditionalConnectors(connector);
|
||||
}
|
||||
|
||||
@Test
|
||||
void useForwardHeaders() throws Exception {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
factory.addContextValves(new RemoteIpValve());
|
||||
assertForwardHeaderIsUsed(factory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void disableDoesNotSaveSessionFiles() throws Exception {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
// If baseDir is not set SESSIONS.ser is written to a different temp directory
|
||||
// each time. By setting it we can really ensure that data isn't saved
|
||||
factory.setBaseDirectory(this.tempDir);
|
||||
this.webServer = factory.getWebServer(sessionServletRegistration());
|
||||
this.webServer.start();
|
||||
String s1 = getResponse(getLocalUrl("/session"));
|
||||
String s2 = getResponse(getLocalUrl("/session"));
|
||||
this.webServer.stop();
|
||||
this.webServer = factory.getWebServer(sessionServletRegistration());
|
||||
this.webServer.start();
|
||||
String s3 = getResponse(getLocalUrl("/session"));
|
||||
String message = "Session error s1=" + s1 + " s2=" + s2 + " s3=" + s3;
|
||||
assertThat(s2.split(":")[0]).as(message).isEqualTo(s1.split(":")[1]);
|
||||
assertThat(s3.split(":")[0]).as(message).isNotEqualTo(s2.split(":")[1]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jndiLookupsCanBePerformedDuringApplicationContextRefresh() throws NamingException {
|
||||
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
|
||||
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(0) {
|
||||
|
||||
@Override
|
||||
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
|
||||
tomcat.enableNaming();
|
||||
return super.getTomcatWebServer(tomcat);
|
||||
}
|
||||
|
||||
};
|
||||
// Server is created in onRefresh
|
||||
this.webServer = factory.getWebServer();
|
||||
// Lookups should now be possible
|
||||
new InitialContext().lookup("java:comp/env");
|
||||
// Called in finishRefresh, giving us an opportunity to remove the context binding
|
||||
// and avoid a leak
|
||||
this.webServer.start();
|
||||
// Lookups should no longer be possible
|
||||
assertThatExceptionOfType(NamingException.class).isThrownBy(() -> new InitialContext().lookup("java:comp/env"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultLocaleCharsetMappingsAreOverridden() throws IOException {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
this.webServer = factory.getWebServer();
|
||||
// override defaults, see org.apache.catalina.util.CharsetMapperDefault.properties
|
||||
Properties charsetMapperDefault = PropertiesLoaderUtils
|
||||
.loadProperties(new ClassPathResource("CharsetMapperDefault.properties", CharsetMapper.class));
|
||||
for (String language : charsetMapperDefault.stringPropertyNames()) {
|
||||
assertThat(getCharset(new Locale(language))).isEqualTo(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void tldSkipPatternsShouldBeAppliedToContextJarScanner() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
factory.addTldSkipPatterns("foo.jar", "bar.jar");
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat();
|
||||
Context context = (Context) tomcat.getHost().findChildren()[0];
|
||||
JarScanFilter jarScanFilter = context.getJarScanner().getJarScanFilter();
|
||||
assertThat(jarScanFilter.check(JarScanType.TLD, "foo.jar")).isFalse();
|
||||
assertThat(jarScanFilter.check(JarScanType.TLD, "bar.jar")).isFalse();
|
||||
assertThat(jarScanFilter.check(JarScanType.TLD, "test.jar")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void tldScanPatternsShouldBeAppliedToContextJarScanner() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat();
|
||||
Context context = (Context) tomcat.getHost().findChildren()[0];
|
||||
JarScanFilter jarScanFilter = context.getJarScanner().getJarScanFilter();
|
||||
String tldScan = ((StandardJarScanFilter) jarScanFilter).getTldScan();
|
||||
assertThat(tldScan).isEqualTo("log4j-taglib*.jar,log4j-jakarta-web*.jar,log4javascript*.jar,slf4j-taglib*.jar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customTomcatHttpOnlyCookie() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
factory.getSettings().getSession().getCookie().setHttpOnly(false);
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat();
|
||||
Context context = (Context) tomcat.getHost().findChildren()[0];
|
||||
assertThat(context.getUseHttpOnly()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void exceptionThrownOnLoadFailureWhenFailCtxIfServletStartFailsIsTrue() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
factory.addContextCustomizers((context) -> {
|
||||
if (context instanceof StandardContext standardContext) {
|
||||
standardContext.setFailCtxIfServletStartFails(true);
|
||||
}
|
||||
});
|
||||
this.webServer = factory
|
||||
.getWebServer((context) -> context.addServlet("failing", FailingServlet.class).setLoadOnStartup(0));
|
||||
assertThatExceptionOfType(WebServerException.class).isThrownBy(this.webServer::start);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exceptionThrownOnLoadFailureWhenFailCtxIfServletStartFailsIsFalse() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
factory.addContextCustomizers((context) -> {
|
||||
if (context instanceof StandardContext standardContext) {
|
||||
standardContext.setFailCtxIfServletStartFails(false);
|
||||
}
|
||||
});
|
||||
this.webServer = factory
|
||||
.getWebServer((context) -> context.addServlet("failing", FailingServlet.class).setLoadOnStartup(0));
|
||||
this.webServer.start();
|
||||
}
|
||||
|
||||
@Test
|
||||
void referenceClearingIsDisabled() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat();
|
||||
StandardContext context = (StandardContext) tomcat.getHost().findChildren()[0];
|
||||
assertThat(context.getClearReferencesRmiTargets()).isFalse();
|
||||
assertThat(context.getClearReferencesThreadLocals()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonExistentUploadDirectoryIsCreatedUponMultipartUpload() {
|
||||
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(0);
|
||||
AtomicReference<ServletContext> servletContextReference = new AtomicReference<>();
|
||||
factory.addInitializers((servletContext) -> {
|
||||
servletContextReference.set(servletContext);
|
||||
Dynamic servlet = servletContext.addServlet("upload", new HttpServlet() {
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
req.getParts();
|
||||
}
|
||||
|
||||
});
|
||||
servlet.addMapping("/upload");
|
||||
servlet.setMultipartConfig(new MultipartConfigElement((String) null));
|
||||
});
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
File temp = (File) servletContextReference.get().getAttribute(ServletContext.TEMPDIR);
|
||||
FileSystemUtils.deleteRecursively(temp);
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("file", new ByteArrayResource(new byte[1024 * 1024]));
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(getLocalUrl("/upload"), requestEntity,
|
||||
String.class);
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exceptionThrownOnContextListenerDestroysServer() {
|
||||
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(0) {
|
||||
|
||||
@Override
|
||||
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
|
||||
try {
|
||||
return super.getTomcatWebServer(tomcat);
|
||||
}
|
||||
finally {
|
||||
assertThat(tomcat.getServer().getState()).isEqualTo(LifecycleState.DESTROYED);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
assertThatExceptionOfType(WebServerException.class).isThrownBy(
|
||||
() -> factory.getWebServer((context) -> context.addListener(new FailingServletContextListener())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerJspServletWithDefaultLoadOnStartup() {
|
||||
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(0);
|
||||
factory.addInitializers((context) -> context.addServlet("manually-registered-jsp-servlet", JspServlet.class));
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void assertThatSslWithInvalidAliasCallFails(ThrowingCallable call) {
|
||||
assertThatExceptionOfType(WebServerException.class).isThrownBy(call);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() throws Exception {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
factory.setShutdown(Shutdown.GRACEFUL);
|
||||
BlockingServlet blockingServlet = new BlockingServlet();
|
||||
this.webServer = factory.getWebServer((context) -> {
|
||||
Dynamic registration = context.addServlet("blockingServlet", blockingServlet);
|
||||
registration.addMapping("/blocking");
|
||||
registration.setAsyncSupported(true);
|
||||
});
|
||||
this.webServer.start();
|
||||
int port = this.webServer.getPort();
|
||||
Future<Object> request = initiateGetRequest(port, "/blocking");
|
||||
blockingServlet.awaitQueue();
|
||||
this.webServer.shutDownGracefully((result) -> {
|
||||
});
|
||||
Object unconnectableRequest = Awaitility.await()
|
||||
.until(() -> initiateGetRequest(HttpClients.createDefault(), port, "/").get(),
|
||||
(result) -> result instanceof Exception);
|
||||
assertThat(unconnectableRequest).isInstanceOf(HttpHostConnectException.class);
|
||||
blockingServlet.admitOne();
|
||||
assertThat(request.get()).isInstanceOf(HttpResponse.class);
|
||||
this.webServer.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenServerIsShuttingDownARequestOnAnIdleConnectionResultsInConnectionReset() throws Exception {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
factory.setShutdown(Shutdown.GRACEFUL);
|
||||
BlockingServlet blockingServlet = new BlockingServlet();
|
||||
this.webServer = factory.getWebServer((context) -> {
|
||||
Dynamic registration = context.addServlet("blockingServlet", blockingServlet);
|
||||
registration.addMapping("/blocking");
|
||||
registration.setAsyncSupported(true);
|
||||
});
|
||||
HttpClient httpClient = HttpClients.createMinimal();
|
||||
this.webServer.start();
|
||||
int port = this.webServer.getPort();
|
||||
Future<Object> keepAliveRequest = initiateGetRequest(httpClient, port, "/blocking");
|
||||
blockingServlet.awaitQueue();
|
||||
blockingServlet.admitOne();
|
||||
assertThat(keepAliveRequest.get()).isInstanceOf(HttpResponse.class);
|
||||
Future<Object> request = initiateGetRequest(port, "/blocking");
|
||||
blockingServlet.awaitQueue();
|
||||
this.webServer.shutDownGracefully((result) -> {
|
||||
});
|
||||
Object idleConnectionRequestResult = Awaitility.await().until(() -> {
|
||||
Future<Object> idleConnectionRequest = initiateGetRequest(httpClient, port, "/");
|
||||
Object result = idleConnectionRequest.get();
|
||||
return result;
|
||||
}, (result) -> result instanceof Exception);
|
||||
assertThat(idleConnectionRequestResult).isInstanceOfAny(SocketException.class, NoHttpResponseException.class);
|
||||
if (idleConnectionRequestResult instanceof SocketException socketException) {
|
||||
assertThat(socketException).hasMessage("Connection reset");
|
||||
}
|
||||
blockingServlet.admitOne();
|
||||
Object response = request.get();
|
||||
assertThat(response).isInstanceOf(HttpResponse.class);
|
||||
this.webServer.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources({ "1.crt", "1.key", "2.crt", "2.key" })
|
||||
void shouldUpdateSslWhenReloadingSslBundles() throws Exception {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
addTestTxtFile(factory);
|
||||
DefaultSslBundleRegistry bundles = new DefaultSslBundleRegistry("test",
|
||||
createPemSslBundle("classpath:1.crt", "classpath:1.key"));
|
||||
factory.setSslBundles(bundles);
|
||||
factory.setSsl(Ssl.forBundle("test"));
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
RememberingHostnameVerifier verifier = new RememberingHostnameVerifier();
|
||||
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build();
|
||||
TlsSocketStrategy tlsSocketStrategy = new DefaultClientTlsStrategy(sslContext, verifier);
|
||||
HttpComponentsClientHttpRequestFactory requestFactory = createHttpComponentsRequestFactory(tlsSocketStrategy);
|
||||
assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test");
|
||||
assertThat(verifier.getLastPrincipal()).isEqualTo("CN=1");
|
||||
requestFactory = createHttpComponentsRequestFactory(tlsSocketStrategy);
|
||||
bundles.updateBundle("test", createPemSslBundle("classpath:2.crt", "classpath:2.key"));
|
||||
assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test");
|
||||
assertThat(verifier.getLastPrincipal()).isEqualTo("CN=2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void sslWithHttp11Nio2Protocol() throws Exception {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
addTestTxtFile(factory);
|
||||
factory.setProtocol(Http11Nio2Protocol.class.getName());
|
||||
factory.setSsl(getSsl(null, "password", "classpath:test.jks"));
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
HttpComponentsClientHttpRequestFactory requestFactory = createHttpComponentsRequestFactory(
|
||||
createTrustSelfSignedTlsSocketStrategy());
|
||||
assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JspServlet getJspServlet() throws ServletException {
|
||||
Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat();
|
||||
Container container = tomcat.getHost().findChildren()[0];
|
||||
StandardWrapper standardWrapper = (StandardWrapper) container.findChild("jsp");
|
||||
if (standardWrapper == null) {
|
||||
return null;
|
||||
}
|
||||
standardWrapper.load();
|
||||
return (JspServlet) standardWrapper.getServlet();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, String> getActualMimeMappings() {
|
||||
Context context = (Context) ((TomcatWebServer) this.webServer).getTomcat().getHost().findChildren()[0];
|
||||
Map<String, String> mimeMappings = new HashMap<>();
|
||||
for (String extension : context.findMimeMappings()) {
|
||||
mimeMappings.put(extension, context.findMimeMapping(extension));
|
||||
}
|
||||
return mimeMappings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Charset getCharset(Locale locale) {
|
||||
Context context = (Context) ((TomcatWebServer) this.webServer).getTomcat().getHost().findChildren()[0];
|
||||
CharsetMapper mapper = ((TomcatEmbeddedContext) context).getCharsetMapper();
|
||||
String charsetName = mapper.getCharset(locale);
|
||||
return (charsetName != null) ? Charset.forName(charsetName) : null;
|
||||
}
|
||||
|
||||
private void assertTimeout(TomcatServletWebServerFactory factory, int expected) {
|
||||
Tomcat tomcat = getTomcat(factory);
|
||||
Context context = (Context) tomcat.getHost().findChildren()[0];
|
||||
assertThat(context.getSessionTimeout()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
private Tomcat getTomcat(TomcatServletWebServerFactory factory) {
|
||||
this.webServer = factory.getWebServer();
|
||||
return ((TomcatWebServer) this.webServer).getTomcat();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleExceptionCausedByBlockedPortOnPrimaryConnector(RuntimeException ex, int blockedPort) {
|
||||
assertThat(ex).isInstanceOf(PortInUseException.class);
|
||||
assertThat(((PortInUseException) ex).getPort()).isEqualTo(blockedPort);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleExceptionCausedByBlockedPortOnSecondaryConnector(RuntimeException ex, int blockedPort) {
|
||||
assertThat(ex).isInstanceOf(ConnectorStartFailedException.class);
|
||||
assertThat(((ConnectorStartFailedException) ex).getPort()).isEqualTo(blockedPort);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String startedLogMessage() {
|
||||
return TomcatAccess.getStartedLogMessage((TomcatWebServer) this.webServer);
|
||||
}
|
||||
|
||||
private static final class RememberingHostnameVerifier implements HostnameVerifier {
|
||||
|
||||
private volatile String lastPrincipal;
|
||||
|
||||
@Override
|
||||
public boolean verify(String hostname, SSLSession session) {
|
||||
try {
|
||||
this.lastPrincipal = session.getPeerPrincipal().getName();
|
||||
}
|
||||
catch (SSLPeerUnverifiedException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
String getLastPrincipal() {
|
||||
return this.lastPrincipal;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import org.apache.catalina.connector.Connector;
|
||||
import org.apache.coyote.http11.AbstractHttp11Protocol;
|
||||
import org.apache.coyote.http2.Http2Protocol;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.web.server.Compression;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link CompressionConnectorCustomizer}
|
||||
*
|
||||
* @author Rudy Adams
|
||||
*/
|
||||
class CompressionConnectorCustomizerTests {
|
||||
|
||||
private static final int MIN_SIZE = 100;
|
||||
|
||||
private final String[] mimeTypes = { "text/html", "text/xml", "text/xhtml" };
|
||||
|
||||
private final String[] excludedUserAgents = { "SomeUserAgent", "AnotherUserAgent" };
|
||||
|
||||
private Compression compression;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.compression = new Compression();
|
||||
this.compression.setEnabled(true);
|
||||
this.compression.setMinResponseSize(DataSize.ofBytes(MIN_SIZE));
|
||||
this.compression.setMimeTypes(this.mimeTypes);
|
||||
this.compression.setExcludedUserAgents(this.excludedUserAgents);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCustomizeCompression() {
|
||||
CompressionConnectorCustomizer compressionConnectorCustomizer = new CompressionConnectorCustomizer(
|
||||
this.compression);
|
||||
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
|
||||
Http2Protocol upgradeProtocol = new Http2Protocol();
|
||||
upgradeProtocol.setHttp11Protocol((AbstractHttp11Protocol<?>) connector.getProtocolHandler());
|
||||
connector.addUpgradeProtocol(upgradeProtocol);
|
||||
compressionConnectorCustomizer.customize(connector);
|
||||
AbstractHttp11Protocol<?> abstractHttp11Protocol = (AbstractHttp11Protocol<?>) connector.getProtocolHandler();
|
||||
compressionOn(abstractHttp11Protocol.getCompression());
|
||||
minSize(abstractHttp11Protocol.getCompressionMinSize());
|
||||
mimeType(abstractHttp11Protocol.getCompressibleMimeTypes());
|
||||
excludedUserAgents(abstractHttp11Protocol.getNoCompressionUserAgents());
|
||||
}
|
||||
|
||||
private void compressionOn(String compression) {
|
||||
assertThat(compression).isEqualTo("on");
|
||||
}
|
||||
|
||||
private void minSize(int minSize) {
|
||||
assertThat(minSize).isEqualTo(MIN_SIZE);
|
||||
}
|
||||
|
||||
private void mimeType(String[] mimeTypes) {
|
||||
assertThat(mimeTypes).isEqualTo(this.mimeTypes);
|
||||
}
|
||||
|
||||
private void excludedUserAgents(String combinedUserAgents) {
|
||||
assertThat(combinedUserAgents).isEqualTo("SomeUserAgent,AnotherUserAgent");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.apache.catalina.connector.Connector;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.tomcat.util.net.SSLHostConfig;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.boot.testsupport.ssl.MockPkcs11Security;
|
||||
import org.springframework.boot.testsupport.ssl.MockPkcs11SecurityProvider;
|
||||
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
|
||||
import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.WebServerSslBundle;
|
||||
|
||||
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 SslConnectorCustomizer}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @author Cyril Dangerville
|
||||
*/
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
@DirtiesUrlFactories
|
||||
@MockPkcs11Security
|
||||
class SslConnectorCustomizerTests {
|
||||
|
||||
private final Log logger = LogFactory.getLog(SslConnectorCustomizerTests.class);
|
||||
|
||||
private Tomcat tomcat;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.tomcat = new Tomcat();
|
||||
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
|
||||
connector.setPort(0);
|
||||
this.tomcat.setConnector(connector);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void stop() throws Exception {
|
||||
System.clearProperty("javax.net.ssl.trustStorePassword");
|
||||
this.tomcat.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void sslCiphersConfiguration() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStore("classpath:test.jks");
|
||||
ssl.setKeyStorePassword("secret");
|
||||
ssl.setCiphers(new String[] { "ALPHA", "BRAVO", "CHARLIE" });
|
||||
Connector connector = this.tomcat.getConnector();
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth());
|
||||
customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap());
|
||||
this.tomcat.start();
|
||||
SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs();
|
||||
assertThat(sslHostConfigs[0].getCiphers()).isEqualTo("ALPHA:BRAVO:CHARLIE");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void sslEnabledMultipleProtocolsConfiguration() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setKeyStore("classpath:test.jks");
|
||||
ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" });
|
||||
ssl.setCiphers(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "BRAVO" });
|
||||
Connector connector = this.tomcat.getConnector();
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth());
|
||||
customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap());
|
||||
this.tomcat.start();
|
||||
SSLHostConfig sslHostConfig = connector.getProtocolHandler().findSslHostConfigs()[0];
|
||||
assertThat(sslHostConfig.getSslProtocol()).isEqualTo("TLS");
|
||||
assertThat(sslHostConfig.getEnabledProtocols()).containsExactlyInAnyOrder("TLSv1.1", "TLSv1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void sslEnabledProtocolsConfiguration() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setKeyStore("classpath:test.jks");
|
||||
ssl.setEnabledProtocols(new String[] { "TLSv1.2" });
|
||||
ssl.setCiphers(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "BRAVO" });
|
||||
Connector connector = this.tomcat.getConnector();
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth());
|
||||
customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap());
|
||||
this.tomcat.start();
|
||||
SSLHostConfig sslHostConfig = connector.getProtocolHandler().findSslHostConfigs()[0];
|
||||
assertThat(sslHostConfig.getSslProtocol()).isEqualTo("TLS");
|
||||
assertThat(sslHostConfig.getEnabledProtocols()).containsExactly("TLSv1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() {
|
||||
assertThatIllegalStateException().isThrownBy(() -> {
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, this.tomcat.getConnector(),
|
||||
Ssl.ClientAuth.NONE);
|
||||
customizer.customize(WebServerSslBundle.get(new Ssl()), Collections.emptyMap());
|
||||
}).withMessageContaining("SSL is enabled but no trust material is configured");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void customizeWhenSslIsEnabledWithPkcs11AndKeyStoreThrowsException() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStoreType("PKCS11");
|
||||
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
|
||||
ssl.setKeyStore("classpath:test.jks");
|
||||
ssl.setKeyPassword("password");
|
||||
assertThatIllegalStateException().isThrownBy(() -> {
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, this.tomcat.getConnector(),
|
||||
ssl.getClientAuth());
|
||||
customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap());
|
||||
}).withMessageContaining("must be empty or null for PKCS11 hardware key stores");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeWhenSslIsEnabledWithPkcs11AndKeyStoreProvider() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStoreType("PKCS11");
|
||||
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
|
||||
ssl.setKeyStorePassword("1234");
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, this.tomcat.getConnector(),
|
||||
ssl.getClientAuth());
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.catalina.Service;
|
||||
import org.apache.catalina.connector.Connector;
|
||||
|
||||
/**
|
||||
* Helper class to provide public access to package-private methods for testing purposes.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public final class TomcatAccess {
|
||||
|
||||
private TomcatAccess() {
|
||||
|
||||
}
|
||||
|
||||
public static Map<Service, Connector[]> getServiceConnectors(TomcatWebServer tomcatWebServer) {
|
||||
return tomcatWebServer.getServiceConnectors();
|
||||
}
|
||||
|
||||
public static String getStartedLogMessage(TomcatWebServer tomcatWebServer) {
|
||||
return tomcatWebServer.getStartedLogMessage();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* 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.tomcat;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.apache.catalina.core.StandardContext;
|
||||
import org.apache.catalina.loader.ParallelWebappClassLoader;
|
||||
import org.apache.catalina.webresources.StandardRoot;
|
||||
import org.apache.catalina.webresources.WarResourceSet;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link TomcatEmbeddedWebappClassLoader}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class TomcatEmbeddedWebappClassLoaderTests {
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
@Test
|
||||
void getResourceFindsResourceFromParentClassLoader() throws Exception {
|
||||
File war = createWar();
|
||||
withWebappClassLoader(war, (classLoader) -> assertThat(classLoader.getResource("test.txt"))
|
||||
.isEqualTo(new URL(webInfClassesUrlString(war) + "test.txt")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getResourcesOnlyFindsResourcesFromParentClassLoader() throws Exception {
|
||||
File warFile = createWar();
|
||||
withWebappClassLoader(warFile, (classLoader) -> {
|
||||
List<URL> urls = new ArrayList<>();
|
||||
CollectionUtils.toIterator(classLoader.getResources("test.txt")).forEachRemaining(urls::add);
|
||||
assertThat(urls).containsExactly(new URL(webInfClassesUrlString(warFile) + "test.txt"));
|
||||
});
|
||||
}
|
||||
|
||||
private void withWebappClassLoader(File war, ClassLoaderConsumer consumer) throws Exception {
|
||||
URLClassLoader parent = new URLClassLoader(new URL[] { new URL(webInfClassesUrlString(war)) }, null);
|
||||
try (ParallelWebappClassLoader classLoader = new TomcatEmbeddedWebappClassLoader(parent)) {
|
||||
StandardContext context = new StandardContext();
|
||||
context.setName("test");
|
||||
StandardRoot resources = new StandardRoot();
|
||||
resources.setContext(context);
|
||||
resources.addJarResources(new WarResourceSet(resources, "/", war.getAbsolutePath()));
|
||||
resources.start();
|
||||
classLoader.setResources(resources);
|
||||
classLoader.start();
|
||||
try {
|
||||
consumer.accept(classLoader);
|
||||
}
|
||||
finally {
|
||||
classLoader.stop();
|
||||
classLoader.close();
|
||||
resources.stop();
|
||||
}
|
||||
}
|
||||
parent.close();
|
||||
}
|
||||
|
||||
private String webInfClassesUrlString(File war) {
|
||||
return "jar:file:" + war.getAbsolutePath() + "!/WEB-INF/classes/";
|
||||
}
|
||||
|
||||
private File createWar() throws IOException {
|
||||
File warFile = new File(this.tempDir, "test.war");
|
||||
try (JarOutputStream warOut = new JarOutputStream(new FileOutputStream(warFile))) {
|
||||
createEntries(warOut, "WEB-INF/", "WEB-INF/classes/", "WEB-INF/classes/test.txt");
|
||||
}
|
||||
return warFile;
|
||||
}
|
||||
|
||||
private void createEntries(JarOutputStream out, String... names) throws IOException {
|
||||
for (String name : names) {
|
||||
out.putNextEntry(new ZipEntry(name));
|
||||
out.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
interface ClassLoaderConsumer {
|
||||
|
||||
void accept(ClassLoader classLoader) throws Exception;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,10 +23,10 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories;
|
||||
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.jetty.JettyServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.tomcat.TomcatServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.undertow.UndertowServletWebServerFactory;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@@ -27,8 +27,8 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.tomcat.TomcatServletWebServerFactory;
|
||||
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
|
||||
import org.springframework.boot.web.servlet.support.ErrorPageFilterIntegrationTests.EmbeddedWebContextLoader;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBLjCB4aADAgECAhQ25wrNnapZEkFc8kgf5NDHXKxnTzAFBgMrZXAwDDEKMAgG
|
||||
A1UEAwwBMTAgFw0yMzEwMTAwODU1MTJaGA8yMTIzMDkxNjA4NTUxMlowDDEKMAgG
|
||||
A1UEAwwBMTAqMAUGAytlcAMhAOyxNxHzcNj7xTkcjVLI09sYUGUGIvdV5s0YWXT8
|
||||
XAiwo1MwUTAdBgNVHQ4EFgQUmm23oLIu5MgdBb/snZSuE+MrRZ0wHwYDVR0jBBgw
|
||||
FoAUmm23oLIu5MgdBb/snZSuE+MrRZ0wDwYDVR0TAQH/BAUwAwEB/zAFBgMrZXAD
|
||||
QQA2KMpIyySC8u4onW2MVW1iK2dJJZbMRaNMLlQuE+ZIHQLwflYW4sH/Pp76pboc
|
||||
QhqKXcO7xH7f2tD5hE2izcUB
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,3 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIJb1A+i5bmilBD9mUbhk1oFVI6FAZQGnhduv7xV6WWEc
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,9 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBLjCB4aADAgECAhR4TMDk3qg5sKREp16lEHR3bV3M9zAFBgMrZXAwDDEKMAgG
|
||||
A1UEAwwBMjAgFw0yMzEwMTAwODU1MjBaGA8yMTIzMDkxNjA4NTUyMFowDDEKMAgG
|
||||
A1UEAwwBMjAqMAUGAytlcAMhADPft6hzyCjHCe5wSprChuuO/CuPIJ2t+l4roS1D
|
||||
43/wo1MwUTAdBgNVHQ4EFgQUfrRibAWml4Ous4kpnBIggM2xnLcwHwYDVR0jBBgw
|
||||
FoAUfrRibAWml4Ous4kpnBIggM2xnLcwDwYDVR0TAQH/BAUwAwEB/zAFBgMrZXAD
|
||||
QQC/MOclal2Cp0B3kmaLbK0M8mapclIOJa78hzBkqPA3URClAF2GmF187wHqi7qV
|
||||
+xZ+KWv26pLJR46vk8Kc6ZIO
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,3 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEICxhres2Z2lICm7/isnm+2iNR12GmgG7KK86BNDZDeIF
|
||||
-----END PRIVATE KEY-----
|
||||
Binary file not shown.
Reference in New Issue
Block a user