1. 程式人生 > 實用技巧 >Springboot 預設的異常處理

Springboot 預設的異常處理

springboot 對異常處理預設的自動配置都封裝在ErrorMvcAutoConfiguration 這個類中,在專案啟動的過程中,會往容器中注入一些預設的元件、如果容器中已經存在了這些元件,那麼就不會再注入這些預設的元件到 IOC 容器中.

專案啟動的時候,會預設往 IOC 容器中注入下面這些元件(我們這裡挑比較重要的來說)

@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(ResourceProperties.class)
public class ErrorMvcAutoConfiguration {
	
	private final ServerProperties serverProperties;
	
	private final List<ErrorViewResolver> errorViewResolvers;

	public ErrorMvcAutoConfiguration(ServerProperties serverProperties,
			ObjectProvider<List<ErrorViewResolver>> errorViewResolversProvider) {
			
		this.serverProperties = serverProperties;
		this.errorViewResolvers = errorViewResolversProvider.getIfAvailable();
	}
	
	@Configuration
	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
		public DefaultErrorViewResolver conventionErrorViewResolver() {
			return new DefaultErrorViewResolver(this.applicationContext,this.resourceProperties);
		}
	}
	
	@Bean
	public ErrorPageCustomizer errorPageCustomizer() {
		return new ErrorPageCustomizer(this.serverProperties);
	}
	
	@Bean
	@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
	public DefaultErrorAttributes errorAttributes() {
		return new DefaultErrorAttributes();
	}

	@Bean
	@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
	public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
		return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
				this.errorViewResolvers);
	}
	
	@Configuration
	@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
	@Conditional(ErrorTemplateMissingCondition.class)
	protected static class WhitelabelErrorViewConfiguration {

		private final SpelView defaultErrorView = new SpelView(
				"<html><body><h1>Whitelabel Error Page</h1>"
						+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
						+ "<div id='created'>${timestamp}</div>"
						+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
						+ "<div>${message}</div></body></html>");

		@Bean(name = "error")
		@ConditionalOnMissingBean(name = "error")
		public View defaultErrorView() {
			return this.defaultErrorView;
		}

		// If the user adds @EnableWebMvc then the bean name view resolver from
		// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
		@Bean
		@ConditionalOnMissingBean(BeanNameViewResolver.class)
		public BeanNameViewResolver beanNameViewResolver() {
			BeanNameViewResolver resolver = new BeanNameViewResolver();
			resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
			return resolver;
		}

	.......
}	

首先是 ErrorMvcAutoConfiguration構造方法,這裡面有兩個賦值的動作,分別將serverProperties、errorViewResolversProvider.getIfAvailable() 賦值給了ErrorMvcAutoConfiguration 的成員變數,我們分別看一下這兩個值到底是什麼?

public ErrorMvcAutoConfiguration(ServerProperties serverProperties,
		ObjectProvider<List<ErrorViewResolver>> errorViewResolversProvider) {
		
	this.serverProperties = serverProperties;
	this.errorViewResolvers = errorViewResolversProvider.getIfAvailable();
}

serverProperties 這個變數的型別是ServerProperties,點開ServerProperties 這個類

// 將 spring 的配置檔案 application.properties 中 server 開頭的配置和 ServerProperties 類的屬性繫結起來
// ignoreUnknownFields=true 的意思是,如果 ServerProperties 中有屬性不能匹配到配置檔案中的值時,不會丟擲異常
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties implements EmbeddedServletContainerCustomizer, EnvironmentAware, Ordered {

	private Integer port;

	private InetAddress address;

	private String contextPath;
	
	...
}

我們可以發現,原來serverProperties 裡面封裝的是我們在 application.properties 中配置的以 server 為字首的標籤,當然如果不配置,它們會有預設值

接著看一下errorViewResolversProvider.getIfAvailable() ,它的型別是List<ErrorViewResolver> ,點開ErrorViewResolver ,我們可以發現它是一個介面,並且只有一個實現類(DefaultErrorViewResolver),我們可以看一下是如何獲取到ErrorViewResolver 的

在ErrorMvcAutoConfiguration 這個類中有一個靜態內部類

