你以為Spring Boot統(tǒng)一異常處理能攔截所有的異常?
通常我們在Spring Boot中設(shè)置的統(tǒng)一異常處理只能處理Controller拋出的異常。有些請求還沒到Controller就出異常了,而這些異常不能被統(tǒng)一異常捕獲,例如Servlet容器的某些異常。今天我在項(xiàng)目開發(fā)中就遇到了一個(gè),這讓我很不爽,因?yàn)樗祷氐腻e(cuò)誤信息格式不能統(tǒng)一處理,我決定找個(gè)方案解決這個(gè)問題。
ErrorPageFilter
Whitelabel Error Page
這類圖相信大家沒少見,Spring Boot 只要出錯(cuò),體現(xiàn)在頁面上的就是這個(gè)。如果你用Postman之類的測試出了異常則是:
- {
- "timestamp": "2021-04-29T22:45:33.231+0000",
- "status": 500,
- "message": "Internal Server Error",
- "path": "foo/bar"
- }
這個(gè)是怎么實(shí)現(xiàn)的呢?Spring Boot在啟動時(shí)會注冊一個(gè)ErrorPageFilter,當(dāng)Servlet發(fā)生異常時(shí),該過濾器就會攔截處理,將異常根據(jù)不同的策略進(jìn)行處理:當(dāng)異常已經(jīng)在處理的話直接處理,否則轉(zhuǎn)發(fā)給對應(yīng)的錯(cuò)誤頁面。有興趣的可以去看下源碼,邏輯不復(fù)雜,這里就不貼了。
另外當(dāng)一個(gè) Servlet 拋出一個(gè)異常時(shí),處理異常的Servlet可以從HttpServletRequest里面得到幾個(gè)屬性,如下:
異常屬性
我們可以從上面的幾個(gè)屬性中獲取異常的詳細(xì)信息。
默認(rèn)錯(cuò)誤頁面
通常Spring Boot出現(xiàn)異常默認(rèn)會跳轉(zhuǎn)到/error進(jìn)行處理,而/error的相關(guān)邏輯則是由BasicErrorController實(shí)現(xiàn)的。
- @Controller
- @RequestMapping("${server.error.path:${error.path:/error}}")
- public class BasicErrorController extends AbstractErrorController {
- //返回錯(cuò)誤頁面
- @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);
- }
- // 其它省略
- }
而對應(yīng)的配置:
- @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()));
- }
所以我們只需要重新實(shí)現(xiàn)一個(gè)ErrorController并注入Spring IoC就可以替代默認(rèn)的處理機(jī)制。而且我們可以很清晰的發(fā)現(xiàn)這個(gè)BasicErrorController不但是ErrorController的實(shí)現(xiàn)而且是一個(gè)控制器,如果我們讓控制器的方法拋異常,肯定可以被自定義的統(tǒng)一異常處理。所以我對BasicErrorController進(jìn)行了改造:
- @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);
- }
- }
直接拋異常,簡單省力!凡是這里捕捉的到的異常大部分還沒有經(jīng)過Controller,我們通過ExceptionController中繼也讓這些異常被統(tǒng)一處理,保證整個(gè)應(yīng)用的異常處理對外保持一個(gè)統(tǒng)一的門面。