你以為Spring Boot統一異常處理能攔截所有的異常?
通常我們在Spring Boot中設置的統一異常處理只能處理Controller拋出的異常。有些請求還沒到Controller就出異常了,而這些異常不能被統一異常捕獲,例如Servlet容器的某些異常。今天我在項目開發中就遇到了一個,這讓我很不爽,因為它返回的錯誤信息格式不能統一處理,我決定找個方案解決這個問題。
ErrorPageFilter
Whitelabel Error Page
這類圖相信大家沒少見,Spring Boot 只要出錯,體現在頁面上的就是這個。如果你用Postman之類的測試出了異常則是:
- {
- "timestamp": "2021-04-29T22:45:33.231+0000",
- "status": 500,
- "message": "Internal Server Error",
- "path": "foo/bar"
- }
這個是怎么實現的呢?Spring Boot在啟動時會注冊一個ErrorPageFilter,當Servlet發生異常時,該過濾器就會攔截處理,將異常根據不同的策略進行處理:當異常已經在處理的話直接處理,否則轉發給對應的錯誤頁面。有興趣的可以去看下源碼,邏輯不復雜,這里就不貼了。
另外當一個 Servlet 拋出一個異常時,處理異常的Servlet可以從HttpServletRequest里面得到幾個屬性,如下:
異常屬性
我們可以從上面的幾個屬性中獲取異常的詳細信息。
默認錯誤頁面
通常Spring Boot出現異常默認會跳轉到/error進行處理,而/error的相關邏輯則是由BasicErrorController實現的。
- @Controller
- @RequestMapping("${server.error.path:${error.path:/error}}")
- public class BasicErrorController extends AbstractErrorController {
- //返回錯誤頁面
- @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, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
- response.setStatus(status.value());
- ModelAndView modelAndView = resolveErrorView(request, response, status, model);
- return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
- }
- // 返回json
- @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, getErrorAttributeOptions(request, MediaType.ALL));
- return new ResponseEntity<>(body, status);
- }
- // 其它省略
- }
而對應的配置:
- @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()));
- }
所以我們只需要重新實現一個ErrorController并注入Spring IoC就可以替代默認的處理機制。而且我們可以很清晰的發現這個BasicErrorController不但是ErrorController的實現而且是一個控制器,如果我們讓控制器的方法拋異常,肯定可以被自定義的統一異常處理。所以我對BasicErrorController進行了改造:
- @Controller
- @RequestMapping("${server.error.path:${error.path:/error}}")
- public class ExceptionController extends AbstractErrorController {
- public ExceptionController(ErrorAttributes errorAttributes) {
- super(errorAttributes);
- }
- @Override
- @Deprecated
- public String getErrorPath() {
- return null;
- }
- @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
- public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
- throw new RuntimeException(getErrorMessage(request));
- }
- @RequestMapping
- public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
- throw new RuntimeException(getErrorMessage(request));
- }
- private String getErrorMessage(HttpServletRequest request) {
- Object code = request.getAttribute("javax.servlet.error.status_code");
- Object exceptionType = request.getAttribute("javax.servlet.error.exception_type");
- Object message = request.getAttribute("javax.servlet.error.message");
- Object path = request.getAttribute("javax.servlet.error.request_uri");
- Object exception = request.getAttribute("javax.servlet.error.exception");
- return String.format("code: %s,exceptionType: %s,message: %s,path: %s,exception: %s",
- code, exceptionType, message, path, exception);
- }
- }
直接拋異常,簡單省力!凡是這里捕捉的到的異常大部分還沒有經過Controller,我們通過ExceptionController中繼也讓這些異常被統一處理,保證整個應用的異常處理對外保持一個統一的門面。