Handle servlet startup failures consistently
Ensure that all servlet containers handle servlet startup failures consistently and throw a `WebServerException` that wraps the original cause. Both Undertow and Jetty already dealt with startup failures in this way, but Tomcat did not. The `TomcatEmbeddedContext` has now been changed to no longer call `super.loadOnStartup` but instead re-implement a version of that method that wraps and rethrows the original exception (as long as `failCtxIfServletStartFails` is `true`, which it now is by default). Closes gh-14790
This commit is contained in:
@@ -16,13 +16,23 @@
|
||||
|
||||
package org.springframework.boot.web.embedded.tomcat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
|
||||
import org.apache.catalina.Container;
|
||||
import org.apache.catalina.LifecycleException;
|
||||
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.util.Assert;
|
||||
import org.springframework.boot.web.server.WebServerException;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
@@ -52,11 +62,37 @@ class TomcatEmbeddedContext extends StandardContext {
|
||||
|
||||
public void deferredLoadOnStartup() throws LifecycleException {
|
||||
doWithThreadContextClassLoader(getLoader().getClassLoader(), () -> {
|
||||
boolean started = super.loadOnStartup(findChildren());
|
||||
Assert.state(started, "Unable to start embedded tomcat context " + getName());
|
||||
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, ArrayList::new);
|
||||
grouped.get(order).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
|
||||
|
||||
@@ -329,8 +329,9 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto
|
||||
ServletContextInitializer[] initializers) {
|
||||
TomcatStarter starter = new TomcatStarter(initializers);
|
||||
if (context instanceof TomcatEmbeddedContext) {
|
||||
// Should be true
|
||||
((TomcatEmbeddedContext) context).setStarter(starter);
|
||||
TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
|
||||
embeddedContext.setStarter(starter);
|
||||
embeddedContext.setFailCtxIfServletStartFails(true);
|
||||
}
|
||||
context.addServletContainerInitializer(starter, NO_CLASSES);
|
||||
for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) {
|
||||
|
||||
@@ -284,7 +284,9 @@ public class TomcatWebServer implements WebServer {
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
logger.error("Cannot start connector: ", ex);
|
||||
if (ex instanceof WebServerException) {
|
||||
throw (WebServerException) ex;
|
||||
}
|
||||
throw new WebServerException("Unable to start embedded Tomcat connectors",
|
||||
ex);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import java.util.Set;
|
||||
import javax.naming.InitialContext;
|
||||
import javax.naming.NamingException;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
|
||||
import org.apache.catalina.Container;
|
||||
import org.apache.catalina.Context;
|
||||
@@ -440,6 +439,20 @@ public class TomcatServletWebServerFactoryTests
|
||||
.isThrownBy(this.webServer::start);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exceptionThrownOnLoadFailureWhenFailCtxIfServletStartFailsIsFalse() {
|
||||
TomcatServletWebServerFactory factory = getFactory();
|
||||
factory.addContextCustomizers((context) -> {
|
||||
if (context instanceof StandardContext) {
|
||||
((StandardContext) context).setFailCtxIfServletStartFails(false);
|
||||
}
|
||||
});
|
||||
this.webServer = factory.getWebServer((context) -> {
|
||||
context.addServlet("failing", FailingServlet.class).setLoadOnStartup(0);
|
||||
});
|
||||
this.webServer.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JspServlet getJspServlet() throws ServletException {
|
||||
Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat();
|
||||
@@ -488,13 +501,4 @@ public class TomcatServletWebServerFactoryTests
|
||||
assertThat(((ConnectorStartFailedException) ex).getPort()).isEqualTo(blockedPort);
|
||||
}
|
||||
|
||||
static class FailingServlet extends HttpServlet {
|
||||
|
||||
@Override
|
||||
public void init() throws ServletException {
|
||||
throw new RuntimeException("Init Failure");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1046,6 +1046,28 @@ public abstract class AbstractServletWebServerFactoryTests {
|
||||
verify(listener).contextDestroyed(any(ServletContextEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exceptionThrownOnLoadFailureIsRethrown() {
|
||||
AbstractServletWebServerFactory factory = getFactory();
|
||||
this.webServer = factory.getWebServer((context) -> {
|
||||
context.addServlet("failing", FailingServlet.class).setLoadOnStartup(0);
|
||||
});
|
||||
assertThatExceptionOfType(WebServerException.class)
|
||||
.isThrownBy(this.webServer::start)
|
||||
.satisfies(this::wrapsFailingServletException);
|
||||
}
|
||||
|
||||
private void wrapsFailingServletException(WebServerException ex) {
|
||||
Throwable cause = ex.getCause();
|
||||
while (cause != null) {
|
||||
if (cause instanceof FailingServletException) {
|
||||
return;
|
||||
}
|
||||
cause = cause.getCause();
|
||||
}
|
||||
fail("Exception did not wrap FailingServletException");
|
||||
}
|
||||
|
||||
protected abstract void addConnector(int port,
|
||||
AbstractServletWebServerFactory factory);
|
||||
|
||||
@@ -1344,4 +1366,21 @@ public abstract class AbstractServletWebServerFactoryTests {
|
||||
|
||||
}
|
||||
|
||||
public static class FailingServlet extends HttpServlet {
|
||||
|
||||
@Override
|
||||
public void init() throws ServletException {
|
||||
throw new FailingServletException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class FailingServletException extends RuntimeException {
|
||||
|
||||
FailingServletException() {
|
||||
super("Init Failure");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user