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 了