返回

SpringBoot——异常报告器解析

发布时间:2023-04-07 06:14:12 354
# java# 容器# 数据# 服务器# 技术

异常报告器类介绍

接口规范

@FunctionalInterface
public interface SpringBootExceptionReporter {

	/**
	 * Report a startup failure to the user.
	 * @param failure the source failure
	 * @return {@code true} if the failure was reported or {@code false} if default
	 * reporting should occur.
	 */
	boolean reportException(Throwable failure);

}

SpringBootExceptionReporter初始化情况

public ConfigurableApplicationContext run(String... args) {
	......
	//初始化构造一个SpringBootExceptionReporter集合
	Collection exceptionReporters = new ArrayList<>();
	
	try {
		...
		//给exceptionReporters集合填充具体的对象
		exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
				new Class[] { ConfigurableApplicationContext.class }, context);
		...
	}
	catch (Throwable ex) {
		handleRunFailure(context, ex, exceptionReporters, listeners);
		throw new IllegalStateException(ex);
	}

	...
	return context;
}

通过SpringFactoriesLoader加载

private  Collection getSpringFactoriesInstances(Class type, Class[] parameterTypes, Object... args) {
	ClassLoader classLoader = getClassLoader();
	// Use names and ensure unique to protect against duplicates
	Set names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
	// 也是通过SpringFactoriesLoader技术来加载,见下图,默认就一个,这边实例化会调用其构造函数
	List instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
	AnnotationAwareOrderComparator.sort(instances);
	return instances;
}
# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers

SpringBoot通过SPI技术为SpringBootExceptionReporter接口指定了唯一的实现FailureAnalyzers

final class FailureAnalyzers implements SpringBootExceptionReporter {
	private final ClassLoader classLoader;

	private final List analyzers;

	FailureAnalyzers(ConfigurableApplicationContext context, ClassLoader classLoader) {
		Assert.notNull(context, "Context must not be null");
		//设置类加载器
		this.classLoader = (classLoader != null) ? classLoader : context.getClassLoader();
		//获取容器中FailureAnalyzer实例对象并排序
		this.analyzers = loadFailureAnalyzers(this.classLoader);
		prepareFailureAnalyzers(this.analyzers, context);
	}

	private List loadFailureAnalyzers(ClassLoader classLoader) {
		List analyzerNames = SpringFactoriesLoader.loadFactoryNames(FailureAnalyzer.class, classLoader);
		List 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("Failed to load " + analyzerName, ex);
			}
		}
		AnnotationAwareOrderComparator.sort(analyzers);
		return analyzers;
	}

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

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

再看List analyzers集合中的FailureAnalyzer

@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
	FailureAnalysis analyze(Throwable failure);

}

public class FailureAnalysis {
	//错误描述
	private final String description;
	//引起错误的动作
	private final String action;
	//错误本身
	private final Throwable cause;
}

下面我们看下FailureAnalyzers实现的SpringBootExceptionReporter的异常报告方法

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

private FailureAnalysis analyze(Throwable failure, List analyzers) {
	for (FailureAnalyzer analyzer : analyzers) {
		try {
			// 依次遍历每个analyzer分析,有结果就返回,这边走到AbstractFailureAnalyzer的analyze方法
			FailureAnalysis analysis = analyzer.analyze(failure);
			if (analysis != null) {
				return analysis;
			}
		}
		catch (Throwable ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("FailureAnalyzer " + analyzer + " failed", ex);
			}
		}
	}
	return null;
}

AbstractFailureAnalyzer实现了FailureAnalyzer接口

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

	protected abstract FailureAnalysis analyze(Throwable rootFailure, T cause);

	// 这个就是解析我的泛型参数,表明我对哪个异常感兴趣
	protected Class getCauseType() {
		return (Class) ResolvableType.forClass(AbstractFailureAnalyzer.class, getClass()).resolveGeneric();
	}

	protected final  E findCause(Throwable failure, Class type) {
		// 依次遍历异常堆栈,如果有对当前异常感兴趣,就返回,否则返回null
		while (failure != null) {
			if (type.isInstance(failure)) {
				return (E) failure;
			}
			failure = failure.getCause();
		}
		return null;
	}
}

再来看看report(analysis, this.classLoader)方法