@Configuration
static class DefaultErrorViewResolverConfiguration {
	// 容器物件
	private final ApplicationContext applicationContext;
	// 靜態資源路徑物件
	private final ResourceProperties resourceProperties;

	DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,ResourceProperties resourceProperties) {
		// 容器物件
		this.applicationContext = applicationContext;
		// 獲取靜態資源路徑
		this.resourceProperties = resourceProperties;
	}
	
	// 將 DefaultErrorViewResolver 注入到容器中 id 為 conventionErrorViewResolver
	@Bean
	@ConditionalOnBean(DispatcherServlet.class)
	// 如果容器中不存在 DefaultErrorViewResolver,那麼我們就往容器中注入該 bean
	@ConditionalOnMissingBean
	public DefaultErrorViewResolver conventionErrorViewResolver() {
		// 返回一個 DefaultErrorViewResolver
		return new DefaultErrorViewResolver(this.applicationContext,this.resourceProperties);
	}
}

我們看一下這個靜態內部類的構造方法,這裡面有一個resourceProperties ,它就是 springboot 預設的靜態資源資訊,可以看出 springboot 預設的靜態資原始檔夾資訊也封裝在裡面

接著看一下怎麼返回DefaultErrorViewResolver 物件的

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
	// 預設的錯誤檢視,如果是客戶端錯誤對應 4xx ,如果是伺服器端的錯誤對應 5xx
	private static final Map<Series, String> SERIES_VIEWS;
	// 靜態程式碼塊,隨著類的載入而載入
	static {
		Map<Series, String> views = new HashMap<Series, String>();
		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;
	// 預設的錯誤檢視解析器優先順序最低,如果我們自己定義了 ErrorViewResolver ,如果要生效,需要設定優先順序高於 DefaultErrorViewResolver
	// 數值越小,優先順序越高,負數的優先順序高於正數
	private int order = Ordered.LOWEST_PRECEDENCE;	
	
	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;
		// 獲取模板物件,並將它賦值給 DefaultErrorViewResolver 類的成員變數
		this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext);
	}
		
	...
}

返回了DefaultErrorViewResolver 物件之後,就將該物件注入到了 IOC 容器中

接著來到錯誤頁面定製器中

@Bean
public ErrorPageCustomizer errorPageCustomizer() {
	// 呼叫 ErrorPageCustomizer 的構造方法,引數是 ErrorMvcAutoConfiguration 的成員變數 serverProperties
	return new ErrorPageCustomizer(this.serverProperties);
}

// ErrorMvcAutoConfiguration 的靜態內部類
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
	// 封裝的是 application.properties 配置檔案中帶 server 字首的屬性
	private final ServerProperties properties;

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

	@Override
	public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
		// 返回一個錯誤頁面
		ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()+ this.properties.getError().getPath());
		// 註冊錯誤頁面
		errorPageRegistry.addErrorPages(errorPage);
	}
	// 優先順序
	@Override
	public int getOrder() {
		return 0;
	}
}

我們看一下如何返回錯誤頁面的

ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()+ this.properties.getError().getPath());

這裡有一個引數,是由兩個表示式通過字串拼接而成的,這個兩個表示式是什麼意思,作用是什麼

this.properties.getServletPrefix():對應的是 application.properties 配置檔案中的 server.servlet-path 配置項 (如果 application.properties 中沒有配置,則預設值為 /)

public String getServletPrefix() {
	// 獲取 springboot 配置檔案 application.properties 中 server.servlet-path 配置項的值
	// 如果沒有配置該配置項,預設值是 ""
	String result = this.servletPath;
	if (result.contains("*")) {
		result = result.substring(0, result.indexOf("*"));
	}
	// 如果配置的是 server.servlet-path=/ 
	if (result.endsWith("/")) {
		// 擷取掉 / ,最終保留的是 "" ,那麼這樣的話配置為 / 和不配置是同樣的效果
		result = result.substring(0, result.length() - 1);
	}
	return result;
}

通過上面的程式碼可以得出

如果配置了 server.servlet-path = /xiaomao ,那麼訪問路徑就是 http://ip:port/xiaomao/

如果不配置或 server.servlet-path = / ,那麼訪問路徑就是http://ip:port/

接著看一下另外一個表示式this.properties.getError().getPath()

public class ErrorProperties {
	// 如果 application.properties 配置了 error.path ,那麼 path 就使用 error.path 的值
	// 如果沒有配置 error.path ,那麼 path 就使用 /error 
	@Value("${error.path:/error}")
	private String path = "/error";

	private IncludeStacktrace includeStacktrace = IncludeStacktrace.NEVER;

	public String getPath() {
		return this.path;
	}
	...
}

由於我們沒有在 application.properties 中配置 server.servert-path 和 error.path 的值,所以它們都是使用預設值,一個是空字串,一個是 /error ,最終拼接的就是 "/error"

呼叫 ErrorPage 的構造方法生產 ErrorPage 物件,這裡的 path 就是 /error

public ErrorPage(String path) {
	// 設定狀態碼
	this.status = null;
	// 設定異常資訊
	this.exception = null;
	// 設定錯誤頁面路徑
	this.path = path;
}

然後就是儲存錯誤頁面到AbstractConfigurableEmbeddedServletContainer 類中的一個 屬性中

// 所有的錯誤頁面都會儲存到這個集合中
private Set<ErrorPage> errorPages = new LinkedHashSet<ErrorPage>();

接著註冊預設的檢視defaultErrorView ,預設的檢視名稱是 error

@Configuration
// application.properties 配置檔案中不管是配置了、還是沒有配置 server.error.whitelabel.enabled 節點,
// 判斷條件都成立
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
// ErrorTemplateMissingCondition 的 matches(...) 方法,返回值為 true , 則判斷條件成立
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
	// 預設的錯誤檢視頁面,也就是我們在瀏覽器看到的 Whitelabel 頁面
	private final SpelView defaultErrorView = new SpelView(
			"<html><body><h1>Whitelabel Error Page</h1>"
					+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
					+ "<div id='created'>${timestamp}</div>"
					+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
					+ "<div>${message}</div></body></html>");
					
	// 往 IOC 容器中注入一個 beanName 為 error 的 bean 
	@Bean(name = "error")
	// 如果 IOC 容器中沒有 error 這個 bean ,那麼判斷條件成立
	@ConditionalOnMissingBean(name = "error")
	public View defaultErrorView() {
		return this.defaultErrorView;
	}

	// If the user adds @EnableWebMvc then the bean name view resolver from
	// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
	@Bean
	@ConditionalOnMissingBean(BeanNameViewResolver.class)
	public BeanNameViewResolver beanNameViewResolver() {
		BeanNameViewResolver resolver = new BeanNameViewResolver();
		resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
		return resolver;
	}
}

再接著是註冊DefaultErrorAttributes ,它裡面主要是包括 時間戳、狀態碼、錯誤資訊等內容

註冊完了之後就是往容器中 注入 BasicErrorController 了

@Bean
// 如果容器中不存在 ErrorController ,判斷條件成立
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
	return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
			this.errorViewResolvers);
}

1、如果瀏覽器或者其它客戶端訪問資源的時候出現了錯誤,就會來到 /error 請求,

千萬要注意,BasicErrorController 只會處理 /error 請求,如果你註冊的錯誤頁面不是 /error ,那麼它就會顯示找不到

例如我這裡通過配置server.servlet-path=/xiaomao 將我註冊的錯誤頁面改為 /xiaomao/error ,當我瀏覽器傳送請求 http://localhost:8080/error 時,由於我的專案下沒有對應的資源,那麼就會出現 404 異常,又由於我註冊的錯誤頁面時 /xiaomao/error ,所以出現了異常之後就會來到 /xiaomaomao/error 請求,但是 BasicErrorController 只會攔截 /error 請求,所以這裡會顯示找不到 localhost 網頁

// 瀏覽器發起請求時,如果出現錯誤時,對應的 Controller 處理邏輯
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,HttpServletResponse response) {
	// 獲取狀態碼
	HttpStatus status = getStatus(request);
	// 獲取模型資料 model
	Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
			request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
	// 響應資料中設定狀態碼
	response.setStatus(status.value());
	// 解析檢視得到 ModelAndView 物件
	ModelAndView modelAndView = resolveErrorView(request, response, status, model);
	// 如果 ModelAndView 不為空,直接返回 ModelAndView 物件,
	// 如果為空,那麼檢視的名稱為 error
	return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}

