SpringBoot源码分析(6)--SpringBootExceptionReporter/异常报告器

文章目录

  • 一、前言
  • 二、异常报告器介绍
    • 2.1、作用
    • 2.2、接口定义
    • 2.3、FailureAnalyzer错误分析器
    • 2.4、FailureAnalysisReporter错误报告器
  • 三 、SpringBootExceptionReporter源码分析
  • 四、shutdownHook介绍
    • 4.1、背景
    • 4.2、什么是Shutdown Hook
    • 4.3、什么时候会调用Shutdown Hook
    • 4.4、如何使用Shutdown Hook
    • 4.5、测试例子
  • 五、自定义异常报告器
    • 5.1、方式一:实现SpringBootExceptionReporter接口
    • 5.2、方式二:实现FailureAnalyzer接口
  • 六、总结

一、前言

本文基于spring-boot-2.2.14.BUILD-SNAPSHOT源码分析SpringBootExceptionReporter异常报告器

本文主要内容是SpringBoot项目启动过程的异常分析器,也就是SpringBootExceptionReporter,回想我们启动项目的时候,是否会因为缺少数据库配置、端口占用、bean命名重复等各种原因,导致项目启动失败呢,比如端口占用的情况下启动项目,控制台会打印如下日志

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2023-07-14 15:12:35.836 ERROR 21456 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : ***************************
APPLICATION FAILED TO START
***************************Description:Web server failed to start. Port 80 was already in use.Action:Identify and stop the process that's listening on port 80 or configure this application to listen on another port.

这段日志会指出启动失败的原因,以及建议的解决方案,比如添加某些配置,或者将某配置设置为true等等
SpringBootExceptionReporter的作用就是对启动过程的异常进行分析、报告,涉及到的代码在SpringApplication类的run方法中:

二、异常报告器介绍

2.1、作用

收集错误信息,用于向用户报告错误原因。

Spring Boot 提出了错误分析器(FailureAnalyzer)与错误报告器(FailureAnalysisReporter)的概念,前者用于将报错信息转换为更加详细的错误分析报告,后者负责将这个报告呈现出来。

2.2、接口定义

@FunctionalInterface
public interface SpringBootExceptionReporter {// 向用户报告失败信息boolean reportException(Throwable failure);}

接口实现

@Override
public boolean reportException(Throwable failure) {//调用FailureAnalyzer获得错误分析报告FailureAnalysis FailureAnalysis analysis = analyze(failure, this.analyzers);//调用FailureAnalysisReporter将报告呈现出来return report(analysis, this.classLoader);
}

2.3、FailureAnalyzer错误分析器

调用FailureAnalyzer获得错误分析报告FailureAnalysis

Spring Boot 中对于 FailureAnalyzer 接口的定义如下,只有一个 analyze 方法,入参是 Throwable,也就是所有异常的基类,返回一个 FailureAnalysis,也就是错误分析报告。

@FunctionalInterface
public interface FailureAnalyzer {FailureAnalysis analyze(Throwable failure);
}

FailureAnalyzer 需要表明自己是哪些异常的分析器,AbstractFailureAnalyzer 实现了 FailureAnalyzer 方法,并在类上申明一个泛型,这个泛型类就是该分析器感兴趣的异常类。具体的代码也很简单,核心是调用异常的 getCause() 进行循环/遍历,以检查异常及其消息的根源,判断是否和泛型是一个类型,Spring Boot 中大部分的分析器都会继承 AbstractFailureAnalyzer。

public abstract class AbstractFailureAnalyzer<T extends Throwable> implements FailureAnalyzer {...
}

回过头来看错误分析报告,该类中包含了这个错误的详细描述(description),错误的解决方式(action)以及异常本身(cause)。我们可以认为,这个报告是 Srping Boot 对于异常类的二次封装,在不破坏原本异常信息的前提下,额外增加了更加详细的异常信息。

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;}...}

2.4、FailureAnalysisReporter错误报告器

负责展示这些错误分析报告

FailureAnalysisReporter 也是一个单方法的接口,入参就是错误分析报告。

@FunctionalInterface
public interface FailureAnalysisReporter {void report(FailureAnalysis analysis);
}

Spring Boot 默认提供了一个 FailureAnalysisReporter,那就是 LoggingFailureAnalysisReporter。这个类会根据当前日志级别的不同,调用日志的 debug 或 error方法进行打印。

public final class LoggingFailureAnalysisReporter implements FailureAnalysisReporter {private static final Log logger = LogFactory.getLog(LoggingFailureAnalysisReporter.class);@Overridepublic 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));}}...}

总结下 Spring Boot 异常处理方案:Spring Boot 在捕获一个异常后,会调用该异常对应的 FailureAnalyzer 对其进行分析,将异常转换为 FailureAnalysis。然后调用 FailureAnalysisReporter 对异常分析报告打印出来。

三 、SpringBootExceptionReporter源码分析

异常报告器是用来捕捉全局异常使用的,当springboot应用程序在发生异常时,异常报告器会将其捕捉并做相应处理

public ConfigurableApplicationContext run(String... args) {......Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();try {......// 获取所有 SpringBootExceptionReporter 实现类exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,new Class[] { ConfigurableApplicationContext.class }, context);......}catch (Throwable ex) {handleRunFailure(context, ex, exceptionReporters, listeners);throw new IllegalStateException(ex);}try {......}catch (Throwable ex) {handleRunFailure(context, ex, exceptionReporters, null);throw new IllegalStateException(ex);}return context;
}

需要注意的是,这个异常报告器只会捕获启动过程抛出的异常,如果是在启动完成后,在用户请求时报错,异常报告器不会捕获请求中出现的异常
在这里插入图片描述
getSpringFactoriesInstances到classpath下的META-INF/spring.factories文件中找SpringBootExceptionReporter的实现类,然后将新建的容器作为参数调用其构造方法

最终只找到一个FailureAnalyzers,在spring-boot包下
在这里插入图片描述
查看FailureAnalyzers类的构造方法

final class FailureAnalyzers implements SpringBootExceptionReporter {private static final Log logger = LogFactory.getLog(FailureAnalyzers.class);private final ClassLoader classLoader;private final List<FailureAnalyzer> analyzers;FailureAnalyzers(ConfigurableApplicationContext context) {this(context, (ClassLoader)null);}FailureAnalyzers(ConfigurableApplicationContext context, ClassLoader classLoader) {Assert.notNull(context, "Context must not be null");this.classLoader = classLoader != null ? classLoader : context.getClassLoader();this.analyzers = this.loadFailureAnalyzers(this.classLoader);prepareFailureAnalyzers(this.analyzers, context);}

使用容器的类加载器,去加载具体的异常分析器,进入loadFailureAnalyzers方法

private List<FailureAnalyzer> loadFailureAnalyzers(ClassLoader classLoader) {List<String> analyzerNames = SpringFactoriesLoader.loadFactoryNames(FailureAnalyzer.class, classLoader);List<FailureAnalyzer> analyzers = new ArrayList<>();for (String analyzerName : analyzerNames) {try {Constructor<?> constructor = ClassUtils.forName(analyzerName, classLoader).getDeclaredConstructor();ReflectionUtils.makeAccessible(constructor);analyzers.add((FailureAnalyzer) constructor.newInstance());}catch (Throwable ex) {logger.trace(LogMessage.format("Failed to load %s", analyzerName), ex);}}AnnotationAwareOrderComparator.sort(analyzers);return analyzers;
}

同样是到spring.factories中加载FailureAnalyzer类型的实现类,并实例化
这次共找到了19个实现类,其中14个位于spring-boot包下,5个位于spring-boot-autoconfigure包下,看名字大多数还是比较熟悉的,比如循环依赖异常、beanDefinition重复异常、端口占用异常等等
在这里插入图片描述
回到FailureAnalyzers构造方法,加载到FailureAnalyzer列表后,调用prepareFailureAnalyzers方法

private void prepareFailureAnalyzers(List<FailureAnalyzer> analyzers, ConfigurableApplicationContext context) {for (FailureAnalyzer analyzer : analyzers) {prepareAnalyzer(context, analyzer);}
}

循环FailureAnalyzer列表,调用prepareAnalyzer方法

private void prepareAnalyzer(ConfigurableApplicationContext context, FailureAnalyzer analyzer) {if (analyzer instanceof BeanFactoryAware) {((BeanFactoryAware) analyzer).setBeanFactory(context.getBeanFactory());}if (analyzer instanceof EnvironmentAware) {((EnvironmentAware) analyzer).setEnvironment(context.getEnvironment());}
}

这个方法查看如果FailureAnalyzer实现了BeanFactoryAware接口和EnvironmentAware接口,将对应的BeanFactory和Environment赋给它

之所以有这个步骤,是因为有些异常分析器处理异常信息的过程中,有可能要依赖容器或者项目的环境,而Aware接口正常的执行时机是在容器刷新的时候,如果在Aware的过程中,或者在这之前就发生了异常,这一部分FailureAnalyzer就没办法正常工作了,所以需要提前将依赖设置进来

需要注意的是,这里设置进来的environment,是直接从容器中取的,它是在容器的构造函数中新建的,并不是我们之前经过了一系列加工的environment,虽然后面会用我们的environment将容器中的替换掉,但是这些FailureAnalyzer持有的environment并没有一起更新,所以这个步骤我个人认为是有点问题的。(上一篇文章中我们有讲过为什么会有两套环境)

prepareAnalyzer完成后,加载SpringBootExceptionReporter的流程就结束了,接下来看catch中是怎么使用这么分析器的,进入handleRunFailure方法

private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception, Collection<SpringBootExceptionReporter> exceptionReporters, SpringApplicationRunListeners listeners) {try {try {handleExitCode(context, exception);if (listeners != null) {//发送启动失败事件listeners.failed(context, exception);}} finally {// 报告失败信息reportFailure(exceptionReporters, exception);if (context != null) {//关闭上下文context.close();}}} catch (Exception ex) {logger.warn("Unable to close ApplicationContext", ex);}ReflectionUtils.rethrowRuntimeException(exception);
}

在这里插入图片描述

先看第一行handleExitCode,它根据exitCode来决定是否发送退出事件,同时也提供了一些接口让我们可以自定义exitCode,0 为正常出,非 0 是异常退出。

private void handleExitCode(ConfigurableApplicationContext context, Throwable exception) {// 从异常中获取退出代码int exitCode = getExitCodeFromException(context, exception);if (exitCode != 0) {if (context != null) {context.publishEvent(new ExitCodeEvent(context, exitCode));}SpringBootExceptionHandler handler = getSpringBootExceptionHandler();if (handler != null) {handler.registerExitCode(exitCode);}}
}

getExitCodeFromException方法根据容器的状态以及异常类型来获取exitCode

private int getExitCodeFromException(ConfigurableApplicationContext context, Throwable exception) {int exitCode = getExitCodeFromMappedException(context, exception);if (exitCode == 0) {exitCode = getExitCodeFromExitCodeGeneratorException(exception);}return exitCode;
}

getExitCodeFromMappedException方法,如果容器还没有启动,直接返回0,否则从容器中获取ExitCodeExceptionMapper类型的Bean,赋给ExitCodeGenerators,并调用其getExitCode方法获取退出码

private int getExitCodeFromMappedException(ConfigurableApplicationContext context, Throwable exception) {if (context == null || !context.isActive()) {return 0;}ExitCodeGenerators generators = new ExitCodeGenerators();Collection<ExitCodeExceptionMapper> beans = context.getBeansOfType(ExitCodeExceptionMapper.class).values();generators.addAll(exception, beans);return generators.getExitCode();
}

ExitCodeExceptionMapper是一个函数式接口,提供了从异常中获取退出码的方法,我们可以通过实现这个接口,来自定义退出码

@FunctionalInterface
public interface ExitCodeExceptionMapper {int getExitCode(Throwable exception);}

通过getExitCode方法遍历这个列表,根据if里面的条件,其实不确定最终的响应码到底是正数还是负数,正负码之间并没有相对的优先级,而程序最终关心的是退出码是否为0

int getExitCode() {int exitCode = 0;for (ExitCodeGenerator generator : this.generators) {try {int value = generator.getExitCode();if (value > 0 && value > exitCode || value < 0 && value < exitCode) {exitCode = value;}}catch (Exception ex) {exitCode = (exitCode != 0) ? exitCode : 1;ex.printStackTrace();}}return exitCode;
}

回到getExitCodeFromException方法,通过上面这一步拿到的退出码如果是0,会再根据异常做一次判断,因为有可能第一步容器并没有激活,直接就返回0了,调用getExitCodeFromExitCodeGeneratorException方法,如果异常类实现了ExitCodeGenerator 接口,就调用其getExitCode方法获取退出码

private int getExitCodeFromExitCodeGeneratorException(Throwable exception) {if (exception == null) {return 0;}if (exception instanceof ExitCodeGenerator) {return ((ExitCodeGenerator) exception).getExitCode();}return getExitCodeFromExitCodeGeneratorException(exception.getCause());}

最终返回的退出码如果不是0,就通过容器发布一个ExitCodeEvent事件,并将退出码注册到SpringBootExceptionHandler上,用于后续的日志记录

退出码处理完毕,回到handleRunFailure方法,接下来看listeners 如果不为空,就通过它来发布启动失败事件

private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception,Collection<SpringBootExceptionReporter> exceptionReporters, SpringApplicationRunListeners listeners) {try {try {handleExitCode(context, exception);if (listeners != null) {listeners.failed(context, exception);}}finally {reportFailure(exceptionReporters, exception);if (context != null) {context.close();}}}catch (Exception ex) {logger.warn("Unable to close ApplicationContext", ex);}ReflectionUtils.rethrowRuntimeException(exception);
}

此时listeners 肯定不为空的,在前面的文章,我们已经通过它发布了应用启动事件ApplicationStartingEvent,以及环境准备就绪事件ApplicationEnvironmentPreparedEvent,而这里就是要发布应用启动失败相关的事件,进入failed方法

void failed(ConfigurableApplicationContext context, Throwable exception) {for (SpringApplicationRunListener listener : this.listeners) {callFailedListener(listener, context, exception);}
}

跟之前一样,这个listeners列表只有一个元素EventPublishingRunListener,将它传给callFailedListener方法

private void callFailedListener(SpringApplicationRunListener listener, ConfigurableApplicationContext context,Throwable exception) {try {listener.failed(context, exception);}catch (Throwable ex) {if (exception == null) {ReflectionUtils.rethrowRuntimeException(ex);}if (this.log.isDebugEnabled()) {this.log.error("Error handling failed", ex);}else {String message = ex.getMessage();message = (message != null) ? message : "no error message";this.log.warn("Error handling failed (" + message + ")");}}
}

最后是调用了EventPublishingRunListener的fail方法

@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {ApplicationFailedEvent event = new ApplicationFailedEvent(this.application, this.args, context, exception);if (context != null && context.isActive()) {// Listeners have been registered to the application context so we should// use it at this point if we cancontext.publishEvent(event);}else {// An inactive context may not have a multicaster so we use our multicaster to// call all of the context's listeners insteadif (context instanceof AbstractApplicationContext) {for (ApplicationListener<?> listener : ((AbstractApplicationContext) context).getApplicationListeners()) {this.initialMulticaster.addApplicationListener(listener);}}this.initialMulticaster.setErrorHandler(new LoggingErrorHandler());this.initialMulticaster.multicastEvent(event);}
}

这里先初始化了一个事件ApplicationFailedEvent ,然后判断容器是否已经启动了,如果是,就由容器来负责事件的发布,否则将容器中已经存在的事件监听器注册到当前的事件多播器中,跟之前几个事件的发布流程一样,继续由它来发布事件

处理退出码、发布启动失败事件等流程结束后,分析异常原因,并关闭容器

   //...... 省略其他代码finally {this.reportFailure(exceptionReporters, exception);if (context != null) {context.close();}}//...... 省略其他代码

看下reportFailure的实现,入参就是开始时找到的SpringBootExceptionReporter,只有一个实现FailureAnalyzers

private void reportFailure(Collection<SpringBootExceptionReporter> exceptionReporters, Throwable failure) {try {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);}
}

进入FailureAnalyzers类的reportException方法

public boolean reportException(Throwable failure) {FailureAnalysis analysis = analyze(failure, this.analyzers);return report(analysis, this.classLoader);
}

先调用analyze,用之前找到的19个异常解析器去分析异常原因,直到某个解析器返回的解析结果不为空,就结束遍历

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.debug(LogMessage.format("FailureAnalyzer %s failed", analyzer), ex);}}return null;
}

文章开头的异常日志中,启动失败的原因以及建议的解决方案,就是封装在这个解析结果中

public class FailureAnalysis {private final String description;private final String action;private final Throwable cause;
}

解析的过程由每个解析器去实现,根据异常的类型来决定是否返回结果,然后将解析结果传给FailureAnalyzers类的report方法

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;
}

该方法先到spring.factories中找FailureAnalysisReporter的实现类,它决定了异常分析结果的报告形式,默认只找到一个LoggingFailureAnalysisReporter,定义在spring-boot包下

# FailureAnalysisReporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter

也就是最终调用了LoggingFailureAnalysisReporter的report方法

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));}
}

根据传进来的结果,调用buildMessage构建输出信息,这个内容就很熟悉了,就是前面日志里展示的异常报告格式

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();
}

打印信息如下:

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2023-07-14 15:12:35.836 ERROR 21456 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : ***************************
APPLICATION FAILED TO START
***************************Description:Web server failed to start. Port 80 was already in use.Action:Identify and stop the process that's listening on port 80 or configure this application to listen on another port.

最后调用context.close方法
在这里插入图片描述
close方法先调用doClose方法,然后移除钩子方法。

public void close() {synchronized (this.startupShutdownMonitor) {doClose();// If we registered a JVM shutdown hook, we don't need it anymore now:// We've already explicitly closed the context.if (this.shutdownHook != null) {try {// 移除钩子方法Runtime.getRuntime().removeShutdownHook(this.shutdownHook);}catch (IllegalStateException ex) {// ignore - VM is already shutting down}}}
}

doClose方法。发布shoutdown广播,关闭一些bean和工厂bean,方便垃圾回收。

protected void doClose() {// Check whether an actual close attempt is necessary...if (this.active.get() && this.closed.compareAndSet(false, true)) {if (logger.isDebugEnabled()) {logger.debug("Closing " + this);}LiveBeansView.unregisterApplicationContext(this);try {// 发布容器关闭事件publishEvent(new ContextClosedEvent(this));}catch (Throwable ex) {logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);}// Stop all Lifecycle beans, to avoid delays during individual destruction.if (this.lifecycleProcessor != null) {try {this.lifecycleProcessor.onClose();}catch (Throwable ex) {logger.warn("Exception thrown from LifecycleProcessor on context close", ex);}}// Destroy all cached singletons in the context's BeanFactory.// 销毁所有的单例beandestroyBeans();// Close the state of this context itself.// 关闭容器closeBeanFactory();// Let subclasses do some final clean-up if they wish...// 调用子类的重写方法,关闭web服务器onClose();// Reset local application listeners to pre-refresh state.if (this.earlyApplicationListeners != null) {this.applicationListeners.clear();this.applicationListeners.addAll(this.earlyApplicationListeners);}// Switch to inactive.this.active.set(false);}
}

钩子方法。

在JVM退出时,调用这个方法

public static void main(String[] args) {System.out.println("hello");Thread close_jvm = new Thread(()-> System.out.println("close jvm"));Runtime.getRuntime().addShutdownHook(close_jvm);System.out.println("world");
}
hello
world
close jvm

四、shutdownHook介绍

  • 作用:JVM退出时执行的业务逻辑
  • 添加:Runtime.getRuntime().addShutdownHook()
  • 移除:Runtime.getRuntime().removeShutdownHook(this.shutdownHook)

4.1、背景

在开发中,遇到这种情况,多个线程同时工作,突然一个线程遇到了fetal的错误,需要立即终止程序,等人工排查解决了问题之后重新启动。但是这样会有一个问题,程序终止时,其他线程可能正在进行重要操作,比如发一个message到另一个模块,并更新数据库状态。突然终止,可能会让这个操作只完成一半,从而导致数据不一致。

解决方案是:参考数据库Transaction原子性的概念,将这一系列重要操作看作一个整体,要么全部完成,要么全部不完成。为方便表述,我们把这一系列重要操作记为操作X。

当程序即将退出时,查看当前是否有操作X在执行中,如果有,等待其完成然后退出。且期间不再接受新的操作X。如果操作X执行之间过长,终止并回滚所有状态。
如果没有,则可以立即退出。

在程序退出的时候,做一些Check,保证已经开始的操作X的原子性,这里就用到了Runtime.ShutdownHook。

4.2、什么是Shutdown Hook

Shutdown hook是一个initialized but unstarted thread。当JVM开始执行shutdown sequence时,会并发运行所有registered Shutdown Hook。这时,在Shutdown Hook这个线程里定义的操作便会开始执行。

需要注意的是,在Shutdown Hook里执行的操作应当是不太耗时的。因为在用户注销或者操作系统关机导致的JVM shutdown的例子中,系统只会预留有限的时间给未完成的工作,超时之后还是会强制关闭。

4.3、什么时候会调用Shutdown Hook

程序正常停止

  • Reach the end of program
  • System.exit

程序异常退出

  • NPE
  • OutOfMemory

受到外界影响停止

  • Ctrl+C
  • kill -9
  • 用户注销或者关机

4.4、如何使用Shutdown Hook

调用java.lang.Runtime这个类的addShutdownHook(Thread hook)方法即可注册一个Shutdown Hook,然后在Thread中定义需要在system exit时进行的操作。如下:

Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("Do something in Shutdown Hook")
));

4.5、测试例子

  • 首先,注册了一个Shutdown Hook。
  • 然后,系统Sleep 3秒,模拟进行某些操作。
  • 然后,调用一个空的List,抛出异常,准备结束程序。
  • 在程序将要结束的时候,执行Shutdown Hook中的内容。
public static void main(String[] args)
{// register shutdown hookRuntime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("Do something in Shutdown Hook")));// sleep for some timetry {for (int i=0; i<3; i++) {System.out.println("Count: " + i + "...");TimeUnit.MILLISECONDS.sleep(1000);}List nullList = new ArrayList<>();System.out.println("Trying to print null list's first element: " + nullList.get(0).toString());} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Ready to exit.");System.exit(0);
}

结果如下:

Count: 0...
Count: 1...
Count: 2...
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0at java.util.ArrayList.rangeCheck(ArrayList.java:653)at java.util.ArrayList.get(ArrayList.java:429)at HookTest.main(HookTest.java:18)
Do something in Shutdown HookProcess finished with exit code 1

需要注意的点

  • 当System.exit之后,当Shutdown Hook开始执行时,其他的线程还是会继续执行。
  • 应当保证Shutdown Hook的线程安全。
  • 在使用多个Shutdown Hook时一定要特别小心,保证其调用的服务不会被其他Hook影响。否则会出现当前Hook所依赖的服务被另外一个Hook终止了的情况。

五、自定义异常报告器

5.1、方式一:实现SpringBootExceptionReporter接口

(1):模拟异常,创建UserService

public class UserService {}

引入UserService

@RestController
public class UserController {@AutowiredUserService userService;}

由于UserService没有加入IOC容器中,所以这里正常启动后报错如下:

***************************
APPLICATION FAILED TO START
***************************Description:Field userService in com.example.demo.controller.UserController required a bean of type 'com.example.demo.service.UserService' that could not be found.The injection point has the following annotations:- @org.springframework.beans.factory.annotation.Autowired(required=true)Action:Consider defining a bean of type 'com.example.demo.service.UserService' in your configuration.

我们想要打印自己定义的格式应该如何做呢?

(2):自定义异常

package com.example.demo.exception;import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.boot.SpringBootExceptionReporter;
import org.springframework.context.ConfigurableApplicationContext;//自定义异常报告器
public class MyExceptionReporter  implements SpringBootExceptionReporter{private ConfigurableApplicationContext context;//实例化构造方法 如果不实例化会报错 报 Cannot instantiate interface//java.lang.NoSuchMethodException: com.example.demo.exception.MyExceptionReporter.// <init>(org.springframework.context.ConfigurableApplicationContext)public MyExceptionReporter(ConfigurableApplicationContext context) {this.context = context;}@Overridepublic boolean reportException(Throwable failure) {if(failure instanceof UnsatisfiedDependencyException){UnsatisfiedDependencyException exception = (UnsatisfiedDependencyException)failure;System.out.println("no such bean " + exception.getInjectionPoint().getField().getName());}//返回false打印详细信息 返回true只打印异常信息return false;}
}

在 spring.factories 文件中注册异常报告器

# 注册异常报告器
org.springframework.boot.SpringBootExceptionReporter=\
com.example.demo.exception.MyExceptionReporter

(3):运行程序,错误输出如下
在这里插入图片描述

5.2、方式二:实现FailureAnalyzer接口

(1):自定义一个异常

public class MyException extends RuntimeException{
}

(2):实现FailureAnalyzer接口

package com.example.demo.exception;public class MyFailureAnalyzer extends AbstractFailureAnalyzer<MyException> {@Overrideprotected FailureAnalysis analyze(Throwable rootFailure, MyException cause) {String des = "发生自定义异常";String action = "由于自定义了一个异常";return new FailureAnalysis(des, action, rootFailure);}
}

在 spring.factories 文件中注册异常分析器

org.springframework.boot.diagnostics.FailureAnalyzer=\
com.example.demo.exception.MyFailureAnalyzer 

(3)测试
需要在Spring Boot 启动的时候抛出异常,为了测试,我们在上下文准备的时候抛出自定义异常,添加到demo中的MyApplicationRunListener中。

public void contextPrepared(ConfigurableApplicationContext context) {System.out.println("在创建和准备ApplicationContext之后,但在加载源之前调用");throw new MyException();
}

启动后打印异常日志如下:

***************************
APPLICATION FAILED TO START
***************************Description:发生自定义异常Action:由于自定义了一个异常

六、总结

  1. Spring Boot 为了将启动过程中的错误以更加友好,灵活的方式呈现出来,设计了一套异常处理方案。
  2. Spring Boot 提出了错误分析器(FailureAnalyzer)与错误报告器(FailureAnalysisReporter)的概念,前者用于将报错信息转换为更加详细的错误分析报告,后者负责将这个报告呈现出来。
  3. 错误分析器(FailureAnalyzer)的职责是识别当前错误的类型,对于感兴趣的错误进行二次包装,包装的结果就是错误分析报告(FailureAnalysis)。
  4. 错误分析报告(FailureAnalysis)中除了原始的错误信息外,新增了描述(description)以及行为(action)用于提示用户后续的处理方式。
  5. Spring Boot 框架异常处理体系中大量使用 SPI 的方式进行特定类的加载,方便框架后续对异常处理方案,特定异常检查,异常展示方式的扩展。

参考文章:
https://blog.csdn.net/m0_37298252/article/details/122879031
https://blog.51cto.com/u_14014612/6007663

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/27146.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NUXT3学习笔记2

1、配置Ant design Vue (两个安装方式随便选一种&#xff0c;yarn会安装的更快) npm i ant-design-vue --save yarn add ant-design-vue 2、使⽤的 Vite&#xff0c;你可以使⽤ unplugin-vue-components 来进⾏按需加载。 yarn add unplugin-vue-components --save 在nuxt.…

第一阶段-第十一章 Python基础的综合案例(数据可视化-地图可视化)

目录 一、基础地图使用  1.学习目标  2.视觉映射器  3.本节的演示二、疫情地图-国内疫情地图  1.案例效果  2.函数的语法  3.本节的代码演示三、疫情地图-省级疫情地图  1.案例效果  2.本节的代码演示 说明&#xff1a;该文章是学习 黑马程序员在B站上分享的视…

【c++修行之路】IO流架构及使用

文章目录 前言输入输出库文件读写序列化与反序列化结语 前言 大家好久不见&#xff0c;今天一起来学习c中的IO流。 输入输出库 这两张架构图略显复杂&#xff0c;这里给出一张比较清楚的IO流架构图&#xff1a; 也就是说&#xff0c;我们平时使用的诸如cin、cout、cerr、cl…

python selenium.webdriver 爬取政策文件

文章目录 获取文章链接批量爬取政策文件应用selenium爬取文件信息数据处理导出为excel 获取文章链接 获取中央人民政府网站链接&#xff0c;进入国务院政策文件库&#xff0c;分为国务院文件和部门文件&#xff08;发改委、工信部、交通运输部、市场监督局、商务部等&#xff…

QT实现按钮开关Form窗体的效果

实现效果叙述如下&#xff1a; MainWindow中的按钮实现Form窗体的开关&#xff0c;Form窗体的关闭按钮禁用掉&#xff0c;只允许使用窗体按钮进行&#xff0c;且关闭MainWindow按钮时Form窗体随之关闭。 注意&#xff1a; 要想实现关闭MainWindow按钮时Form窗体随之关闭&#x…

k8s之Pod容器资源限制

目录 一、Pod 容器的资源限制二、CPU 资源单位三、内存资源单位四、为本地临时性存储设置请求和限制五、总结 一、Pod 容器的资源限制 当定义 Pod 时可以选择性地为每个容器设定所需要的资源数量。 最常见的可设定资源是 CPU 和内存大小&#xff0c;以及其他类型的资源。 当为…

arcgis拓扑检查

不能有悬挂点 不能有伪结点***路网处理很重要&#xff0c;看研究吧。 一直默认到最后。 导入要素类&#xff0c;单个 toupu2右键新建拓扑&#xff08;T&#xff09; 一般选不能有悬挂点&#xff0c;不能重叠。 一路默认 是 拉进图层可视化 线要素的话记得添加字段length&#…

Ghost Buster Pro for mac(快速清理卸载的应用残存文件)

Ghost Buster Pro for mac可从您已卸载的应用程序中查找并删除文件。该应用程序速度快如闪电&#xff0c;可立即释放内存。 许多应用程序都安装在计算机上&#xff0c;但它们通常只会在您的计算机上停留很短的时间。每个应用程序都会创建文件&#xff0c;但删除应用程序不会删…

编码器旋转圈数和单圈计数值计算功能块(SMART PLC梯形图)

有关 PLC的编码器更多应用请参看专栏其它文章,常用链接如下: 西门子SMART PLC高速脉冲计数采集编码器速度(RC滤波)_编码器频率采集计算速度采用什么滤波方法_RXXW_Dor的博客-CSDN博客这篇文章主要讲解西门子 SMART PLC高速计数采集编码器脉冲信号计算速度,根据编码器脉冲数…

【vs code插件推荐】快捷生成 console.log

使用这个插件可以直接双击选中你的变量名 然后使用快捷键 ctrl alt l 即可快速生成 时小记&#xff0c;终有成。

虚幻插件Landscaping Landscaping Mapbox

虚幻插件Landscaping & Landscaping Mapbox Landscaping offers an easy way to import GIS data as single Landscape or World Composition (UE4) or World Partition (UE5) or Procedural/Static Mesh. 提供了一种非常简单的方式来导入GIS数据&#xff0c;可以生成Lands…

Redis连接报错:ERR Client sent AUTH, but no password is set

如果在redis.windows.conf或者redis.conf&#xff08;我的是这个配置文件&#xff09; 文件夹中设置了密码&#xff0c;但是会报错 ERR Client sent AUTH, but no password is set 用记事本打开redis.windows.conf或者redis.conf &#xff08;我的是这个配置文件&#xff09;…