Spring Boot 是如何处理框架异常的

1,200 阅读8分钟

Spring Boot 框架在启动的时候,可能由于各种原因导致异常。Spring Boot 为了让异常能以比较友好的方式呈现出来,方便用户排查问题,设计了一套异常处理方式。该方案遵循 Spring 一贯的风格,具备单一职责,解耦,可扩展的特性,下面我们具体看看 Spring Boot 是如何处理框架异常的。

核心思路

如果让我们来设计一个异常体系的话,最容易想到的就是继承 RuntimeException,覆盖父类的构造方法,加入一些自定义的描述。这样在抛出异常的时候能够通过我们添加的描述来确定异常发生的原因。

如下是 Spring Boot 启动的时候发生端口占用会抛出的异常,PortInUseException。我们可以看出,该异常继承了 WebServerException,WebServerException 继承 RuntimeException。定义了一个成员变量 port,系统在抛出异常的时候必须传入当前的端口号,最终打印的 message 中也会带上端口相关的描述。

public class PortInUseException extends WebServerException {

	private final int port;

	/**
	 * Creates a new port in use exception for the given {@code port}.
	 * @param port the port that was in use
	 */
	public PortInUseException(int port) {
		this(port, null);
	}

	/**
	 * Creates a new port in use exception for the given {@code port}.
	 * @param port the port that was in use
	 * @param cause the cause of the exception
	 */
	public PortInUseException(int port, Throwable cause) {
		super("Port " + port + " is already in use", cause);
		this.port = port;
	}

	...

}

这种通过自定义异常类可以满足大部分的要求,实际开发业务代码的时候,能做到这一步就非常好了。但是 Spring Boot 作为一个框架,并不满足于此。它认为框架内的报错,应该有更加详细的错误分析,以及能指导用户的行为的描述。

一种方案是直接修改自定义异常类,但是这种行为本身就违背了开闭原则,不够灵活,还会将异常和 Spring Boot 绑定,不够灵活。

我们看看 Spring Boot 的设计方案。

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

首先看下错误分析器(FailureAnalyzer),Spring Boot 在捕获某个框架异常后,比如 PortInUseException,会使用错误分析器对其进行分析(并不是每个错误都会分析,后续会详细说明),得到一个错误分析报告。

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

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

  ...

}

接下来错误报告器(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 对异常分析报告打印出来

异常处理流程

了解 Spring Boot 异常处理的方式后,我们再具体看下这个过程是如何组织起来的。

首先我们要知道,Spring Boot 异常处理针对的是启动 run 方法中的异常,catch 代码块中的 handleRunFailure 是异常处理的起点。

public ConfigurableApplicationContext run(String... args) {
  ...
  try {
    ...
  }
  catch (Throwable ex) {
    handleRunFailure(context, ex, listeners);
    throw new IllegalStateException(ex);
  }
  ...
}

handleRunFailure 中做了这么几件事情。

  1. 判断程序退出的 code,0 为正常退成,大于 0 是异常退出。
  2. 如果存在监听器,发出 ApplicationFailedEvent 事件,表明应用启动失败。
  3. 获得 SpringBootExceptionReporter 集合。
  4. 遍历 SpringBootExceptionReporter 集合,调用每个 SpringBootExceptionReporter 的 reportException 方法。
  5. 清理 context

我们重点看下 3、4 步。这里出现一个新的接口,SpringBootExceptionReporter。它其实是封装了获得多个 FailureAnalyzer 并对异常进行分析的过程。

SpringBootExceptionReporter 本身是 Spring 使用 SPI 的方式加载的。

getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class<?>[] { ConfigurableApplicationContext.class }, context);

Spring Boot 在 spring.factories 中指定了该接口的实现是 FailureAnalyzers,注意不要和之前的 FailureAnalyzer 搞混。FailureAnalyzers 可以认为是管理 FailureAnalyzer 的

# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers

FailureAnalyzers 在构造函数中会加载所有的 FailureAnalyzer。同样使用 SPI 的方式进行加载。

FailureAnalyzers(ConfigurableApplicationContext context, ClassLoader classLoader) {
  this.classLoader = (classLoader != null) ? classLoader : getClassLoader(context);
  this.analyzers = loadFailureAnalyzers(context, this.classLoader);
}

