写这篇文章的前景是之前项目有天晚上在遇到需要紧急上线时,却始终发布失败,而从发布日志上无法定位到具体发布失败的原因。对服务造成较大影响。之后痛定思痛,反思如何能从根本上解决这个问题。当然其中涉及到导致代码部署失败的code review 不再本文的分享范畴,这里就不再叙述了。
SpringBoot在项目启动时如果遇到异常并不能友好的打印出具体的堆栈错误信息,我们只能查看到简单的错误消息,以致于并不能及时解决发生的问题,针对这个问题SpringBoot提供了故障分析仪的概念(failure-analyzer),内部根据不同类型的异常提供了一些实现,我们如果想自定义该怎么去做?
FailureAnalyzer
SpringBoot提供了启动异常分析接口FailureAnalyzer
,该接口位于org.springframework.boot.diagnostics
package内。 内部仅提供一个分析的方法,源码如下所示:
@FunctionalInterface
public interface FailureAnalyzer {
/**
* Returns an analysis of the given {@code failure}, or {@code null} if no analysis
* was possible.
* @param failure the failure
* @return the analysis or {@code null}
*/
FailureAnalysis analyze(Throwable failure);
}
该接口会把遇到的异常对象实例Throwable failure
交付给实现类,实现类进行自定义处理。
AbstractFailureAnalyzer
AbstractFailureAnalyzer
是FailureAnalyzer
的基础实现抽象类,实现了FailureAnalyzer
定义的analyze(Throwable failure)
方法,并提供了一个指定异常类型的抽象方法analyze(Throwable rootFailure, T cause)
,源码如下所示:
public abstract class AbstractFailureAnalyzer implements FailureAnalyzer {
@Override
public FailureAnalysis analyze(Throwable failure) {
T cause = findCause(failure, getCauseType());
if (cause != null) {
return analyze(failure, cause);
}
return null;
}
/**
* Returns an analysis of the given {@code rootFailure}, or {@code null} if no
* analysis was possible.
* @param rootFailure the root failure passed to the analyzer
* @param cause the actual found cause
* @return the analysis or {@code null}
*/
protected abstract FailureAnalysis analyze(Throwable rootFailure, T cause);
/**
* Return the cause type being handled by the analyzer. By default the class generic
* is used.
* @return the cause type
*/
@SuppressWarnings("unchecked")
protected Class getCauseType() {
return (Class) ResolvableType.forClass(AbstractFailureAnalyzer.class, getClass()).resolveGeneric();
}
@SuppressWarnings("unchecked")
protected final E findCause(Throwable failure, Class type) {
while (failure != null) {
if (type.isInstance(failure)) {
return (E) failure;
}
failure = failure.getCause();
}
return null;
}
}
通过AbstractFailureAnalyzer
源码我们可以看到,它在实现于FailureAnalyzer
的接口方法内进行了特殊处理,根据getCauseType()
方法获取当前类定义的第一个泛型类型,也就是我们需要分析的指定异常类型
。
获取泛型异常类型后根据方法findCause
判断Throwable
是否与泛型异常类型匹配,如果匹配直接返回给SpringBoot
进行注册处理。
SpringBoot提供的分析实现
SpringBoot
内部通过实现AbstractFailureAnalyzer
抽象类定义了一系列的针对性异常类型的启动分析,如下图所示:
指定异常分析
SpringBoot
内部提供的启动异常分析都是指定具体的异常类型实现的,最常见的一个错误就是端口号被占用(PortInUseException
),虽然SpringBoot
内部提供一个这个异常的启动分析,我们也是可以进行替换这一异常分析的,我们只需要创建PortInUseException
异常的AbstractFailureAnalyzer
,并且实现类注册给SpringBoot
即可,实现自定义如下所示:
/**
* 端口号被占用{@link PortInUseException}异常启动分析
*
*/
public class PortInUseFailureAnalyzer extends AbstractFailureAnalyzer {
/**
* logger instance
*/
static Logger logger = LoggerFactory.getLogger(PortInUseFailureAnalyzer.class);
@Override
protected FailureAnalysis analyze(Throwable rootFailure, PortInUseException cause) {
logger.error("端口被占用。", cause);
return new FailureAnalysis("端口号:" + cause.getPort() + "被占用", "PortInUseException", rootFailure);
}
}
注册启动异常分析
在上面我们只是编写了指定异常启动分析,我们接下来需要让它生效,这个生效方式比较特殊,类似于自定义SpringBoot Starter AutoConfiguration
的形式,我们需要在META-INF/spring.factories
文件内进行定义,如下所示:
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.minbox.chapter.springboot.failure.analyzer.PortInUseFailureAnalyzer
项目上的配置如下所示:
那我们为什么需要使用这种方式定义呢?
项目启动遇到的异常顺序不能确定,很可能在Spring IOC
并未执行初始化之前就出现了异常,我们不能通过@Component
注解的形式使其生效,所以SpringBoot
提供了通过spring.factories
配置文件的方式定义。
启动异常分析继承关系
自定义的运行异常一般都是继承自RuntimeException
,如果我们定义一个RuntimeException
的异常启动分析实例会是什么效果呢?
/**
* 项目启动运行时异常{@link RuntimeException}统一启动分析
*
*/
public class ProjectBootUnifiedFailureAnalyzer extends AbstractFailureAnalyzer {
/**
* logger instance
*/
static Logger logger = LoggerFactory.getLogger(ProjectBootUnifiedFailureAnalyzer.class);
@Override
protected FailureAnalysis analyze(Throwable rootFailure, RuntimeException cause) {
logger.error("遇到运行时异常", cause);
return new FailureAnalysis(cause.getMessage(), "error", rootFailure);
}
}
将该类也一并注册到spring.factories
文件内,如下所示:
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.minbox.chapter.springboot.failure.analyzer.PortInUseFailureAnalyzer,\
org.minbox.chapter.springboot.failure.analyzer.ProjectBootUnifiedFailureAnalyzer
运行项目并测试端口号被占用异常
我们会发现,并没有执行ProjectBootUnifiedFailureAnalyzer
内的analyze
方法,而是继续执行了PortInUseFailureAnalyzer
类内的方法。
那我们将PortInUseFailureAnalyzer
这个启动分析从spring.factories
文件内暂时删除掉
,再来运行项目我们会发现这时却是会执行ProjectBootUnifiedFailureAnalyzer
类内分析方法。
总结
根据本章我们了解了SpringBoot
提供的启动异常分析接口以及基本抽象实现类的运作原理,而且启动异常分析存在分析泛型异常类的上下级继承关系,异常子类的启动分析会覆盖掉异常父类的启动分析,如果你想包含全部异常的启动分析可以尝试使用Exception
作为AbstractFailureAnalyzer
的泛型参数。
spring.factories 不仅仅可以做为异常解析,还可以做服务启动时实例化配置等。通过springboot的源码可以看到,springboot在启动的时候,通过SpringFactoriesLoader的loadFactoryNames方法获取spring.factories的配置列表,该方法会遍历整个ClassLoader中所有jar包下的spring.factories,相互间不会影响也不会覆盖。下面是springboot的一个factories文件。
# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor
# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\
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
Spring Factories。这种扩展机制实际上是仿照Java中的SPI扩展机制来实现的。
什么是SPI机制
SPI的全名为Service Provider Interface.简单的总结下java SPI机制的思想就是,我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
Spring Boot中的SPI机制
在Spring中也有一种类似与Java SPI的加载机制。它在META-INF/spring.factories文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化。
这种自定义的SPI机制是Spring Boot Starter实现的基础。
Spring Factories实现原理
spring-core包里定义了SpringFactoriesLoader类,这个类实现了检索META-INF/spring.factories文件,并获取指定接口的配置的功能。在这个类中定义了两个对外的方法:
loadFactories 根据接口类获取其实现类的实例,这个方法返回的是对象列表。
loadFactoryNames 根据接口获取其接口类的名称,这个方法返回的是类名的列表。
上面的两个方法的关键都是从指定的ClassLoader中获取spring.factories文件,并解析得到类名列表,具体代码如下↓
public static List loadFactoryNames(Class factoryClass, ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); try { Enumeration urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); List result = new ArrayList(); while (urls.hasMoreElements()) { URL url = urls.nextElement(); Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); String factoryClassNames = properties.getProperty(factoryClassName); result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames))); } return result; } catch (IOException ex) { throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() + "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); }}
从代码中我们可以知道,在这个方法中会遍历整个ClassLoader中所有jar包下的spring.factories文件。也就是说我们可以在自己的jar中配置spring.factories文件,不会影响到其它地方的配置,也不会被别人的配置覆盖。
spring.factories的是通过Properties解析得到的,所以我们在写文件中的内容都是安装下面这种方式配置的:
com.xxx.interface=com.xxx.classname
如果一个接口希望配置多个实现类,可以使用’,’进行分割。
Spring Factories在Spring Boot中的应用
关于spring boot如何加载spring.factories 在后续的文章在详细介绍。