diff --git a/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 0dc61339ee6..7ba27ad5451 100644 --- a/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -41,6 +41,9 @@ import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.diagnostics.FailureAnalysisReporter; +import org.springframework.boot.diagnostics.FailureAnalyzer; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ApplicationListener; @@ -812,16 +815,13 @@ public class SpringApplication { private void handleRunFailure(ConfigurableApplicationContext context, SpringApplicationRunListeners listeners, Throwable exception) { - if (logger.isErrorEnabled()) { - logger.error("Application startup failed", exception); - registerLoggedException(exception); - } try { try { handleExitCode(context, exception); listeners.finished(context, exception); } finally { + reportFailure(exception); if (context != null) { context.close(); } @@ -833,6 +833,47 @@ public class SpringApplication { ReflectionUtils.rethrowRuntimeException(exception); } + private void reportFailure(Throwable failure) { + try { + FailureAnalysis failureAnalysis = analyzeFailure(failure); + if (failureAnalysis != null && reportFailureAnalysis(failureAnalysis)) { + registerLoggedException(failure); + return; + } + } + catch (Throwable ex) { + // Continue with normal handling of the original failure + } + if (logger.isErrorEnabled()) { + logger.error("Application startup failed", failure); + registerLoggedException(failure); + } + } + + private FailureAnalysis analyzeFailure(Throwable failure) { + List analyzers = SpringFactoriesLoader + .loadFactories(FailureAnalyzer.class, getClass().getClassLoader()); + for (FailureAnalyzer analyzer : analyzers) { + FailureAnalysis analysis = analyzer.analyze(failure); + if (analysis != null) { + return analysis; + } + } + return null; + } + + private boolean reportFailureAnalysis(FailureAnalysis failureAnalysis) { + List reporters = SpringFactoriesLoader.loadFactories( + FailureAnalysisReporter.class, getClass().getClassLoader()); + if (!reporters.isEmpty()) { + for (FailureAnalysisReporter reporter : reporters) { + reporter.report(failureAnalysis); + } + return true; + } + return false; + } + /** * Register that the given exception has been logged. By default, if the running in * the main thread, this method will suppress additional printing of the stacktrace. diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/PortInUseException.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/PortInUseException.java new file mode 100644 index 00000000000..3b37ff9b9a6 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/PortInUseException.java @@ -0,0 +1,49 @@ +/* + * 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; + +/** + * A {@code PortInUseException} is thrown when an embedded servlet container fails to + * start due to a port already being in use. + * + * @author Andy Wilkinson + * @since 1.4.0 + */ +public class PortInUseException extends EmbeddedServletContainerException { + + private final int port; + + /** + * Creates a new port in use exception for the given {@code port}. + * + * @param port the port that was in use + */ + public PortInUseException(int port) { + super("Port " + port + " is already in use", null); + this.port = port; + } + + /** + * Returns the port that was in use. + * + * @return the port + */ + public int getPort() { + return this.port; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/PortInUseFailureAnalyzer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/PortInUseFailureAnalyzer.java new file mode 100644 index 00000000000..e77bddf7918 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/PortInUseFailureAnalyzer.java @@ -0,0 +1,47 @@ +/* + * 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; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; + +/** + * A {@code FailureAnalyzer} that performs analysis of failures caused by a + * {@code PortInUseException}. + * + * @author Andy Wilkinson + * @since 1.4.0 + */ +public class PortInUseFailureAnalyzer extends AbstractFailureAnalyzer { + + @Override + public FailureAnalysis analyze(Throwable failure) { + PortInUseException portInUseException = findFailure(failure, + PortInUseException.class); + if (portInUseException != null) { + return new FailureAnalysis( + "Embedded servlet container failed to start. Port " + + portInUseException.getPort() + " was already in use.", + "Identify and stop the process that's listening on port " + + portInUseException.getPort() + " or configure this " + + "application to listen on another port.", + portInUseException); + } + return null; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainer.java index 0e91d0b7d75..85b80231269 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainer.java @@ -16,18 +16,21 @@ package org.springframework.boot.context.embedded.jetty; +import java.net.BindException; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.NetworkConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.server.handler.HandlerWrapper; import org.springframework.boot.context.embedded.EmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainerException; +import org.springframework.boot.context.embedded.PortInUseException; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -115,11 +118,23 @@ public class JettyEmbeddedServletContainer implements EmbeddedServletContainer { } Connector[] connectors = this.server.getConnectors(); for (Connector connector : connectors) { - connector.start(); + try { + connector.start(); + } + catch (BindException ex) { + if (connector instanceof NetworkConnector) { + throw new PortInUseException( + ((NetworkConnector) connector).getPort()); + } + throw ex; + } } JettyEmbeddedServletContainer.logger .info("Jetty started on port(s) " + getActualPortsDescription()); } + catch (EmbeddedServletContainerException ex) { + throw ex; + } catch (Exception ex) { throw new EmbeddedServletContainerException( "Unable to start embedded Jetty servlet container", ex); diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainer.java index 95a3eb98004..2359a74ae92 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainer.java @@ -33,6 +33,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.boot.context.embedded.EmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainerException; +import org.springframework.boot.context.embedded.PortInUseException; import org.springframework.util.Assert; /** @@ -166,27 +167,28 @@ public class TomcatEmbeddedServletContainer implements EmbeddedServletContainer if (connector != null && this.autoStart) { startConnector(connector); } - // Ensure process isn't left running if it actually failed to start - if (connectorsHaveFailedToStart()) { - stopSilently(); - throw new IllegalStateException("Tomcat connector in failed state"); - } + + checkThatConnectorsHaveStarted(); + TomcatEmbeddedServletContainer.logger .info("Tomcat started on port(s): " + getPortsDescription(true)); } + catch (PortInUseException ex) { + stopSilently(); + throw ex; + } catch (Exception ex) { throw new EmbeddedServletContainerException( "Unable to start embedded Tomcat servlet container", ex); } } - private boolean connectorsHaveFailedToStart() { + private void checkThatConnectorsHaveStarted() { for (Connector connector : this.tomcat.getService().findConnectors()) { if (LifecycleState.FAILED.equals(connector.getState())) { - return true; + throw new PortInUseException(connector.getPort()); } } - return false; } private void stopSilently() { diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java index 728619febc2..e7a4ab8ca4d 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java @@ -17,6 +17,7 @@ package org.springframework.boot.context.embedded.undertow; import java.lang.reflect.Field; +import java.net.BindException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.ArrayList; @@ -45,6 +46,7 @@ import org.xnio.channels.BoundChannel; import org.springframework.boot.context.embedded.Compression; import org.springframework.boot.context.embedded.EmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainerException; +import org.springframework.boot.context.embedded.PortInUseException; import org.springframework.http.HttpHeaders; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -123,12 +125,32 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine UndertowEmbeddedServletContainer.logger .info("Undertow started on port(s) " + getPortsDescription()); } - catch (ServletException ex) { + catch (Exception ex) { + if (findBindException(ex) != null) { + List failedPorts = getConfiguredPorts(); + List actualPorts = getActualPorts(); + failedPorts.removeAll(actualPorts); + if (failedPorts.size() == 1) { + throw new PortInUseException( + failedPorts.iterator().next().getNumber()); + } + } throw new EmbeddedServletContainerException( "Unable to start embedded Undertow", ex); } } + private BindException findBindException(Exception ex) { + Throwable candidate = ex; + while (candidate != null) { + if (candidate instanceof BindException) { + return (BindException) candidate; + } + candidate = candidate.getCause(); + } + return null; + } + private Undertow createUndertowServer() throws ServletException { HttpHandler httpHandler = this.manager.start(); httpHandler = getContextHandler(httpHandler); @@ -175,14 +197,14 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine } private String getPortsDescription() { - List ports = getPorts(); + List ports = getActualPorts(); if (!ports.isEmpty()) { return StringUtils.collectionToDelimitedString(ports, " "); } return "unknown"; } - private List getPorts() { + private List getActualPorts() { List ports = new ArrayList(); try { if (!this.autoStart) { @@ -218,6 +240,36 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine return null; } + private List getConfiguredPorts() { + List ports = new ArrayList(); + for (Object listener : extractListeners()) { + try { + ports.add(getPortFromListener(listener)); + } + catch (Exception ex) { + // Continue + } + } + return ports; + } + + @SuppressWarnings("unchecked") + private List extractListeners() { + Field listenersField = ReflectionUtils.findField(Undertow.class, "listeners"); + ReflectionUtils.makeAccessible(listenersField); + return (List) ReflectionUtils.getField(listenersField, this.undertow); + } + + private Port getPortFromListener(Object listener) { + Field typeField = ReflectionUtils.findField(listener.getClass(), "type"); + ReflectionUtils.makeAccessible(typeField); + String protocol = ReflectionUtils.getField(typeField, listener).toString(); + Field portField = ReflectionUtils.findField(listener.getClass(), "port"); + ReflectionUtils.makeAccessible(portField); + int port = (Integer) ReflectionUtils.getField(portField, listener); + return new Port(port, protocol); + } + @Override public synchronized void stop() throws EmbeddedServletContainerException { if (this.started) { @@ -235,7 +287,7 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine @Override public int getPort() { - List ports = getPorts(); + List ports = getActualPorts(); if (ports.isEmpty()) { return 0; } @@ -243,7 +295,7 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine } /** - * An active undertow port. + * An active Undertow port. */ private final static class Port { @@ -265,6 +317,29 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine return this.number + " (" + this.protocol + ")"; } + @Override + public int hashCode() { + return this.number; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Port other = (Port) obj; + if (this.number != other.number) { + return false; + } + return true; + } + } private static class CompressibleMimeTypePredicate implements Predicate { diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/AbstractFailureAnalyzer.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/AbstractFailureAnalyzer.java new file mode 100644 index 00000000000..d131b77f310 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/AbstractFailureAnalyzer.java @@ -0,0 +1,40 @@ +/* + * 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; + +/** + * Abstract base class for most {@code FailureAnalyzer} implementations. + * + * @author Andy Wilkinson + * @since 1.4.0 + */ +public abstract class AbstractFailureAnalyzer implements FailureAnalyzer { + + @SuppressWarnings("unchecked") + protected T findFailure(Throwable failure, Class type) { + Throwable candidate = failure; + T mostSpecificMatch = null; + while (candidate != null) { + if (type.isInstance(candidate)) { + mostSpecificMatch = (T) candidate; + } + candidate = candidate.getCause(); + } + return mostSpecificMatch; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/BeanCurrentlyInCreationFailureAnalyzer.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/BeanCurrentlyInCreationFailureAnalyzer.java new file mode 100644 index 00000000000..c750148b8b7 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/BeanCurrentlyInCreationFailureAnalyzer.java @@ -0,0 +1,87 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanCurrentlyInCreationException; +import org.springframework.util.StringUtils; + +/** + * A {@link FailureAnalyzer} the performs analysis of failures caused by a + * {@link BeanCurrentlyInCreationException}. + * + * @author Andy Wilkinson + * @since 1.4.0 + */ +public class BeanCurrentlyInCreationFailureAnalyzer extends AbstractFailureAnalyzer { + + private static final String FIELD_AUTOWIRING_FAILURE_MESSAGE_PREFIX = "Could not autowire field: "; + + @Override + public FailureAnalysis analyze(Throwable failure) { + BeanCurrentlyInCreationException inCreationEx = findFailure(failure, + BeanCurrentlyInCreationException.class); + if (inCreationEx != null) { + List beansInCycle = new ArrayList(); + Throwable candidate = failure; + while (candidate != null) { + if (candidate instanceof BeanCreationException) { + BeanCreationException creationEx = (BeanCreationException) candidate; + if (StringUtils.hasText(creationEx.getBeanName())) { + beansInCycle.add( + creationEx.getBeanName() + getDescription(creationEx)); + } + } + candidate = candidate.getCause(); + } + StringBuilder message = new StringBuilder(); + int uniqueBeans = beansInCycle.size() - 1; + message.append(String + .format("There is a circular dependency between %s beans in the " + + "application context:%n", uniqueBeans)); + for (String bean : beansInCycle) { + message.append(String.format("\t- %s%n", bean)); + } + + return new FailureAnalysis(message.toString(), null, failure); + } + return null; + } + + private String getDescription(BeanCreationException ex) { + if (StringUtils.hasText(ex.getResourceDescription())) { + return String.format(" defined in %s", ex.getResourceDescription()); + } + else if (causedByFieldAutowiringFailure(ex)) { + return String.format(" (field %s)", + ex.getCause().getMessage().substring( + FIELD_AUTOWIRING_FAILURE_MESSAGE_PREFIX.length(), + ex.getCause().getMessage().indexOf(";"))); + + } + return ""; + } + + private boolean causedByFieldAutowiringFailure(BeanCreationException ex) { + return ex.getCause() instanceof BeanCreationException && ex.getCause() + .getMessage().startsWith(FIELD_AUTOWIRING_FAILURE_MESSAGE_PREFIX); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalysis.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalysis.java new file mode 100644 index 00000000000..004a0e97165 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalysis.java @@ -0,0 +1,75 @@ +/* + * 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; + +/** + * The result of analyzing a failure. + * + * @author Andy Wilkinson + * @since 1.4.0 + */ +public class FailureAnalysis { + + private final String description; + + private final String action; + + private final Throwable cause; + + /** + * Creates a new {@code FailureAnalysis} with the given {@code description} and + * {@code action}, if any, that the user should take to address the problem. The + * failure had the given underlying {@code cause}. + * + * @param description the description + * @param action the action + * @param cause the cause + */ + public FailureAnalysis(String description, String action, Throwable cause) { + this.description = description; + this.action = action; + this.cause = cause; + } + + /** + * Returns a description of the failure. + * + * @return the description + */ + public String getDescription() { + return this.description; + } + + /** + * Returns the action, if any, to be taken to address the failure. + * + * @return the action or {@code null} + */ + public String getAction() { + return this.action; + } + + /** + * Returns the cause of the failure. + * + * @return the cause + */ + public Throwable getCause() { + return this.cause; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalysisReporter.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalysisReporter.java new file mode 100644 index 00000000000..a4ae4f43669 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalysisReporter.java @@ -0,0 +1,34 @@ +/* + * 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; + +/** + * Reports a {@code FailureAnalysis} to the user. + * + * @author Andy Wilkinson + * @since 1.4.0 + */ +public interface FailureAnalysisReporter { + + /** + * Reports the given {@code failureAnalysis} to the user. + * + * @param failureAnalysis the analysis + */ + void report(FailureAnalysis failureAnalysis); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzer.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzer.java new file mode 100644 index 00000000000..f210da47aaa --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzer.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * A {@code FailureAnalyzer} is used to analyze a failure and provide diagnostic + * information that can be displayed to the user. + * + * @author Andy Wilkinson + * @since 1.4.0 + */ +public interface FailureAnalyzer { + + /** + * Returns an analysis of the given {@code failure}, or {@code null} if no analysis + * was possible. + * + * @param failure the failure + * @return the analysis or {@code null} + */ + FailureAnalysis analyze(Throwable failure); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/LoggingFailureAnalysisReporter.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/LoggingFailureAnalysisReporter.java new file mode 100644 index 00000000000..7bfca1c5c23 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/LoggingFailureAnalysisReporter.java @@ -0,0 +1,61 @@ +/* + * 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; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.StringUtils; + +/** + * {@link FailureAnalysisReporter} that logs the failure analysis. + * + * @author Andy Wilkinson + * @since 1.4.0 + */ +public final class LoggingFailureAnalysisReporter implements FailureAnalysisReporter { + + private static final Log logger = LogFactory + .getLog(LoggingFailureAnalysisReporter.class); + + @Override + public void report(FailureAnalysis failureAnalysis) { + if (logger.isDebugEnabled()) { + logger.debug("Application failed to start due to an exception", + failureAnalysis.getCause()); + } + if (logger.isErrorEnabled()) { + logger.error(buildMessage(failureAnalysis)); + } + } + + private String buildMessage(FailureAnalysis failureAnalysis) { + StringBuilder builder = new StringBuilder(); + builder.append(String.format("%n%n")); + builder.append(String.format("***************************%n")); + builder.append(String.format("APPLICATION FAILED TO START%n")); + builder.append(String.format("***************************%n%n")); + builder.append(String.format("Description:%n%n")); + builder.append(String.format("%s%n", failureAnalysis.getDescription())); + if (StringUtils.hasText(failureAnalysis.getAction())) { + builder.append(String.format("%nAction:%n%n")); + builder.append(String.format("%s%n", failureAnalysis.getAction())); + } + return builder.toString(); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/ClasspathLoggingApplicationListener.java b/spring-boot/src/main/java/org/springframework/boot/logging/ClasspathLoggingApplicationListener.java index 68eb3f6a90d..18d3d119b8d 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/ClasspathLoggingApplicationListener.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/ClasspathLoggingApplicationListener.java @@ -31,9 +31,8 @@ import org.springframework.core.ResolvableType; /** * A {@link SmartApplicationListener} that reacts to {@link ApplicationStartedEvent start - * events} by logging the classpath of the thread context class loader (TCCL) at - * {@code DEBUG} level and to {@link ApplicationFailedEvent error events} by logging the - * TCCL's classpath at {@code INFO} level. + * events} and to {@link ApplicationFailedEvent failed events} by logging the classpath of + * the thread context class loader (TCCL) at {@code DEBUG} level. * * @author Andy Wilkinson */ @@ -46,15 +45,13 @@ public final class ClasspathLoggingApplicationListener @Override public void onApplicationEvent(ApplicationEvent event) { - if (event instanceof ApplicationStartedEvent) { - if (this.logger.isDebugEnabled()) { + if (this.logger.isDebugEnabled()) { + if (event instanceof ApplicationStartedEvent) { this.logger .debug("Application started with classpath: " + getClasspath()); } - } - else if (event instanceof ApplicationFailedEvent) { - if (this.logger.isInfoEnabled()) { - this.logger.info( + else if (event instanceof ApplicationFailedEvent) { + this.logger.debug( "Application failed to start with classpath: " + getClasspath()); } } diff --git a/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot/src/main/resources/META-INF/spring.factories index da1d266b2f1..5f2f151964f 100644 --- a/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot/src/main/resources/META-INF/spring.factories @@ -30,3 +30,12 @@ org.springframework.boot.logging.LoggingApplicationListener org.springframework.boot.env.EnvironmentPostProcessor=\ org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\ org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor + +# Failure Analyzers +org.springframework.boot.diagnostics.FailureAnalyzer=\ +org.springframework.boot.context.embedded.PortInUseFailureAnalyzer,\ +org.springframework.boot.diagnostics.BeanCurrentlyInCreationFailureAnalyzer + +# FailureAnalysisReporters +org.springframework.boot.diagnostics.FailureAnalysisReporter=\ +org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java index b44a79d1b1e..892ce92e2d2 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java @@ -23,7 +23,9 @@ import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; +import java.net.InetSocketAddress; import java.net.MalformedURLException; +import java.net.ServerSocket; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -81,6 +83,7 @@ import org.springframework.util.StreamUtils; import org.springframework.util.concurrent.ListenableFuture; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; import static org.mockito.Matchers.anyObject; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -715,6 +718,64 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { assertThat(response.getHeaders().getFirst("server")).isEqualTo("MyServer"); } + @Test + public void portClashOfPrimaryConnectorResultsInPortInUseException() + throws IOException { + AbstractEmbeddedServletContainerFactory factory = getFactory(); + + final int port = SocketUtils.findAvailableTcpPort(40000); + + factory.setPort(port); + + this.container = factory.getEmbeddedServletContainer(); + + doWithBlockedPort(port, new Runnable() { + + @Override + public void run() { + try { + AbstractEmbeddedServletContainerFactoryTests.this.container.start(); + fail(); + } + catch (PortInUseException ex) { + assertThat(ex.getPort()).isEqualTo(port); + } + } + + }); + + } + + @Test + public void portClashOfSecondaryConnectorResultsInPortInUseException() + throws IOException { + AbstractEmbeddedServletContainerFactory factory = getFactory(); + factory.setPort(SocketUtils.findAvailableTcpPort(40000)); + + final int port = SocketUtils.findAvailableTcpPort(40000); + + addConnector(port, factory); + this.container = factory.getEmbeddedServletContainer(); + + doWithBlockedPort(port, new Runnable() { + + @Override + public void run() { + try { + AbstractEmbeddedServletContainerFactoryTests.this.container.start(); + fail(); + } + catch (PortInUseException ex) { + assertThat(ex.getPort()).isEqualTo(port); + } + } + + }); + } + + protected abstract void addConnector(int port, + AbstractEmbeddedServletContainerFactory factory); + private boolean doTestCompression(int contentSize, String[] mimeTypes, String[] excludedUserAgents) throws Exception { String testContent = setUpFactoryForCompression(contentSize, mimeTypes, @@ -878,6 +939,18 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { return bean; } + protected final void doWithBlockedPort(final int port, Runnable action) + throws IOException { + ServerSocket serverSocket = new ServerSocket(); + serverSocket.bind(new InetSocketAddress(port)); + try { + action.run(); + } + finally { + serverSocket.close(); + } + } + private class TestGzipInputStreamFactory implements InputStreamFactory { private final AtomicBoolean requested = new AtomicBoolean(false); diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java index a2daa9433fd..72b8e7ded28 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java @@ -136,6 +136,22 @@ public class JettyEmbeddedServletContainerFactoryTests .containsExactly("ALPHA", "BRAVO", "CHARLIE"); } + @Override + protected void addConnector(final int port, + AbstractEmbeddedServletContainerFactory factory) { + ((JettyEmbeddedServletContainerFactory) factory) + .addServerCustomizers(new JettyServerCustomizer() { + + @Override + public void customize(Server server) { + ServerConnector connector = new ServerConnector(server); + connector.setPort(port); + server.addConnector(connector); + } + + }); + } + private void assertTimeout(JettyEmbeddedServletContainerFactory factory, int expected) { this.container = factory.getEmbeddedServletContainer(); diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java index b2ddd7e12f0..18d2f0c04f8 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java @@ -17,9 +17,6 @@ package org.springframework.boot.context.embedded.tomcat; import java.io.File; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.ServerSocket; import java.nio.charset.Charset; import java.util.Arrays; import java.util.HashMap; @@ -42,14 +39,13 @@ import org.junit.After; import org.junit.Test; import org.mockito.InOrder; +import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactoryTests; -import org.springframework.boot.context.embedded.EmbeddedServletContainerException; import org.springframework.boot.context.embedded.Ssl; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.SocketUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.fail; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyObject; @@ -264,61 +260,13 @@ public class TomcatEmbeddedServletContainerFactoryTests assertThat(jsseProtocol.getCiphers()).isEqualTo("ALPHA,BRAVO,CHARLIE"); } - @Test - public void primaryConnectorPortClashThrowsIllegalStateException() - throws InterruptedException, IOException { - final int port = SocketUtils.findAvailableTcpPort(40000); - - doWithBlockedPort(port, new Runnable() { - - @Override - public void run() { - TomcatEmbeddedServletContainerFactory factory = getFactory(); - factory.setPort(port); - - try { - TomcatEmbeddedServletContainerFactoryTests.this.container = factory - .getEmbeddedServletContainer(); - TomcatEmbeddedServletContainerFactoryTests.this.container.start(); - fail(); - } - catch (EmbeddedServletContainerException ex) { - // Ignore - } - } - - }); - - } - - @Test - public void additionalConnectorPortClashThrowsIllegalStateException() - throws InterruptedException, IOException { - final int port = SocketUtils.findAvailableTcpPort(40000); - - doWithBlockedPort(port, new Runnable() { - - @Override - public void run() { - TomcatEmbeddedServletContainerFactory factory = getFactory(); - Connector connector = new Connector( - "org.apache.coyote.http11.Http11NioProtocol"); - connector.setPort(port); - factory.addAdditionalTomcatConnectors(connector); - - try { - TomcatEmbeddedServletContainerFactoryTests.this.container = factory - .getEmbeddedServletContainer(); - TomcatEmbeddedServletContainerFactoryTests.this.container.start(); - fail(); - } - catch (EmbeddedServletContainerException ex) { - // Ignore - } - } - - }); - + @Override + protected void addConnector(int port, + AbstractEmbeddedServletContainerFactory factory) { + Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); + connector.setPort(port); + ((TomcatEmbeddedServletContainerFactory) factory) + .addAdditionalTomcatConnectors(connector); } @Test @@ -403,15 +351,4 @@ public class TomcatEmbeddedServletContainerFactoryTests return ((TomcatEmbeddedServletContainer) this.container).getTomcat(); } - private void doWithBlockedPort(final int port, Runnable action) throws IOException { - ServerSocket serverSocket = new ServerSocket(); - serverSocket.bind(new InetSocketAddress(port)); - try { - action.run(); - } - finally { - serverSocket.close(); - } - } - } diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactoryTests.java index a8ffc224b39..a42d7e31238 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactoryTests.java @@ -188,6 +188,19 @@ public class UndertowEmbeddedServletContainerFactoryTests assertThat(accessLogDirectory.listFiles()).contains(accessLog); } + @Override + protected void addConnector(final int port, + AbstractEmbeddedServletContainerFactory factory) { + ((UndertowEmbeddedServletContainerFactory) factory) + .addBuilderCustomizers(new UndertowBuilderCustomizer() { + + @Override + public void customize(Builder builder) { + builder.addHttpListener(port, "0.0.0.0"); + } + }); + } + @Override protected Object getJspServlet() { return null; // Undertow does not support JSPs diff --git a/spring-boot/src/test/java/org/springframework/boot/diagnostics/BeanCurrentlyInCreationFailureAnalyzerTests.java b/spring-boot/src/test/java/org/springframework/boot/diagnostics/BeanCurrentlyInCreationFailureAnalyzerTests.java new file mode 100644 index 00000000000..8732f095044 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/diagnostics/BeanCurrentlyInCreationFailureAnalyzerTests.java @@ -0,0 +1,152 @@ +/* + * 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; + +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +/** + * Tests for {@link BeanCurrentlyInCreationFailureAnalyzer} + * + * @author Andy Wilkinson + */ +public class BeanCurrentlyInCreationFailureAnalyzerTests { + + private final FailureAnalyzer analyzer = new BeanCurrentlyInCreationFailureAnalyzer(); + + @Test + public void cyclicBeanMethods() { + FailureAnalysis analysis = performAnalysis(CyclicBeanMethodsConfiguration.class); + assertThat(analysis.getDescription()).startsWith( + "There is a circular dependency between 3 beans in the application context:"); + assertThat(analysis.getDescription()).contains( + "one defined in " + CyclicBeanMethodsConfiguration.class.getName()); + assertThat(analysis.getDescription()).contains( + "two defined in " + CyclicBeanMethodsConfiguration.class.getName()); + assertThat(analysis.getDescription()).contains( + "three defined in " + CyclicBeanMethodsConfiguration.class.getName()); + } + + @Test + public void cycleWithAutowiredFields() { + FailureAnalysis analysis = performAnalysis(CycleWithAutowiredFields.class); + assertThat(analysis.getDescription()).startsWith( + "There is a circular dependency between 3 beans in the application context:"); + assertThat(analysis.getDescription()).contains("three defined in " + + CycleWithAutowiredFields.BeanThreeConfiguration.class); + assertThat(analysis.getDescription()) + .contains("one defined in " + CycleWithAutowiredFields.class.getName()); + assertThat(analysis.getDescription()) + .contains(CycleWithAutowiredFields.BeanTwoConfiguration.class.getName() + + " (field private " + BeanThree.class.getName()); + } + + private FailureAnalysis performAnalysis(Class configuration) { + FailureAnalysis analysis = this.analyzer.analyze(createFailure(configuration)); + assertThat(analysis).isNotNull(); + return analysis; + } + + private Exception createFailure(Class configuration) { + ConfigurableApplicationContext context = null; + try { + context = new AnnotationConfigApplicationContext(configuration); + } + catch (Exception ex) { + ex.printStackTrace(); + return ex; + } + finally { + if (context != null) { + context.close(); + } + } + fail("Expected failure did not occur"); + return null; + } + + @Configuration + static class CyclicBeanMethodsConfiguration { + + @Bean + public BeanOne one(BeanTwo two) { + return new BeanOne(); + } + + @Bean + public BeanTwo two(BeanThree three) { + return new BeanTwo(); + } + + @Bean + public BeanThree three(BeanOne one) { + return new BeanThree(); + } + + } + + @Configuration + public static class CycleWithAutowiredFields { + + @Bean + public BeanOne one(BeanTwo two) { + return new BeanOne(); + } + + public static class BeanTwoConfiguration { + + @SuppressWarnings("unused") + @Autowired + private BeanThree three; + + @Bean + public BeanTwo two() { + return new BeanTwo(); + } + } + + public static class BeanThreeConfiguration { + + @Bean + public BeanThree three(BeanOne one) { + return new BeanThree(); + } + } + + } + + static class BeanOne { + + } + + static class BeanTwo { + + } + + static class BeanThree { + + } + +}