我們先看一下是如何獲取 model (模型資料)的

我們可以看出 Collections.unmodifiableMap(...) 方法的引數是通過 getErrorAttributes(...) 方法來獲取的

下面我們就仔細看一下 getErrorAttributes(...) 方法到底做了什麼

protected Map<String, Object> getErrorAttributes(HttpServletRequest request,boolean includeStackTrace) {
	// 建立一個 ServletRequestAttributes 物件,該物件主要是對 request、response、session 等進行了封裝
	RequestAttributes requestAttributes = new ServletRequestAttributes(request);
	// includeStackTrace 的值為 false
	return this.errorAttributes.getErrorAttributes(requestAttributes,includeStackTrace);
}

  繼續點開 this.errorAttributes.getErrorAttributes(requestAttributes,includeStackTrace) ,這裡的 this.errorAttributes 代表的是 ErrorAttributes 物件,點開ErrorAttributes ,發現它是個介面

public interface ErrorAttributes {
	Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
			boolean includeStackTrace);
	...
}

  所以this.errorAttributes.getErrorAttributes(requestAttributes,includeStackTrace) 呼叫的方法實際上是它的實現類的getErrorAttributes(...) ,而該介面只有一個實現類DefaultErrorAttributes ,點進去可以看到如下的內容

// DefaultErrorAttributes 類中的方法
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
			boolean includeStackTrace) {
	// 新建一個 Map 集合 errorAttributes , 用來存放 model 資料
	Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
	// 存放時間戳
	errorAttributes.put("timestamp", new Date());
	// 存放狀態碼,這裡會詳細的說一下
	// 它會去預設去 Request 域中尋找 javax.servlet.error.status_code 屬性對應的值
	// 如果找不到就去 Session 域中尋找, Request 域和 Session 域中如果都找不到,返回 null
	// 如果返回值為 null ,則往 errorAttributes 這個 Map 集合中新增 status =999 ,error = None
	// 如果返回值不為 null ,則往 errorAttributes 中新增 status 
	// 以及在 HttpStatus 中定義好的 status 對應的 error reason
	addStatus(errorAttributes, requestAttributes);
	// 存放錯誤的詳細資訊 message
	addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
	// 存放訪問路徑 localhost:8080/abcde  ====> /abcde
	addPath(errorAttributes, requestAttributes);
	return errorAttributes;
}

  接著就是resolveErrorView(...) 這個方法了

protected ModelAndView resolveErrorView(HttpServletRequest request,
			HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
	// 遍歷迴圈所有的 ErrorViewResolver
	for (ErrorViewResolver resolver : this.errorViewResolvers) {
		// 解析錯誤檢視
		ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
		if (modelAndView != null) {
			return modelAndView;
		}
	}
	// 如果 ModelAndView 物件為空,返回 null
	return null;
}

  解析錯誤檢視

public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
			Map<String, Object> model) {
	// 這裡的引數是將狀態碼轉成了 String 型別的字串,另外一個引數是 model
	// 1、存在 thymeleaf 模板引擎的情況下,就優先去去找 templates/error/status的值.html 這個檢視
	// 2、不存在 thymeleaf 模板引擎的情況下就去所有的靜態資原始檔下尋找 /error/status的值.html 檢視
	// 如果上面兩種情況都找不到,那麼 modelAndView 的值為 null
	ModelAndView modelAndView = resolve(String.valueOf(status), model);
	// modelAndView 的值為 null ,並且如果是 CLIENT_ERROR 或者是 SERVER_ERROR
	if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
		// 假設這裡是客戶端錯誤,那麼就按照 4xx 重複上面的步驟重新解析一次
		// 如果是服務端錯誤,那麼按照 5xx 重複上面的步驟重新解析一次
		modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
	}
	return modelAndView;
}

  解析錯誤檢視,返回 ModelAndView 物件

