Spring Boot 特性之 Failure Analysis
Failure Analysis 就是:SpringBoot 启动错误时,日志会打印专业的错误信息和解决该问题的具体措施。
举个例子
当重复启动 SpringBoot 会输出以下信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2020-04-11 14:20:06.773 ERROR 1899 --- [ restartedMain] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
Web server failed to start. Port 8080 was already in use.
Action:
Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.
一、从日志类剖析
日志输出类是o.s.b.d.LoggingFailureAnalysisReporter,内容有错误描述Description和解决措施Action。
LoggingFailureAnalysisReporter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public final class LoggingFailureAnalysisReporter implements FailureAnalysisReporter {
private static final Log logger = LogFactory.getLog(LoggingFailureAnalysisReporter.class);
@Override
public void report(FailureAnalysis failureAnalysis) {
if (logger.isDebugEnabled()) {
logger.debug("Application failed to start due to an exception", failureAnalysis.getCause());
}
if (logger.isErrorEnabled()) {
logger.error(buildMessage(failureAnalysis));
}
}
private String buildMessage(FailureAnalysis failureAnalysis) {
StringBuilder builder = new StringBuilder();
builder.append(String.format("%n%n"));
builder.append(String.format("***************************%n"));
builder.append(String.format("APPLICATION FAILED TO START%n"));
builder.append(String.format("***************************%n%n"));
builder.append(String.format("Description:%n%n"));
builder.append(String.format("%s%n", failureAnalysis.getDescription()));
if (StringUtils.hasText(failureAnalysis.getAction())) {
builder.append(String.format("%nAction:%n%n"));
builder.append(String.format("%s%n", failureAnalysis.getAction()));
}
return builder.toString();
}
}
LoggingFailureAnalysisReporter 是一个 实现 FailureAnalysisReporter 接口的 final 类。
- 重写 report 方法,根据日志的允许输出的级别,打印传入参数 failureAnalysis 的信息。
- 私有 buildMessage 方法,将传入参数 failureAnalysis 按照指定样式拼接成字符串。
FailureAnalysisReporter
1
2
3
4
@FunctionalInterface
public interface FailureAnalysisReporter {
void report(FailureAnalysis analysis);
}
@FunctionalInterface 注解修饰的功能性接口只提供一个 void report(FailureAnalysis analysis) 方法,向开发人员输出被传入的 analysis 对象。
FailureAnalysis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class FailureAnalysis {
private final String description;
private final String action;
private final Throwable cause;
public FailureAnalysis(String description, String action, Throwable cause) {
this.description = description;
this.action = action;
this.cause = cause;
}
public String getDescription() {
return this.description;
}
public String getAction() {
return this.action;
}
public Throwable getCause() {
return this.cause;
}
}
一个简单的失败分析对象,包含三个属性:
- description,自定义的错误描述。
- action,自定义的解决措施。
- cause,完整的错误堆栈。
二、从 Spring 加载剖析
SpringFactoriesLoader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public final class SpringFactoriesLoader {
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();
private SpringFactoriesLoader() {
}
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
Assert.notNull(factoryType, "'factoryType' must not be null");
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
if (logger.isTraceEnabled()) {
logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames);
}
List<T> result = new ArrayList<>(factoryImplementationNames.size());
for (String factoryImplementationName : factoryImplementationNames) {
result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
}
AnnotationAwareOrderComparator.sort(result);
return result;
}
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
@SuppressWarnings("unchecked")
private static <T> T instantiateFactory(String factoryImplementationName, Class<T> factoryType, ClassLoader classLoader) {
try {
Class<?> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader);
if (!factoryType.isAssignableFrom(factoryImplementationClass)) {
throw new IllegalArgumentException(
"Class [" + factoryImplementationName + "] is not assignable to factory type [" + factoryType.getName() + "]");
}
return (T) ReflectionUtils.accessibleConstructor(factoryImplementationClass).newInstance();
}
catch (Throwable ex) {
throw new IllegalArgumentException(
"Unable to instantiate factory class [" + factoryImplementationName + "] for factory type [" + factoryType.getName() + "]",
ex);
}
}
}
SpringFactoriesLoader 是 Spring 框架内部使用的通用工厂加载机制。
- 静态公共参数,需要加载工厂类的位置,即
META-INF/spring.factories。 - 静态私有参数,使用 LogFactory 进行初始化的日志类 logger。
- 静态私有参数,使用 ConcurrentReferenceHashMap 初始化的集合 cache。
- 构造器私有化,不允许进行 new 实例化。
- 静态公共方法 loadFactories
- 静态公共方法 loadFactoryNames
- 静态私有方法 loadSpringFactories
- 静态私有方法 instantiateFactory
loadFactories 方法
- 传入参数 factoryType 代表工厂类的接口或抽象类。
- 传入参数 classLoader 代表使用的类加载器,可以为空。
- 检查 factoryType 不允许为空
- 判断 classLoader 是否为空
- 为空时,使用当前类 SpringFactoriesLoader 的类加载器。
- 调用
loadFactoryNames 方法 - 判断日志是否允许 trace 级别输出
- 允许时,将调用
loadFactoryNames 方法的结果输出到日志。
- 允许时,将调用
- 初始化一个空 ArrayList 集合
- 循环 factoryImplementationName
- 调用
instantiateFactory 方法 - 将调用结果放入 ArrayList 集合中
- 调用
- 使用 AnnotationAwareOrderComparator 进行排序
- 结束方法并返回 ArrayList 集合
loadFactoryNames 方法
- 传入参数 factoryType 代表工厂类的接口或抽象类。
- 传入参数 classLoader 代表使用的类加载器,可以为空。
- 获取 factoryType 的名称
- 调用
loadSpringFactories 方法 - 结束方法并返回调用结果中的 List 集合
- 当 List 集合为空时,返回一个空的 List 集合。
loadSpringFactories 方法
- 传入参数 classLoader 代表使用的类加载器,可以为空。
- 从 cache 缓存中获取 Map 集合
- 当结果不为空时,结束方法并返回该结果。
- 获取所有 “META-INF/spring.factories” 的绝对路径集合
- 初始化一个空 LinkedMultiValueMap 对象:result
- 循环绝对路径集合
- 获取下一个路径 url
- 加载路径到 UrlResource
- 将 UrlResource 解析成 Properties 对象
- 循环 Properties 对象节点
- 获取 key 的值:factoryTypeName
- 循环 value 的值,以
,为分隔符转换成 List 集合,并循环放入集合 result 中。
- 放入 cache 集合
- 结束方法并返回对象:result
** instantiateFactory 方法**
- 传入参数 factoryImplementationName 代表类的完整路径,从
META-INF/spring.factories中来。 - 传入参数 factoryType 代表工厂类的接口或抽象类。
- 传入参数 classLoader 代表使用的类加载器,可以为空。
- 使用类工具,将 factoryImplementationName 字符串转换成类引用 factoryImplementationClass。
- 判断 factoryImplementationClass 类是否为 factoryType 类的超类或者超接口类
- 不是,抛出异常
- 结束方法并返回通过反射初始化的 factoryImplementationClass 实例。
spring.factories
spring-boot-2.2.6.RELEASE.jar 源码包中,包含了一个 spring.factories 文件。
# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers
# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
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
# FailureAnalysisReporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter
Properties 格式的配置文件。等号前面是接口类全路径,等号后面是接口实现类的全路径。
SpringFactoriesLoader 类在加载工厂类时,会将 factories 文件中的实现类进行实例化。
总结
Failure Analysis,是 Spring 启动类预先准备好的错误说明、解决措施。Spring 启动时,会通过 SpringFactoriesLoader 加载 spring.factories 文件中配置的类路径。当 Spring 启动报错时,会调用日志输出接口。该接口会返回具体类的重写接口的结果,并写入日志文件。
最后,下周二继续努力。