SpringBoot core mechanism IV. FailureAnalyzer

Posted by filn on Fri, 18 Feb 2022 13:41:37 +0100

1, Custom error analyzer

First throw an exception in the main method:

@SpringBootApplication
public class P1Application implements CommandLineRunner {

    public static void main(String[] args) {
        final SpringApplication application = new SpringApplication(P1Application.class);
        application.run(args);
    }

    @Autowired
    private ApplicationContext applicationContext;

    @Override
    public void run(String... args) throws Exception {
        //An ArithmeticException exception will be thrown here
        int i = 1/0;
    }
}

If you run it directly, the error message is just java stack information.

These java standard stack information can be used for troubleshooting errors. However, SpringBoot hopes to give some modification suggestions after reporting an error to teach you to do the "right thing". At this point, you can customize an error analyzer. The error analyzer contains three parts: description, action and cause. In fact, the action to be added is the recommended action.

public class ArithmeticFailureAnalyzer extends AbstractFailureAnalyzer<ArithmeticException> {
    @Override
    protected FailureAnalysis analyze(Throwable rootFailure, ArithmeticException cause) {
        return new FailureAnalysis(cause.getMessage(), "Calculate Error.", cause);
    }
}

Then configure it to spring In factories:

org.springframework.boot.diagnostics.FailureAnalyzer=\
com.roy.failureAnalyzer.ArithmeticFailureAnalyzer

The error prompt becomes a more friendly way:

This error prompt adds a detailed description and can give corresponding handling suggestions for the error.

In fact, how to handle all kinds of exceptions gracefully is a very important link to improve the robustness of applications. This link, in daily development, is easy to be ignored by most programmers. For SpringBoot, many people are just eager to build applications in the learning process. In today's Internet environment, Spring may still be bombarded by interviews with many large companies. More and more people go back to in-depth research and experience the elegance. But for SpringBoot, there is little corresponding enthusiasm. In fact, for application development, SpringBoot system can provide only more help than Spring, no less, because SpringBoot is naturally closer to the application. This neglect is a pity.

2, Interpretation of core mechanism

The interpretation of SpringBoot exception handling mechanism starts with the run method of SpringApplication. In fact, the processing link of the source code is not very long, so it is directly on the key code.

#SpringApplication
public ConfigurableApplicationContext run(String... args) {
		...
		try {
			...
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, listeners); //< = = = exception mechanism entry
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, null); //< = = = exception mechanism entry
			throw new IllegalStateException(ex);
		}
		return context;
	}

Trace down from the handleRunFailure method

private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception,
			SpringApplicationRunListeners listeners) {
		try {
			try {
                //Handling exception information
				handleExitCode(context, exception);
                //Broadcast abnormal events
				if (listeners != null) {
					listeners.failed(context, exception);
				}
			}
			finally {
                //< = = our focus, exception report
				reportFailure(getExceptionReporters(context), exception);
				if (context != null) {
					context.close();
				}
			}
		}
		catch (Exception ex) {
			logger.warn("Unable to close ApplicationContext", ex);
		}
		ReflectionUtils.rethrowRuntimeException(exception);
	}

First look at the getExceptionReporters method

private Collection<SpringBootExceptionReporter> getExceptionReporters(ConfigurableApplicationContext context) {
		try {
			return getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class<?>[] { ConfigurableApplicationContext.class }, context);
		}
		catch (Throwable ex) {
			return Collections.emptyList();
		}
	}

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
		ClassLoader classLoader = getClassLoader();
		// Use names and ensure unique to protect against duplicates
    	//Familiar code
		Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
		List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
		AnnotationAwareOrderComparator.sort(instances);
		return instances;
	}

Here we see the most familiar code in this article, spring factoriesloader. Here is to load spring Configuration of exception handlers in factories:

# spring-boot-2.4.5.jar
# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers

So the whole getExceptionReporters method returns an instance of FailureAnalyzers. Once you know this, it's very clear to trace down the code.

After the exception handler is loaded, the exception report will be printed through its reportException method.

#SpringApplication.java
private void reportFailure(Collection<SpringBootExceptionReporter> exceptionReporters, Throwable failure) {
		try {
            //< = = = print exception report. In the defau lt configuration, there is only one implementation of FailureAnalyzers, so it can be extended here.
			for (SpringBootExceptionReporter reporter : exceptionReporters) {
				if (reporter.reportException(failure)) {//< = = = print error log
					registerLoggedException(failure);
					return;
				}
			}
		}
		catch (Throwable ex) {
			// Continue with normal handling of the original failure
		}
		if (logger.isErrorEnabled()) {
			logger.error("Application run failed", failure);
			registerLoggedException(failure);
		}
	}