private ModelAndView resolve(String viewName, Map<String, Object> model) {
	// error/ 拼接 狀態碼的字串就是錯誤的檢視名稱
	String errorViewName = "error/" + viewName;
	// 根據 errorViewName 獲取可用的模板引擎 
	// 例如 thymeleaf 引擎,那麼判斷當前專案的 templates 下有沒有 /error/viewName.html 這個檢視
	// 如果有 provider 就不為空,如果沒有則為 null
	TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
			.getProvider(errorViewName, this.applicationContext);
	// 如果有可用的模板引擎
	if (provider != null) {
		return new ModelAndView(errorViewName, model);
	}
	// 如果沒有模板引擎,那麼去靜態資原始檔夾下面找 error/status的值.html
	return resolveResource(errorViewName, model);
}

  如果有 thymeleaf 模板引擎的情況下

// 如果存在模板引擎的情況下
public ModelAndView(String viewName, Map<String, ?> model) {
	// ModelAndView.setView("viewName")
	this.view = viewName;
	// 將 model 中的資料作為 ModelAndView 物件的屬性
	if (model != null) {
		getModelMap().addAllAttributes(model);
	}
}

  // 如果沒有模板引擎的情況下

// 如果不存在模板引擎的情況下
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
	// 挨個遍歷所有的靜態資料夾 
	// /META-INF/resources/、classpath:/resources/、classpath:/static/、classpath:/public/、/ 
	for (String location : this.resourceProperties.getStaticLocations()) {
		try {
			Resource resource = this.applicationContext.getResource(location);
			// 檢視上面所有的 5 個靜態資原始檔夾中是否存在 /error/status的值.html 這個檢視
			resource = resource.createRelative(viewName + ".html");
			if (resource.exists()) {
				return new ModelAndView(new HtmlResourceView(resource), model);
			}
		}
		catch (Exception ex) {
		}
	}
	// 所有的靜態資原始檔夾下都不存在 /error/status的值.html 則返回 null
	return null;
}

  假設狀態碼為 404、如果找不到templates/error/404.html、靜態資料夾下/error/404.html、templates/error/4xx.html、靜態資料夾下/error/4xx.html 這幾種情況,那麼 ModelAndView 物件就為 null,這種情況下就會建立一個新的檢視,檢視名稱為 error ,那麼這個 error 檢視是什麼呢?

@Configuration
// 如果 application.properties 中沒有配置 server.error.whitelabel.enabled 標籤,那麼判斷條件成立
// 這裡要注意一下,如果配置了 server.error.whitelabel.enabled=false,那麼判斷條件是不成立的
// 要搞清楚 matchIfMissing=true 到底是什麼意思(沒有配置標籤,判斷條件才成立)
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {

	private final SpelView defaultErrorView = new SpelView(
			"<html><body><h1>Whitelabel Error Page</h1>"
					+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
					+ "<div id='created'>${timestamp}</div>"
					+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
					+ "<div>${message}</div></body></html>");
	// 註冊一個名稱為 error 的檢視物件
	@Bean(name = "error")
	// 如果容器中不存在一個名稱為 error 的檢視物件,條件成立
	@ConditionalOnMissingBean(name = "error")
	public View defaultErrorView() {
		return this.defaultErrorView;
	}
...
}	

  看到沒 @Bean 註解註冊一個 View 物件,它的名稱就是 error,看一下這個檢視不就是我們的Whitelabel 嗎

總結一下上面的原始碼,這裡假設狀態碼為 404 (客戶端錯誤)

1、專案中引入了 thymeleaf 模板引擎,那麼就去尋找 templates/error/404.html 檢視,找不到執行步驟 2

2、去靜態資原始檔(5個靜態資原始檔夾)下找 /error/404.html 檢視,找不到執行步驟 3

3、去找 templates/error/4xx.html 檢視(如果是服務端錯誤,就找 /error/5xx.html),找不到執行步驟 4

4、去找找靜態資原始檔下的 /error/4xx.html 檢視(如果是服務端錯誤,就是 /error/5xx.html)

5、如果上面的情況都不符合,那麼就是找預設的檢視 Whitelabel 了