SpringBoot錯誤處理流程原理

1、根據SpringBoot的慣例或者說方法論,我們研究錯誤處理的話應該先找到錯誤處理的自動配置,趕巧,SpringBoot中確實有一個叫ErrorMvcAutoConfiguration的自動配置類,我們能看到它向Spring容器中注入了一系列的對象,包括DefaultErrorAttributes、BasicErrorController、ErrorPageCustomizer以及通過內部類DefaultErrorViewResolverConfiguration注入的DefaultErrorViewResolver

部分源碼如下:

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {

	private final ServerProperties serverProperties;

	public ErrorMvcAutoConfiguration(ServerProperties serverProperties) {
		this.serverProperties = serverProperties;
	}

	@Bean
	@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
	public DefaultErrorAttributes errorAttributes() {
		return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
	}

	@Bean
	@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
	public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
			ObjectProvider<ErrorViewResolver> errorViewResolvers) {
		return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
				errorViewResolvers.orderedStream().collect(Collectors.toList()));
	}

	@Bean
	public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
		return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
	}

	@Bean
	public static PreserveErrorControllerTargetClassPostProcessor preserveErrorControllerTargetClassPostProcessor() {
		return new PreserveErrorControllerTargetClassPostProcessor();
	}

	@Configuration(proxyBeanMethods = false)
	static class DefaultErrorViewResolverConfiguration {

		private final ApplicationContext applicationContext;

		private final ResourceProperties resourceProperties;

		DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
				ResourceProperties resourceProperties) {
			this.applicationContext = applicationContext;
			this.resourceProperties = resourceProperties;
		}

		@Bean
		@ConditionalOnBean(DispatcherServlet.class)
		@ConditionalOnMissingBean(ErrorViewResolver.class)
		DefaultErrorViewResolver conventionErrorViewResolver() {
			return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
		}

	}

2、ErrorPageCustomizer,錯誤頁面定製器

/**
	 * {@link WebServerFactoryCustomizer} that configures the server's error pages.
	 */
	private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {

		private final ServerProperties properties;

		private final DispatcherServletPath dispatcherServletPath;

		protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
			this.properties = properties;
			this.dispatcherServletPath = dispatcherServletPath;
		}

		@Override
		public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
			ErrorPage errorPage = new ErrorPage(
					this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
			errorPageRegistry.addErrorPages(errorPage);
		}

		@Override
		public int getOrder() {
			return 0;
		}

	}

在源碼中有一個方法registerErrorPages()註冊錯誤頁面,傳入了一個ErrorPageRegistry對象,通過獲取ErrorProperties的path屬性的值(可通過server.error.path進行配置,默認是/error)實例化了一個ErrorPage對象,然後將一個ErrorPage添加到errorPageRegistry中;ErrorPageRegistry接口的實現類很多,SpringBoot默認是嵌入式Tomcat容器,因此此處是TomcatServletWebServerFactory,所以這裏將ErrorPage添加到了TomcatServletWebServerFactory中,也就是說SpringBoot默認出現錯誤時重定向到/error請求。

3、BasicErrorController,基礎錯誤處理器,SpringBoot幫我們定義了一個常見的Controller來處理錯誤

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

	private final ErrorProperties errorProperties;

	/**
	 * Create a new {@link BasicErrorController} instance.
	 * @param errorAttributes the error attributes
	 * @param errorProperties configuration properties
	 */
	public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
		this(errorAttributes, errorProperties, Collections.emptyList());
	}

	/**
	 * Create a new {@link BasicErrorController} instance.
	 * @param errorAttributes the error attributes
	 * @param errorProperties configuration properties
	 * @param errorViewResolvers error view resolvers
	 */
	public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties,
			List<ErrorViewResolver> errorViewResolvers) {
		super(errorAttributes, errorViewResolvers);
		Assert.notNull(errorProperties, "ErrorProperties must not be null");
		this.errorProperties = errorProperties;
	}

	@Override
	public String getErrorPath() {
		return this.errorProperties.getPath();
	}

	@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		Map<String, Object> model = Collections
				.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

	@RequestMapping
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
		HttpStatus status = getStatus(request);
		if (status == HttpStatus.NO_CONTENT) {
			return new ResponseEntity<>(status);
		}
		Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
		return new ResponseEntity<>(body, status);
	}

	/**
	 * Determine if the stacktrace attribute should be included.
	 * @param request the source request
	 * @param produces the media type produced (or {@code MediaType.ALL})
	 * @return if the stacktrace attribute should be included
	 */
	protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {
		IncludeStacktrace include = getErrorProperties().getIncludeStacktrace();
		if (include == IncludeStacktrace.ALWAYS) {
			return true;
		}
		if (include == IncludeStacktrace.ON_TRACE_PARAM) {
			return getTraceParameter(request);
		}
		return false;
	}

	/**
	 * Provide access to the error properties.
	 * @return the error properties
	 */
	protected ErrorProperties getErrorProperties() {
		return this.errorProperties;
	}

}