Therefore, the next logic needs to go to org springframework. boot. diagnostics. See his reportException method in failureanalyzers.

final class FailureAnalyzers implements SpringBootExceptionReporter {
	...
    @Override
	public boolean reportException(Throwable failure) {
		FailureAnalysis analysis = analyze(failure, this.analyzers);
		return report(analysis, this.classLoader);
	}
    
    private FailureAnalysis analyze(Throwable failure, List<FailureAnalyzer> analyzers) {
		for (FailureAnalyzer analyzer : analyzers) {
			try {
				FailureAnalysis analysis = analyzer.analyze(failure);
				if (analysis != null) {
					return analysis;
				}
			}
			catch (Throwable ex) {
				logger.trace(LogMessage.format("FailureAnalyzer %s failed", analyzer), ex);
			}
		}
		return null;
	}

	private boolean report(FailureAnalysis analysis, ClassLoader classLoader) {
		List<FailureAnalysisReporter> reporters = SpringFactoriesLoader.loadFactories(FailureAnalysisReporter.class,
				classLoader);
		if (analysis == null || reporters.isEmpty()) {
			return false;
		}
		for (FailureAnalysisReporter reporter : reporters) {
			reporter.report(analysis);
		}
		return true;
	}
}

The core report method has only two lines of code, which is very easy to understand. The analyze method will analyze spring Org.org configured in the factories file springframework. boot. diagnostics. FailureAnalyzer implements the class, then calls the corresponding component's analyze method to return a FailureAnalysis object.

Then, when the following report method prints the log, it will also parse spring Org. In factories springframework. boot. diagnostics. Failureanalysisreporter component, obtain the specific implementation class, and then print the log information to the Logger.

# spring-boot-2.4.5.jar / spring.factories
# Failure Analysis Reporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter

The final printing method is also quite simple and rough:

#LoggingFailureAnalysisReporter
	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();
	}

**Key points: * * understanding this mechanism also means that there can be a very clear extension mechanism for the operation monitoring of SpringBoot applications. The analysis of abnormal conditions during operation is an important reference to improve the robustness of applications in many business scenarios. And spring The failure analysis reporter mechanism in factories is a very good and convenient extension point. For example, using this mechanism, you can easily record the exception information in the application to redis or hadoop big data components for subsequent batch statistical calculation.

In fact, when you want to understand the source code of the spring boot starter actuator component, it will be expanded in spring See a familiar configuration information in factories. Can you have a different understanding of this component?

org.springframework.boot.diagnostics.FailureAnalyzer=\org.springframework.boot.actuate.autoconfigure.metrics.ValidationFailureAnalyzer

3, The core implementation of SpringBoot

Next, let's summarize the implementation provided by default in SpringBoot:

#spirng-boot.jar
# Failure analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.context.config.ConfigDataNotFoundFailureAnalyzer,\
org.springframework.boot.context.properties.IncompatibleConfigurationFailureAnalyzer,\
org.springframework.boot.context.properties.NotConstructorBoundInjectionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanDefinitionOverrideFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BindValidationFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.UnboundConfigurationPropertyFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ConnectorStartFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoSuchMethodFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyNameFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyValueFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.PatternParseFailureAnalyzer,\
org.springframework.boot.liquibase.LiquibaseChangelogMissingFailureAnalyzer
# Failure Analysis Reporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter


# spring-boot-autoconfigure
# Failure analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.autoconfigure.data.redis.RedisUrlSyntaxFailureAnalyzer,\
org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\
org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\
org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\
org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\
org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\
org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer

This is the error prompt customized for various abnormal situations, which is very important for building a robust application. For example, PortInUseFailureAnalyzer

class PortInUseFailureAnalyzer extends AbstractFailureAnalyzer<PortInUseException> {
	@Override
	protected FailureAnalysis analyze(Throwable rootFailure, PortInUseException cause) {
		return new FailureAnalysis("Web server failed to start. Port " + cause.getPort() + " was already in use.",
				"Identify and stop the process that's listening on port " + cause.getPort() + " or configure this "
						+ "application to listen on another port.",
				cause);
	}
}

I believe that the error prompt of this port occupation should be seen by everyone.

Topics: Java Spring Spring Boot