Provide better diagnostics when an application fails to start
This commit introduces a new failure analysis mechanism that can be used to provide diagnostics to a user when their application fails to start. When application context refresh fails. FailureAnalyzer implementations are loaded via spring.factories. The analyzers are called in order, with the first non-null analysis of the failure being used. The analysis is reported to the use by FailureAnalysisReporters which are also loaded via spring.factories. A single FailureAnalysisReporter is provided out of the box. It logs an error message with details of the analysis, providing the user with a description of the failure and, if available, some actions that they may be able to take to resolve the problem without also displaying a stack trace that is of little value. Two analysers are provided initially. One for an embedded servlet container port clash and another for BeanCurrentlyInCreationExceptions. More analysers are planned (for UnsatisfiedDependencyException and for NoUniqueBeanDefinitionException) once some updates have been made to Spring Framework to make those failures more amenable to analysis. Closes gh-4907pull/5052/merge
parent
d454896755
commit
df6c204193
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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 extends Throwable> T findFailure(Throwable failure, Class<T> type) {
|
||||
Throwable candidate = failure;
|
||||
T mostSpecificMatch = null;
|
||||
while (candidate != null) {
|
||||
if (type.isInstance(candidate)) {
|
||||
mostSpecificMatch = (T) candidate;
|
||||
}
|
||||
candidate = candidate.getCause();
|
||||
}
|
||||
return mostSpecificMatch;
|
||||
}
|
||||
|
||||
}
|
@ -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<String> beansInCycle = new ArrayList<String>();
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue