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

news2025/2/4 10:33:57

文章目录

  • 一、前言
  • 二、异常报告器介绍
    • 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);

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

  ...

}

总结下 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 can
		context.publishEvent(event);
	}
	else {
		// An inactive context may not have a multicaster so we use our multicaster to
		// call all of the context's listeners instead
		if (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.
		// 销毁所有的单例bean
		destroyBeans();

		// 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 hook
    Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("Do something in Shutdown Hook")));

    // sleep for some time
    try {
        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: 0
    at 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 Hook

Process 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 {


    @Autowired
    UserService 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;
    }
 
    @Override
    public 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> {
    @Override
    protected 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.coloradmin.cn/o/761897.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MYSQL 5.7.17 安装版 的配置文件

解压版解压后都有 my.ini配置文件&#xff0c;安装版要查找这个配置文件可以查看 MYSQL Workbench --> 左侧 INSTANCE --> Options File &#xff0c;然后可以看到底部 Configuration File所处的位置&#xff0c;即为my.ini的路径。

医疗设备如何保障?蓄电池自动监测,简直太牛了!

蓄电池监控在医院中扮演着重要的角色&#xff0c;确保在电力故障或断电时医院能够继续供电&#xff0c;保障医疗设备和关键系统的正常运行。 通过监测蓄电池的状态、充电状态和容量&#xff0c;以及触发警报和提醒&#xff0c;监控系统可以提前发现蓄电池的故障或异常情况&…

计算机网络 day8 动态路由 - NAT - SNAT实验 - VMware的网卡的3种模式

目录 动态路由&#xff1a;IGP 和 EGP 参考网课&#xff1a;4.6.1 路由选择协议概述_哔哩哔哩_bilibili ​编辑 IGP&#xff08;Interior Gateway Protocol&#xff09;内部网关协议&#xff1a; EGP&#xff08;Interior Gateway Protocol&#xff09;外部网关协议&#x…

专精特新如何养成?先搞清楚成长路径和核心能力激活高质量发展!

头雁勤&#xff0c;群雁便能“春风一夜到衡阳”。群雁齐飞&#xff0c;最重要的是头雁引领。 当前加快中小企业数字化转型正当其时&#xff0c;“专精特新”企业势必将肩负起“领头雁”之任&#xff0c;为中小企业转型发展做出表率。 装备制造业 专精特新“主力军” 纵观目前…

SpringBoot Data JPA 集成多租户

背景&#xff1a; ​ iot-kit项目用的是SpringBoot JPA&#xff0c;不是Mybatis&#xff0c;项目中需要引入多租户。 文章中心思想&#xff1a; 通过Hibernate Filters 和AspectJ 切面编程&#xff0c;实现SpringBoot JPA多租户 什么是多租户 ​ 多租户我理解就是一个网站允…

【EXCEL】通过url获取网页表格数据

目录 0.环境 1.背景 2.具体操作 0.环境 windows excel2021 1.背景 之前我用python的flask框架的爬虫爬取过豆瓣网的电影信息&#xff0c;没想到excel可以直接通过url去获取网页表格内的信息&#xff0c;比如下图这是电影信息界面 即将上映电影 (douban.com) 通过excel操作&…

Cache——让CPU更快地执行你的代码

概要 Cache对性能的影响 首先我们要知道&#xff0c;CPU访问内存时&#xff0c;不是直接去访问内存的&#xff0c;而是先访问缓存&#xff08;cache&#xff09;。 当缓存中已经有了我们要的数据时&#xff0c;CPU就会直接从缓存中读数据&#xff0c;而不是从内存中读。 CPU…

Python基础编程案例之编写交互式博客系统

文章目录 1、博客系统的需求描述2、面向用户层面各功能的设计思路与代码编写2.1.定义文章库2.2.文章的发布2.3.删除文章2.4.修改文章的标题以及内容2.5.在评论区添加评论2.6.删除文章中的某条评论2.7.阅读文章2.8.对文章进行点赞2.9.对文章进行收藏2.10.对文章进行打赏2.11.查询…

WorkPlus AI助理:结合ChatGPT对话能力与企业数据,助力企业级AI构建!

WorkPlus AI助理是基于GPT和私有数据构建智能知识库和个性化AI&#xff0c;能够帮助企业生成博客、白皮书、社交媒体帖子、新闻稿等等&#xff0c;这些内容可以用于推广产品、服务&#xff0c;增强品牌形象和知名度。此外&#xff0c;利用WorkPlus AI助理还可以生成电子邮件、利…

基于linux串口实现语音刷抖音

目录 1.开发逻辑图及模块 2.编程实现语音和开发板通信 3.手机接入Linux热拔插相关,打开手机开发者模式允许USB调试 4.用shell指令来操作手机屏幕&#xff0c;模拟手动滑屏幕 5.最终主程序代码 1.开发逻辑图及模块 逻辑图&#xff1a; 模块 &#xff08;1&#xff09;语音…

读kafka生产端源码,窥kafka设计之道(上)

1. kafka 高吞吐之道-------异步提交批量发送 简约的发送接口----后面隐藏着并不简单的设计 kafka发送消息的接口非常简约&#xff0c;在简约的表面上&#xff0c;其背后却并不简单。先看下发送接口 kafkaProducer.send(new ProducerRecord(topic,msg), new Callback() {Ove…

8、链路层以太网协议,ARP协议32

网络层IP协议描述了通信中的起点到终点&#xff0c;但是数据不是飞过去的&#xff0c;是经过了大量的中间节点转发完成的。 一、以太网协议 1、MAC地址 物理硬件地址&#xff0c;是每一块网卡在出厂时设定的地址&#xff0c;固定且不可修改&#xff08;早期&#xff0c;现在可…

当DevOps遇到AI,黑马迎来3.0时代丨IDCF

随着GhatGPT的爆火&#xff0c;人工智能和研发效能&#xff0c;无疑成为了2023的两个最重要的关键词。大规模语言模型LLM和相关应用的快速发展正在对研发团队的工作方式产生深远影响&#xff0c;这几乎象征着新的生产力革命的到来。 那么&#xff0c;作为一名工程师&#xff0…

Chat GPT是什么,初学者怎么使用Chat GPT,需要注意些什么

目录 Chat GPT是什么 初学者怎么使用Chat GPT 使用Chat GPT需要注意什么 一些简单的prompt示例 Chat GPT是什么 Chat GPT是由OpenAI开发的一种大型语言模型&#xff0c;它基于GPT&#xff08;Generative Pre-trained Transformer&#xff09;架构。GPT是一种基于深度学习的…

【Matlab】智能优化算法_遗传算法GA

【Matlab】智能优化算法_遗传算法GA 1.背景介绍2.数学模型3.文件结构4.详细代码及注释4.1 crossover.m4.2 elitism.m4.3 GeneticAlgorithm.m4.4 initialization.m4.5 Main.m4.6 mutation.m4.7 selection.m4.8 Sphere.m 5.运行结果6.参考文献 1.背景介绍 遗传算法&#xff08;Ge…

(学习笔记)TCP 为什么是三次握手?不是两次、四次?

常规回答&#xff1a;“因为三次握手才能保证双方具有接收和发送的能力” 原因一&#xff1a;避免历史连接 三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。 假设&#xff1a;客户端先发送了SYN(seq90)报文&#xff0c;然后客户端宕机了&#xff0c;而且这个SYN报…

一种电动汽车智能充电及收费云平台管理方案

摘要&#xff1a;对于电动汽车来说&#xff0c;主要是借助电力作为能源&#xff0c;有着多方面的优点。但是也存在着一定的问题&#xff0c;尤其在续航能力上相对较差。因此&#xff0c;在实际工作中要正确利用现代科学技术&#xff0c;让电动汽车实现智能充电。在研究中所涉及…

JavaScript中的let、const和var

在 JavaScript 中&#xff0c;let、const 和 var 是用于声明变量的关键字&#xff0c;在使用时有以下区别&#xff1a; 作用域&#xff1a;let 和 const 声明的变量具有块级作用域&#xff0c;只能在声明它的块中访问。而 var 声明的变量则是函数作用域或全局作用域&#xff0…

MS31001低压 5V DC 电机驱动

MS31001 是一款低压 5V 直流电机驱动芯片&#xff0c;为摄像机、 消费类产品、玩具和其他低压或者电池供电的运动控制类应用 提供了集成的电机驱动解决方案。 MS31001 能提供高达 0.8A 的输出电流。可以工作在 2.0~5.5V 的电源电压上。 MS31001 具有 PWM &#xff08…

NSSCTF随机一题

[GXYCTF 2019]Ping Ping Ping 应该是命令注入的题目&#xff0c;直接先ping一下本地&#xff0c;发现url栏有ip传参变化 接着就是利用命令注入符&#xff0c;尝试注入 它好像真的在ping&#xff0c;执行得特别慢&#xff0c;利用ls&#xff0c;查询到了flag文件 发现空格过…