從源碼不難看出該Controller處理的就是上面提到的/error請求或者server.error.path配置的路徑,也就是說當請求出現錯誤時,便通過該Controller的相應方法進行處理,這樣的話事情就好辦了,我們把目光聚焦到該Controller的下面兩個方法上

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		Map<String, Object> model = Collections
				.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

	@RequestMapping
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
		HttpStatus status = getStatus(request);
		if (status == HttpStatus.NO_CONTENT) {
			return new ResponseEntity<>(status);
		}
		Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
		return new ResponseEntity<>(body, status);
	}

第一個方法errorHtml上標註了@RequestMapping(produces = MediaType.TEXT_HTML_VALUE),因此當請求類型是text/html,也即是通過瀏覽器訪問時進入該方法,其他客戶端訪問時當然就進入第二個方法了,所以大家會發現在瀏覽器和Postman訪問同一個錯誤路徑時返回的數據是不同了,瀏覽器返回html,而postman返回json數據。

那麼這兩個方法的數據從哪裏來的呢,我們看到他們都調用瞭如下方法

protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
		WebRequest webRequest = new ServletWebRequest(request);
		return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
}

該方法調用了當前對象中的errorAttributes屬性,這不就是最開頭提到的自動配置類中實例化的幾個類中的一個嗎?

4、DefaultErrorAttributes(ErrorAttributes接口的默認實現類)

@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
		Map<String, Object> errorAttributes = new LinkedHashMap<>();
		errorAttributes.put("timestamp", new Date());
		addStatus(errorAttributes, webRequest);
		addErrorDetails(errorAttributes, webRequest, includeStackTrace);
		addPath(errorAttributes, webRequest);
		return errorAttributes;
}

上面的方法就是在BasicErrorController中的兩個處理方法中調用的方法,錯誤頁面和json數據就從此而來。

5、ErrorViewResolver,錯誤頁面解決器

我們再次回到BasicErrorController的errorHtml方法中,其中有如下這行代碼

ModelAndView modelAndView = resolveErrorView(request, response, status, model);

接着是下面這個方法

protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
			Map<String, Object> model) {
		for (ErrorViewResolver resolver : this.errorViewResolvers) {
			ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
			if (modelAndView != null) {
				return modelAndView;
			}
		}
		return null;
}

我們看到該方法中出現了全局屬性errorViewResolvers,這裏從List遍歷出ErrorViewResolver來創建ModelAndView,只要不爲空便返回,由此可見只要我們向Spring容器中注入一個或多個ErrorViewResolver便可自定義錯誤頁面,並優先取出List中的第一個。

進入BasicErrorController父類AbstractErrorController中可看到如下代碼

public AbstractErrorController(ErrorAttributes errorAttributes, List<ErrorViewResolver> errorViewResolvers) {
		Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
		this.errorAttributes = errorAttributes;
		this.errorViewResolvers = sortErrorViewResolvers(errorViewResolvers);
}

private List<ErrorViewResolver> sortErrorViewResolvers(List<ErrorViewResolver> resolvers) {
		List<ErrorViewResolver> sorted = new ArrayList<>();
		if (resolvers != null) {
			sorted.addAll(resolvers);
			AnnotationAwareOrderComparator.sortIfNecessary(sorted);
		}
		return sorted;
}

從源碼中可以看到對errorViewResolvers這個List進行了排序,用的是AnnotationAwareOrderComparator比較器,深入該比較器不難看到,是通過@Order註解從小到大排序的,也即是Order標註的數字越小優先級越高。