// 分析出结果之后,调用report方法之后报告异常
private boolean report(FailureAnalysis analysis, ClassLoader classLoader) {
	// FailureAnalysisReporter这个实现类只有一个,是LoggingFailureAnalysisReporter
	List reporters = SpringFactoriesLoader.loadFactories(FailureAnalysisReporter.class,
			classLoader);
	if (analysis == null || reporters.isEmpty()) {
		return false;
	}
	for (FailureAnalysisReporter reporter : reporters) {
		// 输出异常报告
		reporter.report(analysis);
	}
	return true;
}
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();
	}

}

SpringBoot异常处理流程

public ConfigurableApplicationContext run(String... args) {
	......
	Collection exceptionReporters = new ArrayList<>();
	......
	try {
		......
		exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
				new Class[] { ConfigurableApplicationContext.class }, context);
		......
	}
	catch (Throwable ex) {
		//对启动过程中的失败进行处理
		handleRunFailure(context, ex, exceptionReporters, listeners);
		throw new IllegalStateException(ex);
	}

	......
	return context;
}

点进去handleRunFailure(context, ex, exceptionReporters, listeners)

private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception,
		Collection 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);
}

handleRunFailure(context, ex, exceptionReporters, listeners)逻辑

handleExitCode(context, exception)

private void handleExitCode(ConfigurableApplicationContext context, Throwable exception) {
	// 获取框架内对这个异常定义的exitCode,代表退出状态码,为0代表正常退出,不为0标识异常退出
	int exitCode = getExitCodeFromException(context, exception);
	if (exitCode != 0) {
		if (context != null) {
			// 发布一个ExitCodeEvent事件
			context.publishEvent(new ExitCodeEvent(context, exitCode));
		}
		SpringBootExceptionHandler handler = getSpringBootExceptionHandler();
		if (handler != null) {
			//记录exitCode
			handler.registerExitCode(exitCode);
		}
	}
}

listeners.failed(context, exception)

  • 发布ApplicationFailedEvent事件
public void failed(ConfigurableApplicationContext context, Throwable exception) {
	for (SpringApplicationRunListener listener : this.listeners) {
		callFailedListener(listener, context, exception);
	}
}

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

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
	@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);
		}
	}
}

reportFailure(exceptionReporters, exception)

  • SpringBootExceptionReporter实现类调用reportException方法
  • 成功处理的话,记录已处理异常
private void reportFailure(Collection 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);
	}
}

context.close()

  • 更改应用上下文状态
  • 销毁单例bean
  • 将beanFactory置为空
  • 关闭web容器(web环境)
  • 移除shutdownHook
public abstract class AbstractApplicationContext extends DefaultResourceLoader
		implements ConfigurableApplicationContext {	
	@Override
	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
				}
			}
		}
	}

	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 {
				// Publish shutdown event.
				// 发布容器关闭事件
				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);
		}
	}
}

ReflectionUtils.rethrowRuntimeException(exception)

  • 重新抛出异常
public static void rethrowRuntimeException(Throwable ex) {
	if (ex instanceof RuntimeException) {
		throw (RuntimeException) ex;
	}
	if (ex instanceof Error) {
		throw (Error) ex;
	}
	throw new UndeclaredThrowableException(ex);
}

shutdownHook介绍

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

背景

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

 

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

 

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

 

如果没有,则可以立即退出。

 

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

什么是Shutdown Hook

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

 

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

 

什么时候会调用Shutdown Hook

程序正常停止

  • Reach the end of program
  • System.exit

程序异常退出

  • NPE
  • OutOfMemory

受到外界影响停止

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

如何使用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")));

测试例子

  • 首先,注册了一个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终止了的情况。

参考: https://my.oschina.net/liwanghong/blog/3167733

https://www.cnblogs.com/maxstack/p/9112711.html

https://www.cnblogs.com/tracer-dhy/p/10041643.html

特别声明:以上内容(图片及文字)均为互联网收集或者用户上传发布,本站仅提供信息存储服务!如有侵权或有涉及法律问题请联系我们。
举报
评论区(0)
按点赞数排序
用户头像
精选文章
thumb 中国研究员首次曝光美国国安局顶级后门—“方程式组织”
thumb 俄乌线上战争,网络攻击弥漫着数字硝烟
thumb 从网络安全角度了解俄罗斯入侵乌克兰的相关事件时间线
下一篇
SpringBoot——属性配置解析 2023-04-07 02:52:34