Replace TomcatStarter to break servlet dependency

Replace `TomcatStarter` with a `DeferredStartupExceptions` interface
to break the direct dependency on `Servlet`.

Closes gh-44325
This commit is contained in:
Phillip Webb
2025-09-15 20:03:05 -07:00
parent 111418fbc5
commit 8651e971ea
4 changed files with 47 additions and 23 deletions
@@ -34,6 +34,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.boot.web.server.MimeMappings;
import org.springframework.boot.web.server.WebServerException;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
@@ -46,7 +47,7 @@ import org.springframework.util.ClassUtils;
*/
public class TomcatEmbeddedContext extends StandardContext {
private @Nullable TomcatStarter starter;
private DeferredStartupExceptions deferredStartupExceptions = DeferredStartupExceptions.NONE;
private @Nullable MimeMappings mimeMappings;
@@ -116,12 +117,17 @@ public class TomcatEmbeddedContext extends StandardContext {
}
}
public void setStarter(@Nullable TomcatStarter starter) {
this.starter = starter;
/**
* Set the a strategy used to capture and rethrow deferred startup exceptions.
* @param deferredStartupExceptions the strategy to use
*/
public void setDeferredStartupExceptions(DeferredStartupExceptions deferredStartupExceptions) {
Assert.notNull(deferredStartupExceptions, "'deferredStartupExceptions' must not be null");
this.deferredStartupExceptions = deferredStartupExceptions;
}
@Nullable TomcatStarter getStarter() {
return this.starter;
DeferredStartupExceptions getDeferredStartupExceptions() {
return this.deferredStartupExceptions;
}
public void setMimeMappings(MimeMappings mimeMappings) {
@@ -146,4 +152,24 @@ public class TomcatEmbeddedContext extends StandardContext {
return (this.mimeMappings != null) ? this.mimeMappings.get(extension) : null;
}
/**
* Strategy interface that can be used to rethrow deferred startup exceptions.
*/
@FunctionalInterface
public interface DeferredStartupExceptions {
/**
* {@link DeferredStartupExceptions} that does nothing.
*/
DeferredStartupExceptions NONE = () -> {
};
/**
* Rethrow deferred startup exceptions if there are any.
* @throws Exception the deferred startup exception
*/
void rethrow() throws Exception;
}
}
@@ -200,13 +200,7 @@ public class TomcatWebServer implements WebServer {
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;
}
}
embeddedContext.getDeferredStartupExceptions().rethrow();
}
if (!LifecycleState.STARTED.equals(container.getState())) {
throw new IllegalStateException(container + " failed to start");
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.tomcat;
package org.springframework.boot.tomcat.servlet;
import java.util.Set;
@@ -25,6 +25,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.tomcat.TomcatEmbeddedContext;
import org.springframework.boot.web.servlet.ServletContextInitializer;
/**
@@ -33,17 +34,17 @@ import org.springframework.boot.web.servlet.ServletContextInitializer;
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 4.0.0
*/
public class TomcatStarter implements ServletContainerInitializer {
class DeferredServletContainerInitializers
implements ServletContainerInitializer, TomcatEmbeddedContext.DeferredStartupExceptions {
private static final Log logger = LogFactory.getLog(TomcatStarter.class);
private static final Log logger = LogFactory.getLog(DeferredServletContainerInitializers.class);
private final Iterable<ServletContextInitializer> initializers;
private volatile @Nullable Exception startUpException;
public TomcatStarter(Iterable<ServletContextInitializer> initializers) {
DeferredServletContainerInitializers(Iterable<ServletContextInitializer> initializers) {
this.initializers = initializers;
}
@@ -65,8 +66,11 @@ public class TomcatStarter implements ServletContainerInitializer {
}
}
@Nullable Exception getStartUpException() {
return this.startUpException;
@Override
public void rethrow() throws Exception {
if (this.startUpException != null) {
throw this.startUpException;
}
}
}
@@ -70,7 +70,6 @@ import org.springframework.boot.tomcat.DisableReferenceClearingContextCustomizer
import org.springframework.boot.tomcat.TomcatContextCustomizer;
import org.springframework.boot.tomcat.TomcatEmbeddedContext;
import org.springframework.boot.tomcat.TomcatEmbeddedWebappClassLoader;
import org.springframework.boot.tomcat.TomcatStarter;
import org.springframework.boot.tomcat.TomcatWebServer;
import org.springframework.boot.tomcat.TomcatWebServerFactory;
import org.springframework.boot.web.error.ErrorPage;
@@ -289,12 +288,13 @@ public class TomcatServletWebServerFactory extends TomcatWebServerFactory
* @param initializers initializers to apply
*/
protected void configureContext(Context context, Iterable<ServletContextInitializer> initializers) {
TomcatStarter starter = new TomcatStarter(initializers);
DeferredServletContainerInitializers deferredInitializers = new DeferredServletContainerInitializers(
initializers);
if (context instanceof TomcatEmbeddedContext embeddedContext) {
embeddedContext.setStarter(starter);
embeddedContext.setDeferredStartupExceptions(deferredInitializers);
embeddedContext.setFailCtxIfServletStartFails(true);
}
context.addServletContainerInitializer(starter, NO_CLASSES);
context.addServletContainerInitializer(deferredInitializers, NO_CLASSES);
for (LifecycleListener lifecycleListener : this.getContextLifecycleListeners()) {
context.addLifecycleListener(lifecycleListener);
}