Add Tomcat-specific failure analysis for connector start failures

Previously, when a Tomcat connector failed to start it was assumed that
the failure was due to the port being in use and a PortInUseException
was thrown. Unfortunately, this assumption doesn’t always hold true.
For example, a Tomcat connector will also fail to start when its using
SSL and the key store password is wrong. This could lead to incorrect
guidance from the PortInUseFailureAnalyzer indicating that a port clash
had occurred when, in fact, it was the SSL configuration that needed to
be corrected.

Unfortunately, Tomcat only tells us that the connector failed to start.
It doesn’t provide access to the exception that would allow us to
determine why it failed to start. This commit updates the embedded
Tomcat container to throw a ConnectorStartFailedException in the event
of a connector failing to start. A new failure analyser,
ConnectorStartFailureAnalyzer, has been introduced to analyse the new
exception and offer some more general guidance.

Closes gh-6896
This commit is contained in:
Andy Wilkinson 2016-09-20 13:55:10 +01:00
parent 1641bb2105
commit 564207b092
8 changed files with 127 additions and 7 deletions

View File

@ -0,0 +1,50 @@
/*
* Copyright 2012-2016 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
*
* http://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.context.embedded.tomcat;
import org.apache.catalina.connector.Connector;
import org.springframework.boot.context.embedded.EmbeddedServletContainerException;
/**
* 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 1.4.1
*/
public class ConnectorStartFailedException extends EmbeddedServletContainerException {
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;
}
}

View File

@ -36,7 +36,6 @@ import org.apache.naming.ContextBindings;
import org.springframework.boot.context.embedded.EmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerException; import org.springframework.boot.context.embedded.EmbeddedServletContainerException;
import org.springframework.boot.context.embedded.PortInUseException;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -185,7 +184,7 @@ public class TomcatEmbeddedServletContainer implements EmbeddedServletContainer
TomcatEmbeddedServletContainer.logger TomcatEmbeddedServletContainer.logger
.info("Tomcat started on port(s): " + getPortsDescription(true)); .info("Tomcat started on port(s): " + getPortsDescription(true));
} }
catch (PortInUseException ex) { catch (ConnectorStartFailedException ex) {
stopSilently(); stopSilently();
throw ex; throw ex;
} }
@ -203,7 +202,7 @@ public class TomcatEmbeddedServletContainer implements EmbeddedServletContainer
private void checkThatConnectorsHaveStarted() { private void checkThatConnectorsHaveStarted() {
for (Connector connector : this.tomcat.getService().findConnectors()) { for (Connector connector : this.tomcat.getService().findConnectors()) {
if (LifecycleState.FAILED.equals(connector.getState())) { if (LifecycleState.FAILED.equals(connector.getState())) {
throw new PortInUseException(connector.getPort()); throw new ConnectorStartFailedException(connector.getPort());
} }
} }
} }

View File

@ -0,0 +1,44 @@
/*
* Copyright 2012-2016 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
*
* http://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.diagnostics.analyzer;
import org.springframework.boot.context.embedded.tomcat.ConnectorStartFailedException;
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);
}
}

View File

@ -36,6 +36,7 @@ org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ConnectorStartFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer

View File

@ -840,8 +840,8 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests {
AbstractEmbeddedServletContainerFactoryTests.this.container.start(); AbstractEmbeddedServletContainerFactoryTests.this.container.start();
fail(); fail();
} }
catch (PortInUseException ex) { catch (RuntimeException ex) {
assertThat(ex.getPort()).isEqualTo(port); handleExceptionCausedByBlockedPort(ex, port);
} }
} }
@ -864,8 +864,8 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests {
AbstractEmbeddedServletContainerFactoryTests.this.container.start(); AbstractEmbeddedServletContainerFactoryTests.this.container.start();
fail(); fail();
} }
catch (PortInUseException ex) { catch (RuntimeException ex) {
assertThat(ex.getPort()).isEqualTo(port); handleExceptionCausedByBlockedPort(ex, port);
} }
} }
@ -886,6 +886,9 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests {
protected abstract void addConnector(int port, protected abstract void addConnector(int port,
AbstractEmbeddedServletContainerFactory factory); AbstractEmbeddedServletContainerFactory factory);
protected abstract void handleExceptionCausedByBlockedPort(RuntimeException ex,
int blockedPort);
private boolean doTestCompression(int contentSize, String[] mimeTypes, private boolean doTestCompression(int contentSize, String[] mimeTypes,
String[] excludedUserAgents) throws Exception { String[] excludedUserAgents) throws Exception {
String testContent = setUpFactoryForCompression(contentSize, mimeTypes, String testContent = setUpFactoryForCompression(contentSize, mimeTypes,

View File

@ -45,6 +45,7 @@ import org.mockito.InOrder;
import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactoryTests; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactoryTests;
import org.springframework.boot.context.embedded.Compression; import org.springframework.boot.context.embedded.Compression;
import org.springframework.boot.context.embedded.PortInUseException;
import org.springframework.boot.context.embedded.Ssl; import org.springframework.boot.context.embedded.Ssl;
import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@ -336,4 +337,11 @@ public class JettyEmbeddedServletContainerFactoryTests
return (charsetName != null) ? Charset.forName(charsetName) : null; return (charsetName != null) ? Charset.forName(charsetName) : null;
} }
@Override
protected void handleExceptionCausedByBlockedPort(RuntimeException ex,
int blockedPort) {
assertThat(ex).isInstanceOf(PortInUseException.class);
assertThat(((PortInUseException) ex).getPort()).isEqualTo(blockedPort);
}
} }

View File

@ -498,4 +498,11 @@ public class TomcatEmbeddedServletContainerFactoryTests
return ((TomcatEmbeddedServletContainer) this.container).getTomcat(); return ((TomcatEmbeddedServletContainer) this.container).getTomcat();
} }
@Override
protected void handleExceptionCausedByBlockedPort(RuntimeException ex,
int blockedPort) {
assertThat(ex).isInstanceOf(ConnectorStartFailedException.class);
assertThat(((ConnectorStartFailedException) ex).getPort()).isEqualTo(blockedPort);
}
} }

View File

@ -41,6 +41,7 @@ import org.springframework.boot.context.embedded.AbstractEmbeddedServletContaine
import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactoryTests; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactoryTests;
import org.springframework.boot.context.embedded.ExampleServlet; import org.springframework.boot.context.embedded.ExampleServlet;
import org.springframework.boot.context.embedded.MimeMappings.Mapping; import org.springframework.boot.context.embedded.MimeMappings.Mapping;
import org.springframework.boot.context.embedded.PortInUseException;
import org.springframework.boot.web.servlet.ErrorPage; import org.springframework.boot.web.servlet.ErrorPage;
import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -291,4 +292,11 @@ public class UndertowEmbeddedServletContainerFactoryTests
return (charsetName != null) ? Charset.forName(charsetName) : null; return (charsetName != null) ? Charset.forName(charsetName) : null;
} }
@Override
protected void handleExceptionCausedByBlockedPort(RuntimeException ex,
int blockedPort) {
assertThat(ex).isInstanceOf(PortInUseException.class);
assertThat(((PortInUseException) ex).getPort()).isEqualTo(blockedPort);
}
} }