我们可以在 spring.factories 看到 Spring Boot 中到底哪些异常拥有 FailureAnalyzer,也就是哪些异常会被 Spring Boot 重新包装处理。如下是所有的FailureAnalyzer,其中就有我们之前举例用的 PortInUseFailureAnalyzer,还有一些自动注入失败报的异常,NoUniqueBeanDefinitionFailureAnalyzer,BindFailureAnalyzer 等。

# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.context.config.ConfigDataNotFoundFailureAnalyzer,\
org.springframework.boot.context.properties.IncompatibleConfigurationFailureAnalyzer,\
org.springframework.boot.context.properties.NotConstructorBoundInjectionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanDefinitionOverrideFailureAnalyzer,\
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,\
org.springframework.boot.diagnostics.analyzer.PatternParseFailureAnalyzer,\
org.springframework.boot.liquibase.LiquibaseChangelogMissingFailureAnalyzer

接着就是最重要的 reportFailure 方法。Spring Boot 在初始化完毕所有的 SpringBootExceptionReporter 后,会依次调用每个实例的 reportException 方法来处理异常,如果该方法返回为 true,表示这个异常不需要后续的 SpringBootExceptionReporter 处理了,返回 false 则继续处理。

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

由于 Spring Boot 只有一个默认的 SpringBootExceptionReporter,那就是FailureAnalyzers,所有会执行 FailureAnalyzers 的 reportException 方法。

@Override
public boolean reportException(Throwable failure) {
  // 获得错误分析报告
  FailureAnalysis analysis = analyze(failure, this.analyzers);
  // 打印报告
  return report(analysis, this.classLoader);
}

我们知道,FailureAnalyzers 中使用 SPI 方式加载了所有的 FailureAnalyzer,它的 analyze 方法其实就是遍历 FailureAnalyzer 集合,依次调用 analyze 方法直到获得一个 FailureAnalysis 或遍历结束。再详细一些的话,就是每个 FailureAnalyzer 判断当前的异常是否是自己感兴趣的异常(通过类上的泛型申明),如果是的话,对该异常进行二次包装,加入描述(description)和行为(action)。

report 方法中,使用 SPI 的方式加载 FailureAnalysisReporter 的实现,调用其 report 方法。所以最终调用的是 LoggingFailureAnalysisReporter 的 report 方法。

# Failure Analysis Reporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter

整个流程的本质是获得 FailureAnalyzer 分析异常, 然后通过 FailureAnalysisReporter 将其分析的结果展示的过程。Spring 为了更好的扩展这个流程,基本每个环节都使用 SPI 的方式进行加载。

异常打印出来以后,流程还有最后一步,那就是调用 context.close() 方法,关闭当前上下文,在该方法中,会设置 context 当前状态为关闭,发出ContextClosedEvent 事件,清理 bean,bean 工厂,删除监听器,最后执行 jvm 的关闭钩子方法。

异常处理的扩展方式

Spring Boot 处理框架异常的过程中,使用了三次 SPI 加载方式,也就是有三个扩展点。

第一个扩展点是 SpringBootExceptionReporter,Spring Boot 默认的是实现是 FailureAnalyzers。实现这个接口,可以重新定义整个异常处理流程。

第二个扩展点是 FailureAnalyzer,可以对异常做二次包装,返回一个异常分析报告。

第三个扩展点是 FailureAnalysisReporter,可以自定义异常的展示方式,默认是使用日志组件进行展示。

Spring Boot 的这些扩展点基本是提供给框架内部进行扩展的,某些深入整合 Spring 的 jar 包也可能会对异常做一些扩展。但并不代表这种异常处理方式没有什么价值,相反,我们在复杂业务场景,或是工具类平台的时候,很有可能会遇到异常描述不友好,无法快速修复的场景,这时可以尝试参考 Spring Boot 的做法,向上抽象一层,可以获得较好的体验。

总结

  1. Spring Boot 为了将启动过程中的错误以更加友好,灵活的方式呈现出来,设计了一套异常处理方案。

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

  3. 错误分析器(FailureAnalyzer)的职责是识别当前错误的类型,对于感兴趣的错误进行二次包装,包装的结果就是错误分析报告(FailureAnalysis)。

  4. 错误分析报告(FailureAnalysis)中除了原始的错误信息外,新增了描述(description)以及行为(action)用于提示用户后续的处理方式。

  5. Spring Boot 框架异常处理体系中大量使用 SPI 的方式进行特定类的加载,方便框架后续对异常处理方案,特定异常检查,异常展示方式的扩展。

如果您觉得有所收获,就请点个赞吧!