docs
This documentation has been enhanced for AI coding agents with comprehensive examples, complete API signatures, thread safety notes, error handling patterns, and production-ready usage patterns.
Package: org.springframework.boot.diagnostics
Module: org.springframework.boot:spring-boot
Since: 1.4.0
Spring Boot's diagnostics package provides a framework for analyzing application startup failures and providing actionable error messages. When an application fails to start, failure analyzers examine the exception and produce human-readable descriptions along with suggested actions.
The failure analysis system transforms technical exceptions into user-friendly diagnostics through:
SPI interface for analyzing application failures. Implementations are discovered via SpringFactoriesLoader.
package org.springframework.boot.diagnostics;
/**
* A FailureAnalyzer is used to analyze a failure and provide diagnostic
* information that can be displayed to the user.
*
* Implementations are loaded via META-INF/spring.factories and invoked
* when startup fails.
*
* Thread Safety: Implementations should be stateless and thread-safe.
*
* @since 1.4.0
*/
@FunctionalInterface
public interface FailureAnalyzer {
/**
* Returns an analysis of the given failure, or null if no analysis
* was possible.
*
* @param failure the failure (never null)
* @return the analysis or null if this analyzer cannot handle the failure
*/
FailureAnalysis analyze(Throwable failure);
}Registration in META-INF/spring.factories:
org.springframework.boot.diagnostics.FailureAnalyzer=\
com.example.CustomFailureAnalyzerResult object encapsulating failure analysis.
package org.springframework.boot.diagnostics;
/**
* The result of analyzing a failure.
*
* Immutable value object containing failure description and action.
*
* @since 1.4.0
*/
public class FailureAnalysis {
private final String description;
private final String action;
private final Throwable cause;
/**
* Creates a new FailureAnalysis with the given description and action.
*
* @param description the description (what went wrong)
* @param action the action to fix it (can be null)
* @param cause the underlying cause (never null)
*/
public FailureAnalysis(String description, String action, Throwable cause) {
this.description = (description != null) ? description : "";
this.action = action;
this.cause = cause;
}
/**
* Returns a description of the failure.
*
* @return the description (never null, may be empty)
*/
public String getDescription() {
return this.description;
}
/**
* Returns the action to address the failure.
*
* @return the action or null
*/
public String getAction() {
return this.action;
}
/**
* Returns the cause of the failure.
*
* @return the cause (never null)
*/
public Throwable getCause() {
return this.cause;
}
}Output Format:
***************************
APPLICATION FAILED TO START
***************************
Description:
[description text]
Action:
[action text]Base class simplifying analyzer implementation by handling exception chain walking.
package org.springframework.boot.diagnostics;
/**
* Abstract base class for most FailureAnalyzer implementations.
* Automatically extracts the target exception type from generic parameter
* and walks the exception chain.
*
* @param <T> the type of exception to analyze
* @since 1.4.0
*/
public abstract class AbstractFailureAnalyzer<T extends Throwable>
implements FailureAnalyzer {
@Override
public FailureAnalysis analyze(Throwable failure) {
T cause = findCause(failure, getCauseType());
return (cause != null) ? analyze(failure, cause) : null;
}
/**
* Returns an analysis of the given failure.
*
* @param rootFailure the root failure passed to the analyzer
* @param cause the actual found cause of the specified type
* @return the analysis or null
*/
protected abstract FailureAnalysis analyze(Throwable rootFailure, T cause);
/**
* Return the cause type being handled by the analyzer.
* By default extracted from generic parameter.
*
* @return the cause type
*/
protected Class<? extends T> getCauseType() {
// Extracts T from AbstractFailureAnalyzer<T>
}
/**
* Find the first occurrence of the given type in the exception chain.
*
* @param <E> the exception type
* @param failure the root failure
* @param type the type to search for
* @return the matching exception or null
*/
protected final <E extends Throwable> E findCause(
Throwable failure,
Class<E> type) {
// Walks exception chain via getCause()
}
}Specialized base class for analyzing injection-related failures, providing descriptive information about the injection point.
package org.springframework.boot.diagnostics.analyzer;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
/**
* Abstract base class for FailureAnalyzer that handles injection failures.
* Automatically extracts and formats injection point information from
* UnsatisfiedDependencyException and BeanInstantiationException.
*
* Provides a description of where the injection failed (field, constructor,
* or method parameter) which is passed to the analyze method.
*
* @param <T> the type of exception to analyze
* @since 1.4.1
*/
public abstract class AbstractInjectionFailureAnalyzer<T extends Throwable>
extends AbstractFailureAnalyzer<T> {
/**
* Returns an analysis of the given failure.
*
* @param rootFailure the root failure passed to the analyzer
* @param cause the actual found cause of the specified type
* @param description the description of the injection point (e.g.,
* "Field myService in com.example.MyClass" or
* "Parameter 0 of constructor in com.example.MyClass")
* or null if no injection point information available
* @return the analysis or null
*/
protected abstract FailureAnalysis analyze(
Throwable rootFailure,
T cause,
String description);
}Injection Point Description Formats:
The description parameter provides context about where the injection failed:
"Field myService in com.example.MyClass""Parameter 0 of constructor in com.example.MyClass""Parameter 1 of method setDatabase in com.example.MyClass"When to use: Use this base class when analyzing failures related to dependency injection, such as missing beans, circular dependencies, or bean creation errors.
package com.example.diagnostics;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
/**
* Analyzer for custom database connection exceptions.
*/
public class DatabaseConnectionFailureAnalyzer
extends AbstractFailureAnalyzer<DatabaseConnectionException> {
@Override
protected FailureAnalysis analyze(
Throwable rootFailure,
DatabaseConnectionException cause) {
String description = String.format(
"Failed to connect to database '%s' at %s:%d%n%n" +
"Reason: %s",
cause.getDatabase(),
cause.getHost(),
cause.getPort(),
cause.getMessage()
);
String action = String.format(
"Verify database connection settings:%n%n" +
" spring.datasource.url=jdbc:postgresql://%s:%d/%s%n" +
" spring.datasource.username=<username>%n" +
" spring.datasource.password=<password>%n%n" +
"Ensure the database is running and accessible.",
cause.getHost(),
cause.getPort(),
cause.getDatabase()
);
return new FailureAnalysis(description, action, cause);
}
}Registration:
# META-INF/spring.factories
org.springframework.boot.diagnostics.FailureAnalyzer=\
com.example.diagnostics.DatabaseConnectionFailureAnalyzerpackage com.example.diagnostics;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
/**
* Analyzer handling multiple network exception types.
*/
public class NetworkFailureAnalyzer extends AbstractFailureAnalyzer<IOException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, IOException cause) {
if (cause instanceof ConnectException) {
return analyzeConnectionRefused((ConnectException) cause);
}
else if (cause instanceof SocketTimeoutException) {
return analyzeTimeout((SocketTimeoutException) cause);
}
else if (cause instanceof UnknownHostException) {
return analyzeUnknownHost((UnknownHostException) cause);
}
return null; // Cannot handle this IOException subtype
}
private FailureAnalysis analyzeConnectionRefused(ConnectException ex) {
return new FailureAnalysis(
"Connection refused when connecting to remote service",
"Ensure the remote service is running and firewall allows connections",
ex
);
}
private FailureAnalysis analyzeTimeout(SocketTimeoutException ex) {
return new FailureAnalysis(
"Timeout when connecting to remote service",
"Check network connectivity and increase timeout values if needed:\n\n" +
" spring.datasource.hikari.connection-timeout=30000",
ex
);
}
private FailureAnalysis analyzeUnknownHost(UnknownHostException ex) {
return new FailureAnalysis(
"Unknown host: " + ex.getMessage(),
"Verify the hostname in your configuration and DNS settings",
ex
);
}
}package com.example.diagnostics;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import java.util.List;
import java.util.stream.Collectors;
/**
* Analyzer using Environment for enhanced diagnostics.
*/
public class PropertyMissingFailureAnalyzer
extends AbstractFailureAnalyzer<PropertyNotFoundException>
implements EnvironmentAware {
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
protected FailureAnalysis analyze(
Throwable rootFailure,
PropertyNotFoundException cause) {
String propertyName = cause.getPropertyName();
List<String> suggestions = findSimilarProperties(propertyName);
String description = String.format(
"Property '%s' not found in configuration",
propertyName
);
StringBuilder action = new StringBuilder();
action.append("Add the property to your configuration:\n\n");
action.append(" ").append(propertyName).append("=<value>\n\n");
if (!suggestions.isEmpty()) {
action.append("Did you mean one of these?\n\n");
suggestions.forEach(s ->
action.append(" ").append(s).append("\n")
);
}
return new FailureAnalysis(description, action.toString(), cause);
}
private List<String> findSimilarProperties(String target) {
// Find properties with similar names (Levenshtein distance)
return List.of(); // Simplified
}
}Example using AbstractInjectionFailureAnalyzer to analyze missing bean dependencies:
package com.example.diagnostics;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.diagnostics.analyzer.AbstractInjectionFailureAnalyzer;
/**
* Analyzer for NoSuchBeanDefinitionException providing injection point context.
*/
public class MissingBeanFailureAnalyzer
extends AbstractInjectionFailureAnalyzer<NoSuchBeanDefinitionException> {
@Override
protected FailureAnalysis analyze(
Throwable rootFailure,
NoSuchBeanDefinitionException cause,
String description) {
String beanType = cause.getBeanType().getName();
// Use description to show WHERE the injection failed
String detailedDescription = String.format(
"Failed to inject required bean of type '%s'%n%n" +
"Injection point: %s%n%n" +
"Reason: No qualifying bean of type '%s' available",
beanType,
(description != null) ? description : "Unknown location",
beanType
);
String action = String.format(
"Consider defining a bean of type '%s' in your configuration:%n%n" +
" @Bean%n" +
" public %s myBean() {%n" +
" return new %sImpl();%n" +
" }%n%n" +
"Or ensure that a component scan includes the package containing " +
"the implementation.",
beanType,
cause.getBeanType().getSimpleName(),
cause.getBeanType().getSimpleName()
);
return new FailureAnalysis(detailedDescription, action, cause);
}
}Example Output:
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to inject required bean of type 'com.example.service.UserService'
Injection point: Field userService in com.example.controller.UserController
Reason: No qualifying bean of type 'com.example.service.UserService' available
Action:
Consider defining a bean of type 'com.example.service.UserService' in your configuration:
@Bean
public UserService myBean() {
return new UserServiceImpl();
}
Or ensure that a component scan includes the package containing the implementation.Benefits of AbstractInjectionFailureAnalyzer:
Analyzes circular dependency errors.
public class BeanCurrentlyInCreationFailureAnalyzer
extends AbstractFailureAnalyzer<BeanCurrentlyInCreationException> {
@Override
protected FailureAnalysis analyze(
Throwable rootFailure,
BeanCurrentlyInCreationException cause) {
// Builds circular dependency chain description
}
}Example Output:
Description:
The dependencies of some of the beans form a cycle:
┌─────┐
| serviceA
↑ ↓
| serviceB
└─────┘
Action:
Consider injecting one of the beans as a @Lazy proxy to break the cycle.Analyzes missing bean errors.
Example Output:
Description:
Field repository in com.example.UserService required a bean of type
'com.example.UserRepository' that could not be found.
Action:
Consider defining a bean of type 'com.example.UserRepository' in your configuration.Analyzes configuration property binding failures.
Example Output:
Description:
Binding to target org.example.MyProperties failed:
Property: server.port
Value: "invalid"
Reason: failed to convert java.lang.String to int
Action:
Update your application's configuration. The expected type is 'int'.package com.example.diagnostics;
import org.junit.jupiter.api.Test;
import org.springframework.boot.diagnostics.FailureAnalysis;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for DatabaseConnectionFailureAnalyzer.
*/
class DatabaseConnectionFailureAnalyzerTests {
private final DatabaseConnectionFailureAnalyzer analyzer =
new DatabaseConnectionFailureAnalyzer();
@Test
void analyzesDatabaseConnectionException() {
DatabaseConnectionException ex = new DatabaseConnectionException(
"Connection refused", "localhost", 5432, "mydb"
);
FailureAnalysis analysis = analyzer.analyze(ex);
assertThat(analysis).isNotNull();
assertThat(analysis.getDescription())
.contains("Failed to connect to database 'mydb'")
.contains("localhost:5432");
assertThat(analysis.getAction())
.contains("spring.datasource.url")
.contains("Ensure the database is running");
assertThat(analysis.getCause()).isEqualTo(ex);
}
@Test
void returnsNullForUnrelatedExceptions() {
RuntimeException ex = new RuntimeException("Unrelated error");
FailureAnalysis analysis = analyzer.analyze(ex);
assertThat(analysis).isNull();
}
@Test
void findsExceptionInChain() {
DatabaseConnectionException dbEx = new DatabaseConnectionException(
"Connection refused", "localhost", 5432, "mydb"
);
RuntimeException wrapper = new RuntimeException("Wrapper", dbEx);
IllegalStateException outer = new IllegalStateException("Outer", wrapper);
FailureAnalysis analysis = analyzer.analyze(outer);
assertThat(analysis).isNotNull();
assertThat(analysis.getCause()).isEqualTo(dbEx);
}
}| Analyzer | Exception Type | Handles |
|---|---|---|
BeanCurrentlyInCreationFailureAnalyzer | BeanCurrentlyInCreationException | Circular dependencies |
BeanNotOfRequiredTypeFailureAnalyzer | BeanNotOfRequiredTypeException | Type mismatches |
BindFailureAnalyzer | BindException | Property binding errors |
ConfigDataNotFoundFailureAnalyzer | ConfigDataNotFoundException | Missing config files |
NoSuchBeanDefinitionFailureAnalyzer | NoSuchBeanDefinitionException | Missing beans |
NoUniqueBeanDefinitionFailureAnalyzer | NoUniqueBeanDefinitionException | Multiple bean candidates |
Comprehensive analyzer handling multiple related exception types with rich diagnostic information:
package com.example.diagnostics;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.core.env.Environment;
import org.springframework.context.EnvironmentAware;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.sql.SQLTransientException;
import java.sql.SQLNonTransientException;
/**
* Comprehensive database failure analyzer with detailed diagnostics.
* Analyzes SQL exceptions and provides specific guidance based on error type.
*/
public class DatabaseFailureAnalyzer extends AbstractFailureAnalyzer<SQLException>
implements EnvironmentAware {
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
protected FailureAnalysis analyze(Throwable rootFailure, SQLException cause) {
if (cause instanceof SQLTransientException) {
return analyzeTransientError((SQLTransientException) cause);
} else if (cause instanceof SQLNonTransientException) {
return analyzeNonTransientError((SQLNonTransientException) cause);
} else {
return analyzeGenericSqlError(cause);
}
}
private FailureAnalysis analyzeTransientError(SQLTransientException ex) {
String description = buildTransientErrorDescription(ex);
String action = buildTransientErrorAction(ex);
return new FailureAnalysis(description, action, ex);
}
private String buildTransientErrorDescription(SQLTransientException ex) {
StringBuilder desc = new StringBuilder();
desc.append("A transient database error occurred during application startup.\n\n");
desc.append("SQL State: ").append(ex.getSQLState()).append("\n");
desc.append("Error Code: ").append(ex.getErrorCode()).append("\n");
desc.append("Message: ").append(ex.getMessage()).append("\n\n");
// Add context from environment
String url = environment.getProperty("spring.datasource.url");
String username = environment.getProperty("spring.datasource.username");
if (url != null) {
desc.append("Connection URL: ").append(maskPassword(url)).append("\n");
}
if (username != null) {
desc.append("Username: ").append(username).append("\n");
}
return desc.toString();
}
private String buildTransientErrorAction(SQLTransientException ex) {
StringBuilder action = new StringBuilder();
action.append("This is a temporary error that may resolve on retry.\n\n");
action.append("Recommended actions:\n\n");
action.append("1. Verify the database is running and accessible:\n");
action.append(" - Check database server status\n");
action.append(" - Verify network connectivity\n");
action.append(" - Check firewall rules\n\n");
action.append("2. Configure connection retry in application.properties:\n\n");
action.append(" spring.datasource.hikari.connection-timeout=30000\n");
action.append(" spring.datasource.hikari.initialization-fail-timeout=30000\n\n");
action.append("3. Consider enabling connection pool retry logic:\n\n");
action.append(" spring.datasource.hikari.max-lifetime=600000\n");
action.append(" spring.datasource.hikari.connection-test-query=SELECT 1\n");
return action.toString();
}
private FailureAnalysis analyzeNonTransientError(SQLNonTransientException ex) {
String errorCode = ex.getSQLState();
if (errorCode != null && errorCode.startsWith("28")) {
// Authentication error
return analyzeAuthenticationError(ex);
} else if (errorCode != null && errorCode.startsWith("42")) {
// Syntax error or access rule violation
return analyzeSyntaxError(ex);
} else {
return analyzeGenericNonTransientError(ex);
}
}
private FailureAnalysis analyzeAuthenticationError(SQLNonTransientException ex) {
String description = String.format(
"Database authentication failed.%n%n" +
"SQL State: %s%n" +
"Error Code: %d%n" +
"Message: %s%n%n" +
"Current configuration:%n" +
" URL: %s%n" +
" Username: %s",
ex.getSQLState(),
ex.getErrorCode(),
ex.getMessage(),
maskPassword(environment.getProperty("spring.datasource.url", "not configured")),
environment.getProperty("spring.datasource.username", "not configured")
);
String action =
"Update your database credentials in application.properties:\n\n" +
" spring.datasource.url=jdbc:postgresql://localhost:5432/mydb\n" +
" spring.datasource.username=<valid-username>\n" +
" spring.datasource.password=<valid-password>\n\n" +
"Security note: Use environment variables for sensitive data:\n\n" +
" spring.datasource.username=${DB_USERNAME}\n" +
" spring.datasource.password=${DB_PASSWORD}\n\n" +
"Verify the user has necessary permissions:\n" +
" - GRANT ALL PRIVILEGES ON DATABASE mydb TO username;";
return new FailureAnalysis(description, action, ex);
}
private FailureAnalysis analyzeSyntaxError(SQLNonTransientException ex) {
String description = String.format(
"Database schema or SQL syntax error detected.%n%n" +
"SQL State: %s%n" +
"Error Code: %d%n" +
"Message: %s",
ex.getSQLState(),
ex.getErrorCode(),
ex.getMessage()
);
String action =
"Check the following:\n\n" +
"1. Ensure database schema is up to date:\n" +
" - Run any pending migrations\n" +
" - Verify table structures match entity definitions\n\n" +
"2. Check Hibernate DDL auto configuration:\n\n" +
" spring.jpa.hibernate.ddl-auto=validate # or update for development\n\n" +
"3. Enable SQL logging to see generated queries:\n\n" +
" spring.jpa.show-sql=true\n" +
" logging.level.org.hibernate.SQL=DEBUG";
return new FailureAnalysis(description, action, ex);
}
private FailureAnalysis analyzeGenericNonTransientError(SQLNonTransientException ex) {
String description = String.format(
"A non-transient database error occurred.%n%n" +
"SQL State: %s%n" +
"Error Code: %d%n" +
"Message: %s",
ex.getSQLState(),
ex.getErrorCode(),
ex.getMessage()
);
String action =
"Review the error details above and:\n\n" +
"1. Check database server logs for more information\n" +
"2. Verify database configuration and permissions\n" +
"3. Consult database vendor documentation for SQL state: " + ex.getSQLState();
return new FailureAnalysis(description, action, ex);
}
private FailureAnalysis analyzeGenericSqlError(SQLException ex) {
String description = String.format(
"Database error during application startup.%n%n" +
"Error: %s",
ex.getMessage()
);
String action =
"Verify database configuration:\n\n" +
" spring.datasource.url=jdbc:<database>://<host>:<port>/<database-name>\n" +
" spring.datasource.username=<username>\n" +
" spring.datasource.password=<password>\n" +
" spring.datasource.driver-class-name=<driver-class>";
return new FailureAnalysis(description, action, ex);
}
private String maskPassword(String url) {
if (url == null) return "null";
// Simple password masking for JDBC URLs
return url.replaceAll("password=[^&;]+", "password=****");
}
}Analyzer that walks exception chains and identifies the most relevant cause:
package com.example.diagnostics;
import org.springframework.boot.diagnostics.FailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
/**
* Analyzes complex exception hierarchies to find root causes.
* Walks the entire exception chain and identifies the most actionable error.
*/
public class RootCauseFailureAnalyzer implements FailureAnalyzer {
@Override
public FailureAnalysis analyze(Throwable failure) {
List<Throwable> chain = buildExceptionChain(failure);
// Check for specific known issues in order of priority
for (Throwable throwable : chain) {
FailureAnalysis analysis = analyzeThrowable(throwable, failure);
if (analysis != null) {
return analysis;
}
}
return null; // Cannot handle this failure
}
private List<Throwable> buildExceptionChain(Throwable failure) {
List<Throwable> chain = new ArrayList<>();
Throwable current = failure;
while (current != null && !chain.contains(current)) {
chain.add(current);
current = current.getCause();
}
return chain;
}
private FailureAnalysis analyzeThrowable(Throwable throwable, Throwable rootFailure) {
// Check for connection issues
if (throwable instanceof ConnectException) {
return analyzeConnectionRefused((ConnectException) throwable, rootFailure);
}
// Check for timeout issues
if (throwable instanceof SocketTimeoutException) {
return analyzeSocketTimeout((SocketTimeoutException) throwable, rootFailure);
}
// Check for class loading issues
if (throwable instanceof ClassNotFoundException ||
throwable instanceof NoClassDefFoundError) {
return analyzeClassNotFound(throwable, rootFailure);
}
// Check for port already in use
if (throwable.getMessage() != null &&
throwable.getMessage().contains("Address already in use")) {
return analyzePortInUse(throwable, rootFailure);
}
return null;
}
private FailureAnalysis analyzeConnectionRefused(
ConnectException ex,
Throwable rootFailure) {
String description = String.format(
"Application failed to connect to a required service.%n%n" +
"Root cause: Connection refused%n" +
"Details: %s%n%n" +
"This typically means the target service is not running or not accessible.",
ex.getMessage()
);
String action =
"Check the following:\n\n" +
"1. Verify the target service is running\n" +
"2. Check the connection URL/port in your configuration\n" +
"3. Ensure firewall allows the connection\n" +
"4. For database connections:\n" +
" - Verify spring.datasource.url\n" +
" - Check database server status\n" +
"5. For external services:\n" +
" - Verify service endpoint URLs\n" +
" - Check network connectivity";
return new FailureAnalysis(description, action, rootFailure);
}
private FailureAnalysis analyzeSocketTimeout(
SocketTimeoutException ex,
Throwable rootFailure) {
String description = String.format(
"Connection timeout while connecting to a service.%n%n" +
"Details: %s%n%n" +
"The service did not respond within the configured timeout period.",
ex.getMessage()
);
String action =
"Resolve the timeout issue:\n\n" +
"1. Verify the service is responding normally\n" +
"2. Check network latency and connectivity\n" +
"3. Increase connection timeout:\n\n" +
" For DataSource:\n" +
" spring.datasource.hikari.connection-timeout=30000\n\n" +
" For RestTemplate:\n" +
" Use custom RestTemplate with longer timeouts\n\n" +
"4. If this is startup initialization:\n" +
" spring.datasource.hikari.initialization-fail-timeout=60000";
return new FailureAnalysis(description, action, rootFailure);
}
private FailureAnalysis analyzeClassNotFound(
Throwable ex,
Throwable rootFailure) {
String className = extractClassName(ex);
String description = String.format(
"Required class not found on classpath.%n%n" +
"Missing class: %s%n%n" +
"This usually indicates a missing dependency or incorrect classpath.",
className
);
String action = String.format(
"Add the missing dependency to your pom.xml or build.gradle:%n%n" +
"If this is a JDBC driver, add:%n" +
" <!-- PostgreSQL example -->%n" +
" <dependency>%n" +
" <groupId>org.postgresql</groupId>%n" +
" <artifactId>postgresql</artifactId>%n" +
" </dependency>%n%n" +
"If this is a Spring Boot starter, add:%n" +
" <dependency>%n" +
" <groupId>org.springframework.boot</groupId>%n" +
" <artifactId>spring-boot-starter-[component]</artifactId>%n" +
" </dependency>%n%n" +
"Run 'mvn clean install' or 'gradle clean build' after adding dependencies."
);
return new FailureAnalysis(description, action, rootFailure);
}
private FailureAnalysis analyzePortInUse(
Throwable ex,
Throwable rootFailure) {
String description = String.format(
"Cannot start application - port already in use.%n%n" +
"Details: %s%n%n" +
"Another application is using the configured port.",
ex.getMessage()
);
String action =
"Choose one of the following options:\n\n" +
"1. Stop the other application using the port\n\n" +
"2. Change the application port in application.properties:\n\n" +
" server.port=8081\n\n" +
"3. Use a random available port:\n\n" +
" server.port=0\n\n" +
"4. Find which process is using the port:\n\n" +
" On Linux/Mac: lsof -i :<port>\n" +
" On Windows: netstat -ano | findstr :<port>";
return new FailureAnalysis(description, action, rootFailure);
}
private String extractClassName(Throwable ex) {
if (ex instanceof ClassNotFoundException) {
return ex.getMessage();
} else if (ex instanceof NoClassDefFoundError) {
String message = ex.getMessage();
// Convert internal format to class name
return message != null ? message.replace('/', '.') : "unknown";
}
return "unknown";
}
}Validates configuration properties and provides specific guidance:
package com.example.diagnostics;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.context.properties.bind.BindException;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.core.env.Environment;
import org.springframework.context.EnvironmentAware;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* Analyzes configuration binding failures with detailed guidance.
* Provides suggestions for fixing configuration errors.
*/
public class ConfigurationBindingFailureAnalyzer
extends AbstractFailureAnalyzer<BindException>
implements EnvironmentAware {
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
protected FailureAnalysis analyze(Throwable rootFailure, BindException cause) {
ConfigurationPropertyName propertyName = extractPropertyName(cause);
if (propertyName == null) {
return analyzeGenericBindError(rootFailure, cause);
}
String description = buildDescription(cause, propertyName);
String action = buildAction(cause, propertyName);
return new FailureAnalysis(description, action, cause);
}
private ConfigurationPropertyName extractPropertyName(BindException ex) {
// Extract property name from bind exception
try {
return ex.getProperty();
} catch (Exception e) {
return null;
}
}
private String buildDescription(BindException ex, ConfigurationPropertyName propertyName) {
StringBuilder desc = new StringBuilder();
desc.append("Configuration property binding failed.\n\n");
desc.append("Property: ").append(propertyName).append("\n");
// Try to get the actual value that failed
String actualValue = environment.getProperty(propertyName.toString());
if (actualValue != null) {
desc.append("Value: \"").append(actualValue).append("\"\n");
} else {
desc.append("Value: <not set>\n");
}
desc.append("\nReason: ").append(ex.getMessage()).append("\n");
// Add similar property names that might be typos
List<String> suggestions = findSimilarPropertyNames(propertyName.toString());
if (!suggestions.isEmpty()) {
desc.append("\nDid you mean one of these properties?\n");
suggestions.forEach(s -> desc.append(" - ").append(s).append("\n"));
}
return desc.toString();
}
private String buildAction(BindException ex, ConfigurationPropertyName propertyName) {
StringBuilder action = new StringBuilder();
action.append("Fix the configuration property:\n\n");
// Determine expected type from exception
String expectedType = extractExpectedType(ex);
if (expectedType != null) {
action.append("Expected type: ").append(expectedType).append("\n\n");
}
action.append("Update your configuration file (application.properties or application.yml):\n\n");
// Provide examples based on common types
if (expectedType != null) {
action.append(getExampleForType(propertyName.toString(), expectedType));
} else {
action.append(propertyName).append("=<correct-value>\n");
}
action.append("\nConfiguration file locations checked:\n");
action.append(" - classpath:application.properties\n");
action.append(" - classpath:application.yml\n");
action.append(" - Environment variables\n");
action.append(" - Command line arguments\n");
return action.toString();
}
private FailureAnalysis analyzeGenericBindError(Throwable rootFailure, BindException cause) {
String description = String.format(
"Failed to bind configuration properties.%n%n" +
"Error: %s",
cause.getMessage()
);
String action =
"Review your configuration files for errors:\n\n" +
"1. Check for typos in property names\n" +
"2. Verify property values match expected types\n" +
"3. Ensure nested properties are correctly formatted\n\n" +
"Common issues:\n" +
" - Missing quotes around string values in YAML\n" +
" - Incorrect indentation in YAML files\n" +
" - Type mismatches (e.g., string instead of number)\n" +
" - Missing required properties\n\n" +
"Enable debug logging to see binding details:\n" +
" logging.level.org.springframework.boot.context.properties=DEBUG";
return new FailureAnalysis(description, action, rootFailure);
}
private String extractExpectedType(BindException ex) {
String message = ex.getMessage();
if (message != null) {
if (message.contains("java.lang.Integer") || message.contains("int")) {
return "integer";
} else if (message.contains("java.lang.Long") || message.contains("long")) {
return "long";
} else if (message.contains("java.lang.Boolean") || message.contains("boolean")) {
return "boolean";
} else if (message.contains("java.lang.Double") || message.contains("double")) {
return "double";
} else if (message.contains("java.time.Duration")) {
return "duration";
}
}
return null;
}
private String getExampleForType(String propertyName, String type) {
return switch (type) {
case "integer", "long" -> String.format("%s=8080%n", propertyName);
case "boolean" -> String.format("%s=true%n", propertyName);
case "double" -> String.format("%s=1.5%n", propertyName);
case "duration" -> String.format(
"%s=30s # or 5m, 1h, 2d, etc.%n", propertyName);
default -> String.format("%s=<value>%n", propertyName);
};
}
private List<String> findSimilarPropertyNames(String target) {
List<String> similar = new ArrayList<>();
// Get all property names from environment
environment.getPropertySources().forEach(source -> {
if (source.getSource() instanceof java.util.Map) {
@SuppressWarnings("unchecked")
java.util.Map<String, Object> map = (java.util.Map<String, Object>) source.getSource();
map.keySet().stream()
.filter(key -> isSimilar(key, target))
.forEach(similar::add);
}
});
return similar.stream().distinct().limit(5).collect(Collectors.toList());
}
private boolean isSimilar(String actual, String target) {
// Simple similarity check - could use Levenshtein distance
int distance = levenshteinDistance(actual.toLowerCase(), target.toLowerCase());
return distance <= 3 && distance > 0;
}
private int levenshteinDistance(String s1, String s2) {
int[][] dp = new int[s1.length() + 1][s2.length() + 1];
for (int i = 0; i <= s1.length(); i++) {
dp[i][0] = i;
}
for (int j = 0; j <= s2.length(); j++) {
dp[0][j] = j;
}
for (int i = 1; i <= s1.length(); i++) {
for (int j = 1; j <= s2.length(); j++) {
int cost = (s1.charAt(i - 1) == s2.charAt(j - 1)) ? 0 : 1;
dp[i][j] = Math.min(
Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1),
dp[i - 1][j - 1] + cost
);
}
}
return dp[s1.length()][s2.length()];
}
}Analyzer that provides different guidance based on active profiles:
package com.example.diagnostics;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.core.env.Environment;
import org.springframework.context.EnvironmentAware;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Provides profile-specific failure analysis.
* Adjusts recommendations based on development, testing, or production environments.
*/
public class ProfileAwareFailureAnalyzer<T extends Exception>
extends AbstractFailureAnalyzer<T>
implements EnvironmentAware {
private Environment environment;
private final Class<T> exceptionType;
public ProfileAwareFailureAnalyzer(Class<T> exceptionType) {
this.exceptionType = exceptionType;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
protected Class<? extends T> getCauseType() {
return exceptionType;
}
@Override
protected FailureAnalysis analyze(Throwable rootFailure, T cause) {
Set<String> activeProfiles = getActiveProfiles();
boolean isProduction = activeProfiles.contains("prod") || activeProfiles.contains("production");
boolean isDevelopment = activeProfiles.contains("dev") || activeProfiles.contains("development");
boolean isTest = activeProfiles.contains("test");
String description = buildDescription(cause, activeProfiles);
String action = buildProfileSpecificAction(cause, isProduction, isDevelopment, isTest);
return new FailureAnalysis(description, action, cause);
}
private Set<String> getActiveProfiles() {
return Arrays.stream(environment.getActiveProfiles())
.collect(Collectors.toSet());
}
private String buildDescription(T cause, Set<String> activeProfiles) {
StringBuilder desc = new StringBuilder();
desc.append("Application startup failed.\n\n");
desc.append("Active profiles: ");
if (activeProfiles.isEmpty()) {
desc.append("none (using default profile)\n");
} else {
desc.append(String.join(", ", activeProfiles)).append("\n");
}
desc.append("\nError details:\n");
desc.append(cause.getMessage()).append("\n");
return desc.toString();
}
private String buildProfileSpecificAction(
T cause,
boolean isProduction,
boolean isDevelopment,
boolean isTest) {
StringBuilder action = new StringBuilder();
if (isProduction) {
action.append("PRODUCTION ENVIRONMENT DETECTED\n\n");
action.append("Critical actions required:\n\n");
action.append("1. Check application logs for detailed error information\n");
action.append("2. Verify all required services are running\n");
action.append("3. Check configuration in production config files\n");
action.append("4. Review recent deployment changes\n");
action.append("5. Consider rolling back to previous version if needed\n\n");
action.append("DO NOT make changes directly to production.\n");
action.append("Test fixes in staging environment first.\n");
} else if (isDevelopment) {
action.append("DEVELOPMENT ENVIRONMENT\n\n");
action.append("Troubleshooting steps:\n\n");
action.append("1. Enable debug logging:\n\n");
action.append(" logging.level.root=DEBUG\n");
action.append(" logging.level.org.springframework=DEBUG\n\n");
action.append("2. Check your local configuration:\n");
action.append(" - application-dev.properties\n");
action.append(" - Environment variables\n\n");
action.append("3. Verify database/services are running locally\n\n");
action.append("4. Try cleaning and rebuilding:\n");
action.append(" mvn clean install\n");
} else if (isTest) {
action.append("TEST ENVIRONMENT\n\n");
action.append("Test configuration issues:\n\n");
action.append("1. Check test-specific configuration\n");
action.append("2. Verify test database setup\n");
action.append("3. Review @TestConfiguration beans\n");
action.append("4. Check for test property overrides\n");
} else {
action.append("No specific profile detected (using default).\n\n");
action.append("Consider activating a profile:\n\n");
action.append(" spring.profiles.active=dev # for development\n");
action.append(" spring.profiles.active=test # for testing\n");
action.append(" spring.profiles.active=prod # for production\n\n");
action.append("Or set via environment variable:\n");
action.append(" SPRING_PROFILES_ACTIVE=dev\n");
}
return action.toString();
}
}Collects multiple failure analyses and presents them in a structured format:
package com.example.diagnostics;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.diagnostics.FailureAnalysisReporter;
import java.io.PrintStream;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
/**
* Custom failure reporter that aggregates and formats multiple failure analyses.
* Provides structured output with timestamps and categorization.
*/
public class StructuredFailureReporter implements FailureAnalysisReporter {
private static final DateTimeFormatter TIMESTAMP_FORMAT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
.withZone(ZoneId.systemDefault());
private final PrintStream output;
private final List<FailureAnalysis> reportedFailures;
public StructuredFailureReporter() {
this(System.err);
}
public StructuredFailureReporter(PrintStream output) {
this.output = output;
this.reportedFailures = new ArrayList<>();
}
@Override
public void report(FailureAnalysis analysis) {
if (analysis == null) {
return;
}
reportedFailures.add(analysis);
output.println();
output.println("═".repeat(80));
output.println("APPLICATION STARTUP FAILURE");
output.println("═".repeat(80));
output.println();
printTimestamp();
printSeparator();
printDescription(analysis);
printSeparator();
printAction(analysis);
printSeparator();
printCauseDetails(analysis);
output.println();
output.println("═".repeat(80));
output.println();
}
private void printTimestamp() {
String timestamp = TIMESTAMP_FORMAT.format(Instant.now());
output.println("Timestamp: " + timestamp);
output.println();
}
private void printSeparator() {
output.println("─".repeat(80));
}
private void printDescription(FailureAnalysis analysis) {
output.println("PROBLEM DESCRIPTION");
output.println();
String description = analysis.getDescription();
if (description != null && !description.isEmpty()) {
wrapAndPrint(description, 78);
} else {
output.println("No description available.");
}
output.println();
}
private void printAction(FailureAnalysis analysis) {
output.println("RECOMMENDED ACTION");
output.println();
String action = analysis.getAction();
if (action != null && !action.isEmpty()) {
wrapAndPrint(action, 78);
} else {
output.println("No specific action recommended.");
output.println("Review the error details below for more information.");
}
output.println();
}
private void printCauseDetails(FailureAnalysis analysis) {
output.println("ERROR DETAILS");
output.println();
Throwable cause = analysis.getCause();
if (cause != null) {
output.println("Exception: " + cause.getClass().getName());
String message = cause.getMessage();
if (message != null && !message.isEmpty()) {
output.println("Message: " + message);
}
output.println();
output.println("Stack trace:");
cause.printStackTrace(output);
} else {
output.println("No exception details available.");
}
output.println();
}
private void wrapAndPrint(String text, int maxWidth) {
String[] lines = text.split("\n");
for (String line : lines) {
if (line.length() <= maxWidth) {
output.println(line);
} else {
// Simple word wrapping
String[] words = line.split(" ");
StringBuilder currentLine = new StringBuilder();
for (String word : words) {
if (currentLine.length() + word.length() + 1 <= maxWidth) {
if (currentLine.length() > 0) {
currentLine.append(" ");
}
currentLine.append(word);
} else {
output.println(currentLine.toString());
currentLine = new StringBuilder(word);
}
}
if (currentLine.length() > 0) {
output.println(currentLine.toString());
}
}
}
}
/**
* Get all failures reported during this session.
*
* @return list of reported failures
*/
public List<FailureAnalysis> getReportedFailures() {
return new ArrayList<>(reportedFailures);
}
/**
* Write a summary report of all failures.
*/
public void writeSummary() {
if (reportedFailures.isEmpty()) {
output.println("No failures reported.");
return;
}
output.println();
output.println("═".repeat(80));
output.println("FAILURE SUMMARY");
output.println("═".repeat(80));
output.println();
output.println("Total failures: " + reportedFailures.size());
output.println();
for (int i = 0; i < reportedFailures.size(); i++) {
FailureAnalysis analysis = reportedFailures.get(i);
output.printf("%d. %s%n",
i + 1,
analysis.getCause().getClass().getSimpleName()
);
}
output.println();
output.println("═".repeat(80));
output.println();
}
}Problem: Analyzer returns FailureAnalysis for exceptions it cannot properly handle
Error: Generic or misleading error messages shown to users
Solution:
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
public class MyFailureAnalyzer extends AbstractFailureAnalyzer<MySpecificException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, MySpecificException cause) {
// Wrong - always returns analysis even if we can't help
// return new FailureAnalysis("Something went wrong", "Fix it", cause);
// Correct - check if we can actually provide useful guidance
if (!canAnalyze(cause)) {
return null; // Let other analyzers handle it
}
return new FailureAnalysis(
buildDescription(cause),
buildAction(cause),
cause
);
}
private boolean canAnalyze(MySpecificException ex) {
// Only analyze if we have specific guidance to provide
return ex.getErrorCode() != null && isKnownErrorCode(ex.getErrorCode());
}
private boolean isKnownErrorCode(String errorCode) {
// Check if this is an error code we can provide guidance for
return errorCode.startsWith("DB_") || errorCode.startsWith("AUTH_");
}
}Rationale: Returning null allows other analyzers to handle the exception. Only return FailureAnalysis when you can provide specific, actionable guidance. Multiple analyzers may be registered, and the first non-null result wins.
Problem: Analyzer class exists but is never invoked during failures
Error: Custom analyzer not being called, default error messages shown
Solution:
# Wrong - file missing or in wrong location
# Correct - create file at: src/main/resources/META-INF/spring.factories
org.springframework.boot.diagnostics.FailureAnalyzer=\
com.example.diagnostics.DatabaseFailureAnalyzer,\
com.example.diagnostics.NetworkFailureAnalyzer,\
com.example.diagnostics.ConfigurationFailureAnalyzerRationale: Spring Boot discovers FailureAnalyzers via spring.factories. Without proper registration, analyzers are never instantiated. The file must be in META-INF/spring.factories and use the fully qualified class names.
Problem: Analyzer uses instance variables that are mutated during analysis
Error:
Race conditions when multiple threads fail simultaneously
Incorrect analysis resultsSolution:
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadSafeAnalyzer extends AbstractFailureAnalyzer<Exception> {
// Wrong - mutable instance state
// private int analysisCount;
// private String lastError;
// Correct - use thread-safe or immutable state
private final AtomicInteger analysisCount = new AtomicInteger(0);
@Override
protected FailureAnalysis analyze(Throwable rootFailure, Exception cause) {
// All analysis should use local variables
String errorMessage = cause.getMessage();
String description = buildDescription(errorMessage);
// If you need to track state, use thread-safe structures
analysisCount.incrementAndGet();
return new FailureAnalysis(description, buildAction(), cause);
}
private String buildDescription(String errorMessage) {
// Use only parameters and local variables
return "Error occurred: " + errorMessage;
}
private String buildAction() {
// Pure function - no side effects
return "Check your configuration";
}
}Rationale: Multiple threads can fail simultaneously during application startup. Analyzers must be stateless or use thread-safe structures. Instance variables should be final and immutable. All analysis logic should use local variables and method parameters.
Problem: Including passwords or API keys in failure descriptions
Error:
Description:
Failed to connect to database at jdbc:postgresql://prod-db:5432/mydb?user=admin&password=secretpass123
Sensitive data logged and exposed in error messagesSolution:
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import java.util.regex.Pattern;
public class SecureFailureAnalyzer extends AbstractFailureAnalyzer<Exception> {
private static final Pattern PASSWORD_PATTERN =
Pattern.compile("password=([^&\\s]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern API_KEY_PATTERN =
Pattern.compile("(api[_-]?key|token)=([^&\\s]+)", Pattern.CASE_INSENSITIVE);
@Override
protected FailureAnalysis analyze(Throwable rootFailure, Exception cause) {
String message = cause.getMessage();
// Wrong - expose sensitive data
// String description = "Error: " + message;
// Correct - mask sensitive information
String sanitizedMessage = maskSensitiveInfo(message);
String description = "Error: " + sanitizedMessage;
return new FailureAnalysis(description, buildAction(), cause);
}
private String maskSensitiveInfo(String text) {
if (text == null) return null;
// Mask passwords
text = PASSWORD_PATTERN.matcher(text)
.replaceAll("password=****");
// Mask API keys and tokens
text = API_KEY_PATTERN.matcher(text)
.replaceAll("$1=****");
// Mask connection strings
text = maskJdbcPassword(text);
return text;
}
private String maskJdbcPassword(String text) {
// Pattern for JDBC URLs with embedded credentials
return text.replaceAll(
"://([^:]+):([^@]+)@",
"://$1:****@"
);
}
private String buildAction() {
return "Check configuration (sensitive values hidden for security)";
}
}Rationale: Failure messages are logged and may be visible to operators or in error reports. Always mask passwords, API keys, tokens, and other sensitive data. Use regex patterns to identify and redact sensitive information before including it in descriptions.
Problem: Analyzer targets Exception or Throwable instead of specific types
Error:
Analyzer catches all exceptions and provides generic unhelpful messages
Prevents more specific analyzers from runningSolution:
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
// Wrong - too generic
// public class GenericAnalyzer extends AbstractFailureAnalyzer<Exception>
// Correct - target specific exception types
public class SqlExceptionAnalyzer extends AbstractFailureAnalyzer<java.sql.SQLException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, java.sql.SQLException cause) {
// Provide specific SQL error guidance
return new FailureAnalysis(
"Database error: " + cause.getSQLState(),
"Check database configuration and connectivity",
cause
);
}
}
// For handling multiple related types, use common parent carefully
public class NetworkAnalyzer extends AbstractFailureAnalyzer<java.io.IOException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, java.io.IOException cause) {
// Check if this is a network issue we can help with
if (!(cause instanceof java.net.ConnectException ||
cause instanceof java.net.SocketTimeoutException)) {
return null; // Not a network issue we handle
}
return new FailureAnalysis(
"Network connectivity error",
"Check service availability and network configuration",
cause
);
}
}Rationale: Target the most specific exception type possible. Generic types like Exception or Throwable cause your analyzer to intercept all failures, preventing more specific analyzers from providing better guidance. When you must use a broader type, check subtypes and return null for ones you don't handle.
Problem: Providing generic advice without considering application configuration
Error:
Action suggests checking "spring.datasource.url" but application uses different property naming
Generic suggestions that don't match actual configurationSolution:
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.core.env.Environment;
import org.springframework.context.EnvironmentAware;
public class ContextAwareAnalyzer extends AbstractFailureAnalyzer<Exception>
implements EnvironmentAware {
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
protected FailureAnalysis analyze(Throwable rootFailure, Exception cause) {
// Wrong - generic advice
// String action = "Configure spring.datasource.url";
// Correct - check actual configuration
String datasourceUrl = environment.getProperty("spring.datasource.url");
String customUrl = environment.getProperty("app.database.url");
StringBuilder action = new StringBuilder();
if (datasourceUrl != null) {
action.append("Current datasource URL: ").append(datasourceUrl).append("\n");
action.append("Verify this URL is correct and database is accessible\n");
} else if (customUrl != null) {
action.append("Using custom database URL from: app.database.url\n");
action.append("Current value: ").append(customUrl).append("\n");
} else {
action.append("No database URL configured.\n");
action.append("Add one of:\n");
action.append(" spring.datasource.url=jdbc:... (standard)\n");
action.append(" app.database.url=... (if custom property)\n");
}
return new FailureAnalysis(
"Database configuration error",
action.toString(),
cause
);
}
}Rationale: Implement EnvironmentAware to access application configuration. Use the actual property values to provide specific, actionable guidance. Check for custom property names and active profiles to tailor recommendations to the actual application setup.
Problem: Only analyzing the immediate exception, missing root cause
Error:
Analyzing a wrapper exception instead of the underlying cause
Providing generic advice about wrappers instead of specific guidanceSolution:
import org.springframework.boot.diagnostics.FailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import java.sql.SQLException;
public class RootCauseAwareAnalyzer implements FailureAnalyzer {
@Override
public FailureAnalysis analyze(Throwable failure) {
// Wrong - only check top-level exception
// if (failure instanceof SQLException) { ... }
// Correct - walk the exception chain
SQLException sqlException = findCause(failure, SQLException.class);
if (sqlException == null) {
return null;
}
return analyzeException(sqlException, failure);
}
private <T extends Throwable> T findCause(Throwable failure, Class<T> type) {
Throwable current = failure;
while (current != null) {
if (type.isInstance(current)) {
return type.cast(current);
}
current = current.getCause();
}
return null;
}
private FailureAnalysis analyzeException(SQLException sqlEx, Throwable rootFailure) {
return new FailureAnalysis(
"SQL Error: " + sqlEx.getSQLState() + " - " + sqlEx.getMessage(),
"Check database configuration and connectivity",
rootFailure // Use root failure, not the specific cause
);
}
}Rationale: Exceptions are often wrapped in BeanCreationException, IllegalStateException, or other wrappers. Use AbstractFailureAnalyzer's findCause() or implement your own chain walking. Always return the root failure in FailureAnalysis, not the specific cause you found.
Problem: Vague descriptions and actions that don't help users
Error:
Description: An error occurred
Action: Fix the problemSolution:
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
public class DescriptiveAnalyzer extends AbstractFailureAnalyzer<Exception> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, Exception cause) {
// Wrong - vague messages
// String description = "Error occurred";
// String action = "Fix it";
// Correct - specific, detailed, actionable
String description = buildDetailedDescription(cause);
String action = buildStepByStepAction(cause);
return new FailureAnalysis(description, action, cause);
}
private String buildDetailedDescription(Exception cause) {
StringBuilder desc = new StringBuilder();
// What happened
desc.append("Application failed to start due to database connection failure.\n\n");
// Why it matters
desc.append("The application requires a working database connection ");
desc.append("to initialize the DataSource bean.\n\n");
// Specific details
desc.append("Error details:\n");
desc.append(" Exception: ").append(cause.getClass().getSimpleName()).append("\n");
desc.append(" Message: ").append(cause.getMessage()).append("\n");
return desc.toString();
}
private String buildStepByStepAction(Exception cause) {
return """
Follow these steps to resolve the issue:
1. Verify the database is running:
- Check database server status
- Try connecting with a database client
2. Verify connection configuration in application.properties:
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=myuser
spring.datasource.password=mypassword
3. Test connectivity:
- Ping the database host
- Check firewall rules
- Verify credentials are correct
4. Check logs for more details:
logging.level.org.springframework.jdbc=DEBUG
""";
}
}Rationale: Good error messages explain what happened, why it matters, and exactly how to fix it. Use numbered steps, concrete examples, and specific configuration snippets. Avoid vague terms like "check configuration" - instead specify which properties to check and what values to look for.
Problem: Exception has null message causing NullPointerException in analyzer
Error:
java.lang.NullPointerException in FailureAnalyzer.analyze()
Analyzer itself crashes, no failure analysis shownSolution:
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
public class NullSafeAnalyzer extends AbstractFailureAnalyzer<Exception> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, Exception cause) {
// Wrong - assumes message is non-null
// String description = "Error: " + cause.getMessage().toLowerCase();
// Correct - handle null safely
String message = cause.getMessage();
String safeMessage = (message != null && !message.isEmpty())
? message
: "No error message available";
String description = String.format(
"Application startup failed.%n%n" +
"Exception: %s%n" +
"Message: %s%n",
cause.getClass().getName(),
safeMessage
);
// Also check for null from other methods
String exceptionString = safeToString(cause);
String action = buildAction(cause, safeMessage);
return new FailureAnalysis(description, action, cause);
}
private String safeToString(Throwable throwable) {
try {
return throwable.toString();
} catch (Exception e) {
return throwable.getClass().getName() + " (toString() failed)";
}
}
private String buildAction(Exception cause, String message) {
// Safe string operations
String lowerMessage = message.toLowerCase();
if (lowerMessage.contains("connection")) {
return "Check connection configuration";
}
return "Review error details and check configuration";
}
}Rationale: Exception messages can be null. Always check for null before using message strings. Use safe defaults and defensive programming. If your analyzer crashes, users see no error analysis at all, making the problem worse.
Problem: Analyzer deployed without testing, fails in production
Error:
Analyzer throws exceptions
Provides incorrect or misleading guidance
Returns null unexpectedlySolution:
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class DatabaseFailureAnalyzerTest {
private final DatabaseFailureAnalyzer analyzer = new DatabaseFailureAnalyzer();
@Test
void analyzesConnectionRefusedError() {
SQLException ex = new SQLException(
"Connection refused",
"08001", // SQL State for connection error
1040 // Error code
);
FailureAnalysis analysis = analyzer.analyze(ex);
assertThat(analysis).isNotNull();
assertThat(analysis.getDescription())
.contains("Connection refused")
.contains("08001");
assertThat(analysis.getAction())
.contains("database")
.contains("spring.datasource");
assertThat(analysis.getCause()).isEqualTo(ex);
}
@Test
void returnsNullForUnrelatedExceptions() {
RuntimeException ex = new RuntimeException("Unrelated error");
FailureAnalysis analysis = analyzer.analyze(ex);
assertThat(analysis).isNull();
}
@Test
void findsExceptionInChain() {
SQLException sqlEx = new SQLException("DB Error");
RuntimeException wrapper = new RuntimeException("Wrapper", sqlEx);
IllegalStateException outer = new IllegalStateException("Outer", wrapper);
FailureAnalysis analysis = analyzer.analyze(outer);
assertThat(analysis).isNotNull();
assertThat(analysis.getDescription()).contains("DB Error");
}
@Test
void handlesNullMessages() {
// Some exceptions have null messages
SQLException ex = new SQLException(null, "08001", 1040);
FailureAnalysis analysis = analyzer.analyze(ex);
assertThat(analysis).isNotNull();
assertThat(analysis.getDescription()).isNotNull();
assertThat(analysis.getAction()).isNotNull();
}
@Test
void masksPasswordsInDescription() {
SQLException ex = new SQLException(
"Connection failed to jdbc:postgresql://host/db?user=admin&password=secret123"
);
FailureAnalysis analysis = analyzer.analyze(ex);
assertThat(analysis.getDescription())
.doesNotContain("secret123")
.contains("password=****");
}
@Test
void providesUsefulAction() {
SQLException ex = new SQLException("Test error");
FailureAnalysis analysis = analyzer.analyze(ex);
assertThat(analysis.getAction())
.isNotEmpty()
.containsAnyOf(
"spring.datasource",
"check",
"verify",
"configuration"
);
}
}Rationale: Test your analyzers thoroughly before deployment. Verify they handle expected exceptions, return null for unhandled cases, walk exception chains correctly, handle null values safely, and mask sensitive data. Untested analyzers can make failures worse by crashing or providing wrong guidance.
The default failure analysis reporter that logs formatted failure analysis to the application log.
package org.springframework.boot.diagnostics;
/**
* FailureAnalysisReporter that logs the failure analysis.
* This is the default reporter that formats and outputs failure analysis
* to the application logger in a user-friendly format.
*
* Output format:
* ***************************
* APPLICATION FAILED TO START
* ***************************
*
* Description:
* [failure description]
*
* Action:
* [suggested action]
*
* Thread Safety: Thread-safe. Stateless implementation.
*
* @since 1.4.0
*/
public final class LoggingFailureAnalysisReporter implements FailureAnalysisReporter {
/**
* Report the given failure analysis by logging it.
* Logs the failure cause at DEBUG level and the formatted analysis at ERROR level.
*
* @param failureAnalysis the analysis to report
*/
@Override
public void report(FailureAnalysis failureAnalysis);
}Example Output:
***************************
APPLICATION FAILED TO START
***************************
Description:
The Tomcat connector configured to listen on port 8080 failed to start. The port may already be in use or you may not have permission to listen on that port.
Action:
Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.How It Works:
FailureAnalysis from analyzers via Spring Boot's failure analysis mechanismCustom Reporter Example:
package com.example.diagnostics;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.diagnostics.FailureAnalysisReporter;
import org.springframework.stereotype.Component;
/**
* Custom reporter that sends failure analysis to external monitoring system.
*/
@Component
public class MonitoringFailureAnalysisReporter implements FailureAnalysisReporter {
private final MonitoringService monitoringService;
public MonitoringFailureAnalysisReporter(MonitoringService monitoringService) {
this.monitoringService = monitoringService;
}
@Override
public void report(FailureAnalysis analysis) {
// Send to monitoring system
monitoringService.reportFailure(
analysis.getDescription(),
analysis.getAction(),
analysis.getCause()
);
// Also delegate to default logging reporter
new LoggingFailureAnalysisReporter().report(analysis);
}
}Registering Custom Reporters:
# META-INF/spring.factories
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
com.example.diagnostics.MonitoringFailureAnalysisReporterFailureAnalyzer implementations: Should be stateless and thread-safe. Multiple analyzers may execute concurrently.
LoggingFailureAnalysisReporter: Thread-safe. Stateless implementation using static logger.
// Core diagnostics
import org.springframework.boot.diagnostics.FailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysisReporter;
import org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter;
// Spring context
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
// Exceptions
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;