好,到我們真正進入ErrorViewResolver的時候了,源碼如下,部分省略:

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

	private static final Map<Series, String> SERIES_VIEWS;

	static {
		Map<Series, String> views = new EnumMap<>(Series.class);
		views.put(Series.CLIENT_ERROR, "4xx");
		views.put(Series.SERVER_ERROR, "5xx");
		SERIES_VIEWS = Collections.unmodifiableMap(views);
	}

	private ApplicationContext applicationContext;

	private final ResourceProperties resourceProperties;

	private final TemplateAvailabilityProviders templateAvailabilityProviders;

	private int order = Ordered.LOWEST_PRECEDENCE;

	/**
	 * Create a new {@link DefaultErrorViewResolver} instance.
	 * @param applicationContext the source application context
	 * @param resourceProperties resource properties
	 */
	public DefaultErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties) {
		Assert.notNull(applicationContext, "ApplicationContext must not be null");
		Assert.notNull(resourceProperties, "ResourceProperties must not be null");
		this.applicationContext = applicationContext;
		this.resourceProperties = resourceProperties;
		this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext);
	}

	DefaultErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties,
			TemplateAvailabilityProviders templateAvailabilityProviders) {
		Assert.notNull(applicationContext, "ApplicationContext must not be null");
		Assert.notNull(resourceProperties, "ResourceProperties must not be null");
		this.applicationContext = applicationContext;
		this.resourceProperties = resourceProperties;
		this.templateAvailabilityProviders = templateAvailabilityProviders;
	}

	@Override
	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
         //用錯誤代碼作爲ModelAndView的name,錯誤頁面的名稱
		ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
        //如果頁面不存在,則判斷是否存在4xx,5xx命名的頁面
		if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
			modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
		}
		return modelAndView;
	}

	private ModelAndView resolve(String viewName, Map<String, Object> model) {
        //錯誤頁面需要放到error目錄中
		String errorViewName = "error/" + viewName;
        //判斷當前項目是否使用了模板引擎
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
				this.applicationContext);
		if (provider != null) {
			return new ModelAndView(errorViewName, model);
		}
          //如果當前項目沒有使用模板引擎,則調用該方法
		return resolveResource(errorViewName, model);
	}

	private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
		for (String location : this.resourceProperties.getStaticLocations()) {
			try {
				Resource resource = this.applicationContext.getResource(location);
                //錯誤頁面需.html結尾
				resource = resource.createRelative(viewName + ".html");
				if (resource.exists()) {
					return new ModelAndView(new HtmlResourceView(resource), model);
				}
			}
			catch (Exception ex) {
			}
		}
		return null;
	}

	@Override
	public int getOrder() {
		return this.order;
	}

	public void setOrder(int order) {
		this.order = order;
	}

}

從源碼的resolveErrorView方法可看出,默認是通過錯誤代碼(404,500等)作爲錯誤頁面的名稱的,如果找不到錯誤代碼命名的頁面,則查找4xx,5xx命名的頁面。

這裏還需要注意resolve方法,在該方法中先進行判斷當前項目是否使用了模板引擎,如果使用了則從templates目錄下的error目錄中查找對應的錯誤頁面;如果未使用,從resolveResource方法中可以看到需要從ResourceProperties的staticLocations屬性保存的路徑下的error目錄中去查找,他們默認是["classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"],如下源碼

@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {

	private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
			"classpath:/resources/", "classpath:/static/", "classpath:/public/" };

如果以上都找不到則使用SpringBoot提供的默認的StaticView,該類定義在ErrorMvcAutoConfiguration自動配置類中。

6、如何定製錯誤處理頁面

通過上面的分析我們已完全掌握了SpringBoot的錯誤處理流程,因此如何定製肯定不是難事了。

我們再次回到ErrorMvcAutoConfiguration,會發現上面幾個類的實例化方法除了basicErrorController()都有@ConditionalOnMissingBean(value = XXXX.class, search = SearchStrategy.CURRENT)註解,意思是當Spring容器中不存在上述這些類的對象時才通過該方法注入對象,因此咱們可以定義自己的類去繼承他們,並在配置類中實例化注入到Spring容器中即可能使用。

1)、如果項目使用了模板引擎,那麼只需要在templates目錄下新建error目錄,在其中建我們的錯誤頁面即可;如果未使用則一般在static目錄下新建error目錄保存錯誤頁面。

2)、重寫ErrorViewResolvers並注入到Spring容器中,改寫創建ModelAndView的方法從而達到目的

3)、重寫DefaultErrorAttributes來改寫創建Model數據的方法,這種辦法可以改寫json返回的數據

4)、最後當然是使用SpringBoot提供的@ControllerAdvice和@ExceptionHandler(XXXException.class)兩個註解來根據不同的異常定製錯誤處理方法。

發佈了52 篇原創文章 · 獲贊 28 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章