Skip to content

Commit 683e953

Browse files
committed
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
1 parent 4823114 commit 683e953

File tree

5 files changed

+98
-16
lines changed

5 files changed

+98
-16
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatEmbeddedContext.java

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,23 @@
1616

1717
package org.springframework.boot.web.embedded.tomcat;
1818

19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.TreeMap;
23+
import java.util.stream.Stream;
24+
25+
import javax.servlet.ServletException;
26+
1927
import org.apache.catalina.Container;
2028
import org.apache.catalina.LifecycleException;
2129
import org.apache.catalina.Manager;
30+
import org.apache.catalina.Wrapper;
2231
import org.apache.catalina.core.StandardContext;
32+
import org.apache.catalina.core.StandardWrapper;
2333
import org.apache.catalina.session.ManagerBase;
2434

25-
import org.springframework.util.Assert;
35+
import org.springframework.boot.web.server.WebServerException;
2636
import org.springframework.util.ClassUtils;
2737

2838
/**
@@ -52,11 +62,37 @@ public void setManager(Manager manager) {
5262

5363
public void deferredLoadOnStartup() throws LifecycleException {
5464
doWithThreadContextClassLoader(getLoader().getClassLoader(), () -> {
55-
boolean started = super.loadOnStartup(findChildren());
56-
Assert.state(started, "Unable to start embedded tomcat context " + getName());
65+
getLoadOnStartupWrappers(findChildren()).forEach(this::load);
5766
});
5867
}
5968

69+
private Stream<Wrapper> getLoadOnStartupWrappers(Container[] children) {
70+
Map<Integer, List<Wrapper>> grouped = new TreeMap<>();
71+
for (Container child : children) {
72+
Wrapper wrapper = (Wrapper) child;
73+
int order = wrapper.getLoadOnStartup();
74+
if (order >= 0) {
75+
grouped.computeIfAbsent(order, ArrayList::new);
76+
grouped.get(order).add(wrapper);
77+
}
78+
}
79+
return grouped.values().stream().flatMap(List::stream);
80+
}
81+
82+
private void load(Wrapper wrapper) {
83+
try {
84+
wrapper.load();
85+
}
86+
catch (ServletException ex) {
87+
String message = sm.getString("standardContext.loadOnStartup.loadException",
88+
getName(), wrapper.getName());
89+
if (getComputedFailCtxIfServletStartFails()) {
90+
throw new WebServerException(message, ex);
91+
}
92+
getLogger().error(message, StandardWrapper.getRootCause(ex));
93+
}
94+
}
95+
6096
/**
6197
* Some older Servlet frameworks (e.g. Struts, BIRT) use the Thread context class
6298
* loader to create servlet instances in this phase. If they do that and then try to

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,9 @@ protected void configureContext(Context context,
329329
ServletContextInitializer[] initializers) {
330330
TomcatStarter starter = new TomcatStarter(initializers);
331331
if (context instanceof TomcatEmbeddedContext) {
332-
// Should be true
333-
((TomcatEmbeddedContext) context).setStarter(starter);
332+
TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
333+
embeddedContext.setStarter(starter);
334+
embeddedContext.setFailCtxIfServletStartFails(true);
334335
}
335336
context.addServletContainerInitializer(starter, NO_CLASSES);
336337
for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) {

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,9 @@ private void performDeferredLoadOnStartup() {
284284
}
285285
}
286286
catch (Exception ex) {
287-
logger.error("Cannot start connector: ", ex);
287+
if (ex instanceof WebServerException) {
288+
throw (WebServerException) ex;
289+
}
288290
throw new WebServerException("Unable to start embedded Tomcat connectors",
289291
ex);
290292
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import javax.naming.InitialContext;
3131
import javax.naming.NamingException;
3232
import javax.servlet.ServletException;
33-
import javax.servlet.http.HttpServlet;
3433

3534
import org.apache.catalina.Container;
3635
import org.apache.catalina.Context;
@@ -440,6 +439,20 @@ public void exceptionThrownOnLoadFailureWhenFailCtxIfServletStartFailsIsTrue() {
440439
.isThrownBy(this.webServer::start);
441440
}
442441

442+
@Test
443+
public void exceptionThrownOnLoadFailureWhenFailCtxIfServletStartFailsIsFalse() {
444+
TomcatServletWebServerFactory factory = getFactory();
445+
factory.addContextCustomizers((context) -> {
446+
if (context instanceof StandardContext) {
447+
((StandardContext) context).setFailCtxIfServletStartFails(false);
448+
}
449+
});
450+
this.webServer = factory.getWebServer((context) -> {
451+
context.addServlet("failing", FailingServlet.class).setLoadOnStartup(0);
452+
});
453+
this.webServer.start();
454+
}
455+
443456
@Override
444457
protected JspServlet getJspServlet() throws ServletException {
445458
Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat();
@@ -488,13 +501,4 @@ protected void handleExceptionCausedByBlockedPort(RuntimeException ex,
488501
assertThat(((ConnectorStartFailedException) ex).getPort()).isEqualTo(blockedPort);
489502
}
490503

491-
static class FailingServlet extends HttpServlet {
492-
493-
@Override
494-
public void init() throws ServletException {
495-
throw new RuntimeException("Init Failure");
496-
}
497-
498-
}
499-
500504
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,28 @@ public void servletContextListenerContextDestroyedIsCalledWhenContainerIsStopped
10461046
verify(listener).contextDestroyed(any(ServletContextEvent.class));
10471047
}
10481048

1049+
@Test
1050+
public void exceptionThrownOnLoadFailureIsRethrown() {
1051+
AbstractServletWebServerFactory factory = getFactory();
1052+
this.webServer = factory.getWebServer((context) -> {
1053+
context.addServlet("failing", FailingServlet.class).setLoadOnStartup(0);
1054+
});
1055+
assertThatExceptionOfType(WebServerException.class)
1056+
.isThrownBy(this.webServer::start)
1057+
.satisfies(this::wrapsFailingServletException);
1058+
}
1059+
1060+
private void wrapsFailingServletException(WebServerException ex) {
1061+
Throwable cause = ex.getCause();
1062+
while (cause != null) {
1063+
if (cause instanceof FailingServletException) {
1064+
return;
1065+
}
1066+
cause = cause.getCause();
1067+
}
1068+
fail("Exception did not wrap FailingServletException");
1069+
}
1070+
10491071
protected abstract void addConnector(int port,
10501072
AbstractServletWebServerFactory factory);
10511073

@@ -1344,4 +1366,21 @@ public boolean isTrusted(X509Certificate[] chain, String authType)
13441366

13451367
}
13461368

1369+
public static class FailingServlet extends HttpServlet {
1370+
1371+
@Override
1372+
public void init() throws ServletException {
1373+
throw new FailingServletException();
1374+
}
1375+
1376+
}
1377+
1378+
private static class FailingServletException extends RuntimeException {
1379+
1380+
FailingServletException() {
1381+
super("Init Failure");
1382+
}
1383+
1384+
}
1385+
13471386
}

0 commit comments

Comments
 (0)