文章目录
一、自定义错误分析器
首先在main方法中抛出一个异常:
@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 {
//这里会抛出一个ArithmeticException异常
int i = 1/0;
}
}
如果直接运行, 那这个报错信息就只是java的堆栈信息。
这些java标准的堆栈信息,对于排查错误是可以了。但是SpringBoot更希望能够报错之后给出一些修改建议,教你做"正确的事"。这时,可以定制一个错误分析器。错误分析器中包含了description, action, cause三个部分,其实一般要补充的也就是其中的action,这就是建议操作。
public class ArithmeticFailureAnalyzer extends AbstractFailureAnalyzer<ArithmeticException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, ArithmeticException cause) {
return new FailureAnalysis(cause.getMessage(), "Calculate Error.", cause);
}
}
然后将它配置到spring.factories中:
org.springframework.boot.diagnostics.FailureAnalyzer=\
com.roy.failureAnalyzer.ArithmeticFailureAnalyzer
错误提示就变成了这样更友好的方式:
这种错误提示增加了详细的描述,并能够针对错误给出相应的处理建议。
其实如何优雅的处理程序运行时的各种异常,是提高应用健壮性的一个非常重要的环节。而这个环节,在日常开发中,是很容易被绝大多数的程序员忽略的。而对于SpringBoot,很多人在学习过程中只是急于上手去构建应用。在如今互联网环境,Spring或许还在很多大公司面试的狂轰滥炸下,越来越多的人回去深入研究,体会一下其中的优雅。但是对于SpringBoot,却罕有对应的热情。其实对于应用开发而言,SpringBoot体系中能够提供的帮助,比Spring只有多,没有少,因为SpringBoot天生就更贴近应用。而这种忽视,不能不说是一种遗憾。
二、核心机制解读
对于SpringBoot异常处理机制的解读,还是从SpringApplication的run方法开始。源码的处理链路其实并不是很长,就直接上关键代码了。
#SpringApplication
public ConfigurableApplicationContext run(String... args) {
...
try {
...
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners); //<===异常机制入口
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null); //<===异常机制入口
throw new IllegalStateException(ex);
}
return context;
}
从handleRunFailure方法往下跟踪
private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception,
SpringApplicationRunListeners listeners) {
try {
try {
//处理异常信息
handleExitCode(context, exception);
//广播异常事件
if (listeners != null) {
listeners.failed(context, exception);
}
}
finally {
//<==我们的重点,异常报告
reportFailure(getExceptionReporters(context), exception);
if (context != null) {
context.close();
}
}
}
catch (Exception ex) {
logger.warn("Unable to close ApplicationContext", ex);
}
ReflectionUtils.rethrowRuntimeException(exception);
}
首先看这个getExceptionReporters方法
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
//熟悉的代码
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
在这里就看到了我们这一篇中最为熟悉的代码SpringFactoriesLoader。这里就是加载spring.factories中对于异常处理器的配置:
# spring-boot-2.4.5.jar
# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers
所以整个getExceptionReporters方法返回的就是一个FailureAnalyzers实例。了解这一点之后,再往下跟踪代码就非常明了了。
加载出这个异常处理器后,就会通过他的reportException方法打印出异常报告。
#SpringApplication.java
private void reportFailure(Collection<SpringBootExceptionReporter> exceptionReporters, Throwable failure) {
try {
//<===打印异常报告。在默认配置中只有FailureAnalyzers一个实现,所以这里其实是可以扩展的。
for (SpringBootExceptionReporter reporter : exceptionReporters) {
if (reporter.reportException(failure)) {//<===打印错误日志
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);
}
}
于是,接下来的逻辑就需要到org.springframework.boot.diagnostics.FailureAnalyzers中去查看他的reportException方法。
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;
}
}
核心的report方法只有两行代码,就非常好理解了。analyze方法就会分析Spring.factories文件中配置的org.springframework.boot.diagnostics.FailureAnalyzer实现类,然后调用对应组件的analyze方法,返回一个FailureAnalysis对象。
然后下面的report方法打印日志的过程中时,也会通过解析spring.factories中org.springframework.boot.diagnostics.FailureAnalysisReporter组件,获得具体的实现类,然后将日志信息打印到Logger中。
# spring-boot-2.4.5.jar / spring.factories
# Failure Analysis Reporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter
最后打印的方法,也是相当的简单粗暴:
#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();
}
**重点:**了解了这个机制,也意味着对于SpringBoot应用的运行情况监控,可以有一个非常清晰的扩展机制。对于运行过程中的异常情况分析,在很多业务场景下,是提高应用健壮性的重要参考。而spring.facotries中的FailureAnalysisReporter机制就是一个非常好也非常方便的扩展点。例如利用这个机制,就可以很方便的将应用中的异常信息记录到redis或者hadoop大数据组件,进行后续批量统计计算。
实际上,由此扩展出来,当你想要去理解spring-boot-starter-actuator组件的源码时,就会在spring.factories中看到一个熟悉的配置信息。是不是能对这个组件有不一样的理解?
org.springframework.boot.diagnostics.FailureAnalyzer=\org.springframework.boot.actuate.autoconfigure.metrics.ValidationFailureAnalyzer
三、SpringBoot当中的核心实现
接下来还是总结下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
这就是针对各种异常情况定制的错误提示,这对于构建一个健壮的应用是非常重要的。例如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);
}
}
相信这个端口占用的错误提示大家都应该看到过的把。