Spring Boot的FailureAnalyzer机制及如何解救应用启动危机
作者:张彦峰ZYF
在程序员的世界里,错误就像邻居家的狗,总是无时无刻不在你的代码中“汪汪”作响,仿佛在说:“嘿,注意我,我又来了!”当你优雅地写完一段代码,按下回车键,心里满是期待时,突然跳出来一个神秘的异常,这感觉就像是开门见到一只大黑狗——又惊又怕。
但是,别担心,今天我们要介绍的“FailureAnalyzer”就像一位经验丰富的兽医,专门为你的代码诊断问题,帮你把那些不请自来的错误处理得服服帖帖!它不仅能识别出问题所在,还能给你提供相应的解决方案,让你的代码重新焕发生机,宛如一只朝气蓬勃的小狗,摇着尾巴跑向美好的明天。
在这篇文章中,我们将一起探索FailureAnalyzer的奥秘,学习如何自定义它以应对那些棘手的错误。准备好了吗?让我们带上“程序员的护目镜”,一起踏上这场充满挑战和乐趣的冒险之旅吧!
一、走进FailureAnalyzer
想象一下,你正在开发一个基于Spring Boot的网络应用程序,你已经编写了一大堆代码,做了各种配置,终于迫不及待地想要启动你的应用程序,看看它是不是如你所愿地运行。
你兴奋地运行了启动命令,但突然间,控制台上出现了一堆红色的错误信息。如下:
可以立刻看到这个报错来自于LoggingFailureAnalysisReporter,其内部其实就是Spring Boot中被誉为故障排查神器的工具FailureAnalyzer
。你决定让它出马,看看能否解决你的问题。
你应该感到非常惊讶和兴奋,因为FailureAnalyzer
不仅仅找出了问题,还给出了解决方案:按照建议修复了配置,再次启动应用程序,这一次一切都运行得非常顺利。
通过这个简单的场景,你立刻感受到了FailureAnalyzer
的价值和魔力。它就像是你的应用程序启动的保险,让你在遇到问题时能够迅速找出解决方案,让你的开发过程更加流畅和高效。
二、在Spring Boot中如何生效
在Spring Boot的spring.factories
文件(位于META-INF
目录下)中已经包含了一些FailureAnalyzer
的配置,FailureAnalyzer
实现类通常在spring.factories
文件中被声明,以便在应用程序启动时被Spring Boot自动发现并注册。
org.springframework.boot.diagnostics.FailureAnalyzer=\ org.springframework.boot.autoconfigure.jdbc.DataSourceFailedAnalyzer,\ org.springframework.boot.autoconfigure.jms.activemq.ActiveMQConnectFailureAnalyzer,\ org.springframework.boot.autoconfigure.jms.artemis.ArtemisJmsConnectionFailureAnalyzer,\ org.springframework.boot.autoconfigure.jms.hornetq.HornetQConnectFailureAnalyzer,\ org.springframework.boot.autoconfigure.jms.hornetq.HornetQDependencyExceptionAnalyzer,\ org.springframework.boot.autoconfigure.solr.SolrExceptionAnalyzer,\ org.springframework.boot.autoconfigure.web.servlet.DispatcherServletRegistrationBeanNotAvailableAnalyzer,\ org.springframework.boot.cloud.CloudPlatformConnectorsFailureAnalyzer,\ org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessorFailureAnalyzer,\ org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessorChecker,\ org.springframework.boot.devtools.autoconfigure.DevToolsMissingFilterFailureAnalyzer,\ org.springframework.boot.devtools.autoconfigure.LocalDevToolsAutoConfiguration$LocalDevToolsFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.BeanDefinitionOverrideAnalyzer,\ org.springframework.boot.diagnostics.analyzer.IllegalComponentScanFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyNameFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyValueFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.InvalidEmbeddedServletContainerConfigurationFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.InvalidTemplateAvailabilityProviderAnalyzer,\ org.springframework.boot.diagnostics.analyzer.NonCompatibleConfigurationClassFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.SingleConstructorInjectionAnalyzer
当应用程序启动失败时,Spring Boot会自动触发这个机制,尝试识别和处理启动失败的原因,并提供有用的诊断信息和解决方案。
三、为什么可能需要自定义FailureAnalyzer
当然,如果有需要的话,我们可以
自定义FailureAnalyzer
来更灵活地处理应用程序启动失败的情况,提供更精准的故障诊断和解决方案等,特别是针对做基础架构的同学。
理由 | 说明 |
---|---|
特定错误情况处理 | 默认的 |
额外的诊断信息 | 默认的 |
集成外部系统 | 应用程序与外部系统集成,而启动失败可能是由于与这些外部系统的交互出现问题所致。通过自定义可以集成额外的逻辑,例如调用外部API或检查外部系统的状态,以诊断和解决与外部系统相关的问题。 |
定制化的解决方案 | 某些错误情况需要特定的解决方案,通过自定义可以根据应用程序的特定需求或约束,提供定制化的解决方案,以更好地满足应用程序的需求。 |
四、实现自定义基本步骤
(一)完整步骤要求
要实现自定义的FailureAnalyzer,我们需要完成以下步骤:
- 自定义异常,并创建检查要求规定。
- 创建一个类并实现AbstractFailureAnalyzer接口,重写analyze()方法,用于分析异常并返回FailureAnalysis对象。
- 将自定义的FailureAnalyzer类注册到Spring Boot应用程序中。
注意在 Spring Boot 应用程序中,自定义的多个失败分析器在实现上没有固定的先后次序。当应用程序启动时,Spring Boot 会自动扫描并注册所有的失败分析器,然后按照它们的类名顺序进行调用。这意味着,无论你如何组织和编写你的失败分析器类,它们都将在应用程序启动时同时注册,并且没有先后次序。
(二)注册方式说明
要让自定义的FailureAnalyzer
生效注册到Spring Boot应用程序中,一般有两种方法:
通过Spring Boot的spring.factories
文件(建议方式)
- 在
src/main/resources
目录下创建一个名为META-INF/spring.factories
的文件(如果已存在则跳过此步骤)。 - 在
spring.factories
文件中添加用于实现的自定义FailureAnalyzer类,和上文中展示的spring.factories
文件中的格式一样。
在启动类中手动注册(本人不建议)
在Spring Boot应用程序的启动类(@SpringBootApplication)中手动注册FailureAnalyzer
:
public static void main(String[] args) { SpringApplication application = new SpringApplication(ZYFApplication.class); application.addListeners(new ConfigFileFailureAnalyzer()); application.run(args); }
后续列举一些案例,但是情况请依据实际项目需求来定。我一般是以上面建议方式进行写的注册。
五、实现自定义举例
假设我们的应用程序在启动时需要加载某些特定的配置文件,但如果对应配置文件不存在将导致应用程序启动失败。默认的FailureAnalyzer
可能无法准确地识别或处理这种特定情况,因此我们可以自定义一个FailureAnalyzer
来处理这种特定的错误情况。
首先定义必要文件未找到异常如下:
package org.zyf.javabasic.spring.failureanalyzer.exception; /** * @program: zyfboot-javabasic * @description: ConfigFileNotFoundException * @author: zhangyanfeng * @create: 2024-05-02 17:25 **/ public class ConfigFileNotFoundException extends RuntimeException { private final String fileNames; public ConfigFileNotFoundException(String fileNames) { super("Configuration file '" + fileNames + "' not found"); this.fileNames = fileNames; } public String getFileNames() { return fileNames; } }
接着创建检查类对我们系统要求的必要文件作出基本的检查并返回要求文件异常基本信息:
package org.zyf.javabasic.spring.failureanalyzer.checker; import com.google.common.collect.Lists; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Component; import org.zyf.javabasic.spring.failureanalyzer.exception.ConfigFileNotFoundException; import javax.annotation.PostConstruct; import java.util.List; /** * @program: zyfboot-javabasic * @description: 系统必要配置文件检查 * @author: zhangyanfeng * @create: 2024-05-02 18:14 **/ @Component public class ConfigFileNotFoundChecker { private final ResourceLoader resourceLoader; public ConfigFileNotFoundChecker(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } public boolean exists(String fileName) { Resource resource = resourceLoader.getResource("classpath:" + fileName); return resource.exists(); } @PostConstruct public void checkConfigFiles() throws ConfigFileNotFoundException { // 要检查的文件列表 List<String> filesToCheck = Lists.newArrayList(); filesToCheck.add("application.yml"); filesToCheck.add("zyf_application_context.xml"); filesToCheck.add("report-config.xml"); filesToCheck.add("urlzyf.properties"); // 存储不存在的文件名 List<String> notFoundFiles = Lists.newArrayList(); // 检查每个文件是否存在 for (String fileName : filesToCheck) { if (!exists(fileName)) { notFoundFiles.add(fileName); } } // 如果存在未找到的文件,则抛出异常 if (!notFoundFiles.isEmpty()) { throw new ConfigFileNotFoundException(notFoundFiles.toString()); } } }
接着创建并实现AbstractFailureAnalyzer,重写analyze()方法如下:
package org.zyf.javabasic.spring.failureanalyzer.analyzer; import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; import org.springframework.boot.diagnostics.FailureAnalysis; import org.zyf.javabasic.spring.failureanalyzer.exception.ConfigFileNotFoundException; import org.zyf.javabasic.spring.failureanalyzer.exception.RequiredPropertyException; /** * @program: zyfboot-javabasic * @description: 检查必要文件是否存在异常 * @author: zhangyanfeng * @create: 2024-05-02 18:26 **/ public class ZYFConfigFileFailureAnalyzer extends AbstractFailureAnalyzer<ConfigFileNotFoundException> { @Override protected FailureAnalysis analyze(Throwable rootFailure, ConfigFileNotFoundException cause) { String description = description(cause); String action = action(cause); return new FailureAnalysis(description, action, cause); } private String description(ConfigFileNotFoundException ex) { return String.format("Failed to load configuration file '%s'.", ex.getFileNames()); } private String action(ConfigFileNotFoundException ex) { return String.format("Check if the configuration file:'%s' exists.", ex.getFileNames()); } }
spring.factories中增加本次新增验证:
org.springframework.boot.diagnostics.FailureAnalyzer=\ org.zyf.javabasic.spring.failureanalyzer.analyzer.ZYFConfigFileFailureAnalyzer
现在其中本地未创建ourlzyf.properties文件,故程序启动报错如下:
符合我们的预期。
接着如果要求针对报告配置文件 report-config.xml
中必须包含数据库连接信息和报告生成器的情况,并要求有更加详细和严格的配置文件格式验证,接着我们继续定义一个异常类且需要指明异常情况。
先定义一个这种类型返回的基本配置错误信息如下:
package org.zyf.javabasic.spring.failureanalyzer.model; /** * @program: zyfboot-javabasic * @description: 表示不同的错误类型,例如缺少必要属性、属性值格式错误等 * @author: zhangyanfeng * @create: 2024-05-02 19:57 **/ public class ConfigFileFormatErrorInfo { private final boolean fileNotFound; private final ErrorType errorType; private final String fileName; public ConfigFileFormatErrorInfo(boolean fileNotFound, ErrorType errorType, String fileName) { this.fileNotFound = fileNotFound; this.errorType = errorType; this.fileName = fileName; } public boolean isFileNotFound() { return fileNotFound; } public ErrorType getErrorType() { return errorType; } public String getFileName() { return fileName; } public DescriptionAndAction getDescriptionAndAction() { String description; String action; if (fileNotFound) { description = "Configuration file '" + fileName + "' not found"; action = "Check if the configuration file exists."; } else { switch (errorType) { case MISSING_PROPERTY: description = "Missing required property in configuration file '" + fileName + "'"; action = "Ensure all required properties are provided in the configuration file."; break; case INVALID_VALUE: description = "Invalid value for property in configuration file '" + fileName + "'"; action = "Correct the value of the property in the configuration file."; break; case OTHER: default: description = "Other configuration file format error in file '" + fileName + "'"; action = "Review the configuration file for formatting issues."; break; } } return new DescriptionAndAction(description, action); } public enum ErrorType { MISSING_PROPERTY, INVALID_VALUE, OTHER } } package org.zyf.javabasic.spring.failureanalyzer.model; /** * @program: zyfboot-javabasic * @description: DescriptionAndAction * @author: zhangyanfeng * @create: 2024-05-02 20:19 **/ public class DescriptionAndAction { private final String description; private final String action; public DescriptionAndAction(String description, String action) { this.description = description; this.action = action; } public String getDescription() { return description; } public String getAction() { return action; } }
然后定义基本的异常信息如下:
package org.zyf.javabasic.spring.failureanalyzer.exception; import com.alibaba.fastjson.JSON; import org.zyf.javabasic.spring.failureanalyzer.model.ConfigFileFormatErrorInfo; /** * @program: zyfboot-javabasic * @description: 配置文件格式问题异常 * @author: zhangyanfeng * @create: 2024-05-02 19:23 **/ public class ConfigFileFormatException extends RuntimeException { private final ConfigFileFormatErrorInfo errorInfo; public ConfigFileFormatException(ConfigFileFormatErrorInfo errorInfo) { super("Configuration file format error: " + JSON.toJSONString(errorInfo)); this.errorInfo = errorInfo; } public ConfigFileFormatErrorInfo getErrorInfo() { return errorInfo; } }
检查指定的配置文件(report-config.xml)是否符合预期的格式要求:
- 必须包含一个名为 "dataSource" 的 Bean 定义,用于配置数据库连接信息。
- "dataSource" Bean 中必须包含以下属性:
driverClassName
:数据库驱动类名;url
:数据库连接 URL;username
:数据库用户名;password
:数据库密码。 password
属性必须已加密,即以特定字符串开头。- 必须包含一个名为 "reportGenerator" 的 Bean 定义,用于配置报告生成器。
- "reportGenerator" Bean 中必须包含一个名为
dataSource
的属性引用,指向之前定义的 "dataSource" Bean。
如果配置文件不符合上述要求之一,就会抛出相应的异常,指示配置文件格式错误。具体对应的检查实现如下:
package org.zyf.javabasic.spring.failureanalyzer.checker; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Component; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.zyf.javabasic.spring.failureanalyzer.exception.ConfigFileFormatException; import org.zyf.javabasic.spring.failureanalyzer.model.ConfigFileFormatErrorInfo; import javax.annotation.PostConstruct; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.InputStream; import static org.zyf.javabasic.spring.failureanalyzer.model.ConfigFileFormatErrorInfo.ErrorType.*; /** * @program: zyfboot-javabasic * @description: 指定配置文件验证逻辑 * @author: zhangyanfeng * @create: 2024-05-02 20:12 **/ @Component public class ConfigFileFormatChecker { @Autowired private ResourceLoader resourceLoader; @PostConstruct public void checkConfigFileFormat() { String fileName = "report-config.xml"; Resource resource = resourceLoader.getResource("classpath:" + fileName); if (!resource.exists()) { throw new ConfigFileFormatException(new ConfigFileFormatErrorInfo(true, null, fileName)); } Element root = null; try (InputStream inputStream = resource.getInputStream()) { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(new InputSource(inputStream)); // 获取根元素 root = document.getDocumentElement(); } catch (Exception e) { throw new ConfigFileFormatException(new ConfigFileFormatErrorInfo(true, OTHER, fileName)); } // 检查 dataSource Bean 定义 checkDataSourceDefinition(root, fileName); // 检查报告生成器定义 checkReportGeneratorDefinition(root, fileName); } private void checkDataSourceDefinition(Element root, String fileName) { // 获取 dataSource 元素 NodeList dataSourceList = root.getElementsByTagName("bean"); for (int i = 0; i < dataSourceList.getLength(); i++) { Element dataSourceElement = (Element) dataSourceList.item(i); String id = dataSourceElement.getAttribute("id"); if ("dataSource".equals(id)) { // 获取 driverClassName 属性 String driverClassName = dataSourceElement.getElementsByTagName("property") .item(0) .getAttributes() .getNamedItem("value") .getNodeValue(); // 获取 url 属性 String url = dataSourceElement.getElementsByTagName("property") .item(1) .getAttributes() .getNamedItem("value") .getNodeValue(); // 获取 username 属性 String username = dataSourceElement.getElementsByTagName("property") .item(2) .getAttributes() .getNamedItem("value") .getNodeValue(); // 获取 password 属性 String password = dataSourceElement.getElementsByTagName("property") .item(3) .getAttributes() .getNamedItem("value") .getNodeValue(); if (StringUtils.isAnyBlank(driverClassName, url, username, password)) { throw new ConfigFileFormatException(new ConfigFileFormatErrorInfo(false, MISSING_PROPERTY, fileName)); } if (!isPasswordEncrypted(password)) { throw new ConfigFileFormatException(new ConfigFileFormatErrorInfo(false, INVALID_VALUE, fileName)); } } } } private void checkReportGeneratorDefinition(Element root, String fileName) { // 获取 reportGenerator 元素 NodeList reportGeneratorList = root.getElementsByTagName("bean"); for (int i = 0; i < reportGeneratorList.getLength(); i++) { Element reportGeneratorElement = (Element) reportGeneratorList.item(i); String id = reportGeneratorElement.getAttribute("id"); if ("reportGenerator".equals(id)) { // 获取 dataSource 属性的引用 String dataSourceRef = reportGeneratorElement.getElementsByTagName("property") .item(0) .getAttributes() .getNamedItem("ref") .getNodeValue(); if (StringUtils.isAnyBlank(dataSourceRef)) { throw new ConfigFileFormatException(new ConfigFileFormatErrorInfo(false, MISSING_PROPERTY, fileName)); } } } } private boolean isPasswordEncrypted(String password) { // 检查密码是否已加密,这里可以根据具体加密方式进行验证 return password.startsWith("Zyf"); } }
接着创建并实现AbstractFailureAnalyzer,重写analyze()方法如下:
package org.zyf.javabasic.spring.failureanalyzer.analyzer; import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; import org.springframework.boot.diagnostics.FailureAnalysis; import org.zyf.javabasic.spring.failureanalyzer.exception.ConfigFileFormatException; import org.zyf.javabasic.spring.failureanalyzer.model.ConfigFileFormatErrorInfo; import org.zyf.javabasic.spring.failureanalyzer.model.DescriptionAndAction; /** * @program: zyfboot-javabasic * @description: 指定配置文件具体格式要求 * @author: zhangyanfeng * @create: 2024-05-02 20:31 **/ public class ZYFConfigFileFormatFailureanalyzer extends AbstractFailureAnalyzer<ConfigFileFormatException> { @Override protected FailureAnalysis analyze(Throwable rootFailure, ConfigFileFormatException cause) { ConfigFileFormatErrorInfo errorInfo = cause.getErrorInfo(); String description; String action; if (errorInfo.isFileNotFound()) { description = "Configuration file '" + errorInfo.getFileName() + "' not found"; action = "Check if the configuration file exists."; } else { DescriptionAndAction descriptionAndAction = errorInfo.getDescriptionAndAction(); description = descriptionAndAction.getDescription(); action = descriptionAndAction.getAction(); } return new FailureAnalysis(description, action, cause); } }
spring.factories中增加本次新增验证:
org.springframework.boot.diagnostics.FailureAnalyzer=\ org.zyf.javabasic.spring.failureanalyzer.analyzer.ZYFConfigFileFailureAnalyzer,\ org.zyf.javabasic.spring.failureanalyzer.analyzer.ZYFConfigFileFormatFailureanalyzer
但是我实际report-config.xml配置如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 数据库连接信息 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/zyf"/> <property name="username" value="root"/> <property name="password" value="Zsyf2014"/> </bean> <!-- 报告生成器 --> <bean id="reportGenerator" class="org.zyf.javabasic.spring.beanFactory.ReportGenerator"> <property name="dataSource" ref="dataSource"/> <!-- 其他配置属性 --> </bean> </beans>
由于数据库加密不正确,故程序启动报错如下:
六、一些建议
如果确定了需要创建自定义的 FailureAnalyzer
时,必须有几个注意事项:
- 确保
FailureAnalyzer
能够准确地识别失败的原因,避免误导性的诊断,帮助更快地找到并解决问题。 - 在诊断中提供尽可能详细的信息,包括失败的具体原因、发生失败的位置等。
- 提供清晰的建议或解决方案,可以包括修复代码、调整配置或执行其他操作的建议。
相关源码依旧在常用的github地址中。
七、总结
在本文中,我们深入探讨了FailureAnalyzer这一强大的工具,它不仅能帮助我们快速识别和处理代码中的错误,还能极大地提升我们的开发效率。通过详细的实例分析,我们了解了FailureAnalyzer如何通过自定义逻辑应对不同类型的异常,让程序员能够更好地定位问题并迅速找到解决方案。
借助FailureAnalyzer,我们可以在面对复杂的错误时不再手忙脚乱,而是能够从容应对。这种能力的提升,不仅让我们的代码更加稳健,也让我们在开发过程中保持了更高的生产力和信心。正如我们在引言中提到的,面对那些不断“汪汪”作响的错误,FailureAnalyzer就像一位贴心的朋友,时刻陪伴在侧,为我们指引方向。
最后,掌握FailureAnalyzer的使用,不仅能帮助我们更有效地处理错误,还能促使我们在编程的旅程中不断成长。希望本文能激励你在未来的项目中充分利用这一工具,持续提升自己的代码质量和开发技能。让我们一起,迈向更加稳定和高效的编程世界!
到此这篇关于Spring Boot的FailureAnalyzer机制的文章就介绍到这了,更多相关Spring Boot FailureAnalyzer机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!