SpringBoot核心机制四、FailureAnalyzer

2022-12-30 11:08:53

一、自定义错误分析器

首先在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);
	}
}

相信这个端口占用的错误提示大家都应该看到过的把。

  • 作者:roykingw
  • 原文链接:https://blog.csdn.net/roykingw/article/details/116266836
    更新时间:2022-12-30 11:08:53