SpringBoot全局異常處理方式
異常處理方案分類
異常處理主要分為三類:
- 基于請求轉(zhuǎn)發(fā)的方式處理異常;
- 基于異常處理器的方式處理異常;
- 基于過濾器的方式處理異常。
基于請求轉(zhuǎn)發(fā)
基于請求轉(zhuǎn)發(fā)的異常處理方式是真正的全局異常處理。
實現(xiàn)方式有:
- BasicExceptionController
基于異常處理器
基于異常處理器的異常處理方式其實并不是真正的全局異常處理,因為它處理不了過濾器等拋出的異常。
實現(xiàn)方式有:
- @ExceptionHandler
- @ControllerAdvice+@ExceptionHandler
- SimpleMappingExceptionResolver
- HandlerExceptionResolver
基于過濾器
基于過濾器的異常處理方式近似與全局異常處理。它能處理過濾器及之后的環(huán)節(jié)拋出的異常。
實現(xiàn)方式有:
- Filter
常見異常處理實現(xiàn)方案
1. BasicExceptionController
這是SpringBoot默認處理異常方式:一旦程序中出現(xiàn)了異常SpringBoot就會請求/error的url,在SpringBoot中提供了一個叫BasicExceptionController的類來處理/error請求,然后跳轉(zhuǎn)到默認顯示異常的頁面來展示異常信息。顯示異常的頁面也可以自定義,在目錄src/main/resources/templates/下定義一個叫error的文件,可以是jsp也可以是html 。
此種方式是通過請求轉(zhuǎn)發(fā)實現(xiàn)的,出現(xiàn)異常時,會轉(zhuǎn)發(fā)到請求到/error,該接口對異常進行處理返回。是最符合全局異常處理的。
可以自定義Controller繼承BasicErrorController異常處理來實現(xiàn)異常處理的自定義。
@Slf4j @RestController public class MyErrorController extends BasicErrorController { public MyErrorController() { super(new DefaultErrorAttributes(), new ErrorProperties()); } /** * produces 設(shè)置返回的數(shù)據(jù)類型:application/json * @param request 請求 * @return 自定義的返回實體類 */ @Override @RequestMapping(value = "", produces = {MediaType.APPLICATION_JSON_VALUE}) public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { // 獲取錯誤信息 Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = getStatus(request); String code = body.get("status").toString(); String message = body.get("message").toString(); return new ResponseEntity(ApiUtil.fail(message), HttpStatus.OK); } }
需要注意
1.該種方式能獲取到的信息時有限的。一般情況只能獲取到下面這幾個參數(shù)(特殊情況會有補充參數(shù))。
現(xiàn)在一般項目需要的響應信息都是自定義統(tǒng)一格式的JSON(code、msg、data)。對于自定義業(yè)務錯誤碼code不好得到,對于錯誤信息msg有時得到的也不一定是你所想要的(簡單說就是一些特殊的異常描述信息不好得到)。
比如:自定義的參數(shù)校驗信息
@NotNull(message = "主鍵不能為空")
message參數(shù)取到的并不是“主鍵不能為空”。
2.當出現(xiàn)拋出兩次異常,第一次被異常處理器處理,第二次異常轉(zhuǎn)由BasicExceptionController處理。但能取到的異常信息可能是一次的,具體原因下面有分析。
2. @ExceptionHandler
該種方式只能作用于使用@ExceptionHandler注解的Controller的異常,對于其他Controller的異常就無能為力了,所以并不不推薦使用。
此種方式是通過異常處理器實現(xiàn)的,使用HandlerExceptionResolverComposite異常處理器中的ExceptionHandlerExceptionResolver異常處理器處理的。
@RestController public class TestController { @GetMapping("test9") public FundInfo test9() throws Exception { throw new Exception("test9 error"); } @GetMapping("test10") public FundInfo test10() throws Exception { throw new IOException("test10 error"); } @ExceptionHandler(Exception.class) public ApiResult exceptionHandler(Exception e) { return ApiUtil.custom(500, e.getMessage()); } }
注意:如果既在具體Controller使用了@ExceptionHandler,也定義了全局異常處理器類(@ControllerAdvice+@ExceptionHandler),優(yōu)先使用Controller定義的@ExceptionHandler處理。如果處理不了,才會使用全局異常處理器處理。
3. @ControllerAdvice+@ExceptionHandler
使用 @ControllerAdvice+@ExceptionHandler注解能夠進行近似全局異常處理,這種方式推薦使用。
一般說它只能處理控制器中拋出的異常,這種說法并不準確,其實它能處理DispatcherServlet.doDispatch方法中DispatcherServlet.processDispatchResult方法之前捕捉到的所有異常,包括:攔截器、參數(shù)綁定(參數(shù)解析、參數(shù)轉(zhuǎn)換、參數(shù)校驗)、控制器、返回值處理等模塊拋出的異常。
此種方式是通過異常處理器實現(xiàn)的,使用HandlerExceptionResolverComposite異常處理器中的ExceptionHandlerExceptionResolver異常處理器處理的。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ......省略代碼...... try { ModelAndView mv = null; Exception dispatchException = null; try { ......省略代碼...... mappedHandler = getHandler(processedRequest); ......省略代碼...... HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); ......省略代碼...... if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } ......省略代碼...... mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } ......省略代碼...... mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { dispatchException = new NestedServletException("Handler dispatch failed", err); } processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { ......省略代碼...... } catch (Throwable err) { ......省略代碼...... } finally { ......省略代碼...... } }
使用方式
定義一個類,使用@ControllerAdvice注解該類,使用@ExceptionHandler注解方法。@RestControllerAdvice注解是@ControllerAdvice注解的擴展(@RestControllerAdvice=@ControllerAdvice+@ResponseBody),返回值自動為JSON的形式。
/** * 全局異常處理器 */ @Slf4j @SuppressWarnings("ALL") @RestControllerAdvice public class MyGlobalExceptionHandler { @ExceptionHandler(BindException.class) @ResponseStatus(HttpStatus.OK) public ApiResult bindException(HttpServletRequest request, HttpServletResponse response, BindException exception) { return ApiUtil.fail(exception.getBindingResult().getFieldError().getDefaultMessage()); } @ExceptionHandler(org.springframework.web.bind.MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.OK) public ApiResult methodArgumentNotValidException(HttpServletRequest request, HttpServletResponse response, MethodArgumentNotValidException exception) { return ApiUtil.fail(exception.getBindingResult().getFieldError().getDefaultMessage()); } @ExceptionHandler(MissingServletRequestParameterException.class) @ResponseStatus(HttpStatus.OK) public ApiResult methodArgumentNotValidException(HttpServletRequest request, HttpServletResponse response, MissingServletRequestParameterException exception) { return ApiUtil.fail(exception.getMessage()); } @ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.OK) public ApiResult methodArgumentNotValidException(HttpServletRequest request, HttpServletResponse response, ConstraintViolationException exception) { System.out.println(exception.getLocalizedMessage()); Iterator<ConstraintViolation<?>> iterator = exception.getConstraintViolations().iterator(); if (iterator.hasNext()) { ConstraintViolationImpl next = (ConstraintViolationImpl)iterator.next(); return ApiUtil.fail(next.getMessage()); } return ApiUtil.fail(exception.getMessage()); } @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.OK) public ApiResult exception(HttpServletRequest request, HttpServletResponse response, Exception exception) { return ApiUtil.fail(exception.getMessage()); } }
@ResponseStatus注解
作用:指定http狀態(tài)碼,正確執(zhí)行時返回該狀態(tài)碼,但方法執(zhí)行報錯時,該返回啥狀態(tài)碼就是啥狀態(tài)碼,指定的狀態(tài)碼無效。
4. SimpleMappingExceptionResolver
使用簡單映射異常處理器處理異常,通過配置SimpleMappingExceptionResolver類也是進行近似全局異常處理,但該種方式不能得到具體的異常信息,且返回的是視圖,不推薦使用。
此種方式是通過異常處理器實現(xiàn)的,使用SimpleMappingExceptionResolver異常處理器處理的。
@Configuration public class GlobalExceptionConfig { @Bean public SimpleMappingExceptionResolver getSimpleMappingExceptionResolver(){ SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver(); /** * 參數(shù)一:異常的類型,這里必須要異常類型的全名 * 參數(shù)二:要跳轉(zhuǎn)的視圖名稱 */ Properties mappings = new Properties(); mappings.put("java.lang.ArithmeticException", "error1"); mappings.put("java.lang.NullPointerException", "error1"); mappings.put("java.lang.Exception", "error1"); mappings.put("java.io.IOException", "error1"); // 設(shè)置異常與視圖的映射信息 resolver.setExceptionMappings(mappings); return resolver; } }
5. HandlerExceptionResolver
實現(xiàn)HandlerExceptionResolver接口來處理異常,該種方式是近似全局異常處理。
此種方式是通過異常處理器實現(xiàn)的,使用自定義的異常處理器(實現(xiàn)HandlerExceptionResolver接口)處理的。
public class MyExceptionResolver extends AbstractHandlerExceptionResolver { /** * 異常解析器的順序, 數(shù)值越小,表示優(yōu)先級越高 * @return */ @Override public int getOrder() { return -999999; } @Override protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { response.setContentType(MediaType.APPLICATION_JSON_VALUE); try { response.getWriter().write(JSON.toJSONString(ApiUtil.fail(ex.getMessage()))); } catch (IOException e) { e.printStackTrace(); } return null; } }
AbstractHandlerExceptionResolver類實現(xiàn)了HandlerExceptionResolver接口。
6. Filter
基于過濾器的異常處理方式,比異常處理器處理的范圍要大一些(能處理到Filter過濾器拋出的異常),更近似全局異常處理。使用自定義過濾器進行異常處理時,該過濾器應該放到過濾鏈的第一個位置,這樣才能保證能處理到后續(xù)過濾器拋出的異常。
@Bean ExceptionFilter exceptionFilter() { return new ExceptionFilter(); } @Bean public FilterRegistrationBean exceptionFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(exceptionFilter()); registration.setName("exceptionFilter"); //此處盡量小,要比其他Filter靠前 registration.setOrder(-1); return registration; }
/** * 自定義異常過濾器 * 用于處理Controller外拋出的異常(如Filter拋出的異常) */ @Slf4j public class ExceptionFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException,IOException { try { filterChain.doFilter(httpServletRequest, httpServletResponse); } catch (IOException e) { httpServletResponse.getWriter().write(JSON.toJSONString(ApiUtil.fail(e.getMessage()))); } } }
上面的寫法其實還是有一定問題的,如果進入了catch就會重復寫入httpServletResponse,可能會導致產(chǎn)生一些列的問題。
舉例一個問題來說明,通過上面的寫法,這樣的對響應的寫入一般是累加的,可能會導致返回的數(shù)據(jù)格式有問題,比如:當異常處理器處理了Controller拋出的異常,寫入了響應,然后過濾器又拋出了異常,被ExceptionFilter給catch到,這就有一次處理了異常,寫入了響應,最后的到的響應數(shù)據(jù)可能是這樣的:
{ "code": 500, "msg": "Controller error" }{ "code": 505, "msg": "Filter error" }
這個時候我們一般會使用代理類來再次封裝Response,filterChain.doFilter傳遞的是封裝后的代理類。
Response代理類
/** * Response代理類 */ public class ResponseWrapper extends HttpServletResponseWrapper { private ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); private PrintWriter printWriter = new PrintWriter(outputStream); public ResponseWrapper(HttpServletResponse response) { super(response); } @Override public ServletOutputStream getOutputStream() throws IOException { return new ServletOutputStream() { @Override public boolean isReady() { return false; } @Override public void setWriteListener(WriteListener writeListener) { } @Override public void write(int b) throws IOException { outputStream.write(b); } @Override public void write(byte[] b) throws IOException { outputStream.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { outputStream.write(b, off, len); } @Override public void flush() throws IOException { outputStream.flush(); } }; } @Override public PrintWriter getWriter() throws IOException { return printWriter; } public void flush(){ try { printWriter.flush(); printWriter.close(); outputStream.flush(); outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } public byte[] getContent() { flush(); return outputStream.toByteArray(); } }
自定義過濾器類修改為
@Slf4j public class ExceptionFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException,IOException { try { // 封裝Response,得到代理對象 ResponseWrapper responseWrapper = new ResponseWrapper(httpServletResponse); // 使用代理對象 filterChain.doFilter(httpServletRequest, responseWrapper); // 讀取響應內(nèi)容 byte[] bytes = responseWrapper.getContent(); // 這里可以對響應內(nèi)容進行修改等操作 // 模擬Filter拋出異常 if (true) { throw new IOException("Filter error"); } // 內(nèi)容重新寫入原響應對象中 httpServletResponse.getOutputStream().write(bytes); } catch (Exception e) { httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE); httpServletResponse.getOutputStream().write(JSON.toJSONString(ApiUtil.fail(e.getMessage())).getBytes()); } } }
全局異常處理實現(xiàn)方案
要想實現(xiàn)正在的全局異常處理,顯然只通過異常處理器的方式處理是不夠的,這種方案處理不了過濾器等拋出的異常。
全局異常處理的幾種實現(xiàn)方案:
- 基于請求轉(zhuǎn)發(fā);
- 基于異常處理器+請求轉(zhuǎn)發(fā)補充;
- 基于過濾器;
- 基于異常處理器+過濾器補充。
1. 請求轉(zhuǎn)發(fā)
該方案貌似不好獲取到特殊的異常描述信息(沒仔細研究),如參數(shù)校驗中的message屬性信息:
@NotNull(message = "主鍵不能為空")
本方案通過自定義錯誤處理Controller繼承BasicExceptionController來實現(xiàn)。
具體實現(xiàn)參考:常用異常處理實現(xiàn)方案1。
2. 異常處理器+請求轉(zhuǎn)發(fā)補充
(1)自定義異常處理Controller實現(xiàn)BasicExceptionController
具體實現(xiàn)參考:常用異常處理實現(xiàn)方案1。
(2)異常處理器實現(xiàn)
- 方式1:@ControllerAdvice+@ExceptionHandler(推薦使用)
具體實現(xiàn)參考:常用異常處理實現(xiàn)方案3。 - 方式2:SimpleMappingExceptionResolver
具體實現(xiàn)參考:常用異常處理實現(xiàn)方案4。 - 方式3:HandlerExceptionResolver
具體實現(xiàn)參考:常用異常處理實現(xiàn)方案5。
3. 過濾器
具體實現(xiàn)參考:常用異常處理實現(xiàn)方案6。
4. 異常處理器+過濾器補充
創(chuàng)建自定義過濾器bean
@Bean ExceptionFilter exceptionFilter() { return new ExceptionFilter(); } @Bean public FilterRegistrationBean exceptionFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(exceptionFilter()); registration.setName("exceptionFilter"); //此處盡量小,要比其他Filter靠前 registration.setOrder(-1); return registration; }
方式1:@ControllerAdvice+@ExceptionHandler+Filter(推薦使用)
@ControllerAdvice+@ExceptionHandler的實現(xiàn)參考:常用異常處理實現(xiàn)方案3。
Filter實現(xiàn):
- 方式1:參考常用異常處理實現(xiàn)方案6。
- 方式2:借助異常處理器處理異常。
@Slf4j public class ExceptionFilter extends OncePerRequestFilter { /** * 遇到的坑,ExceptionFilter對象的創(chuàng)建沒有交給Spring容器(直接new的),導致@Autowired注入不會生效 */ @Autowired private HandlerExceptionResolver handlerExceptionResolver; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException,IOException { try { // 封裝Response,得到代理對象 ResponseWrapper responseWrapper = new ResponseWrapper(httpServletResponse); // 使用代理對象 filterChain.doFilter(httpServletRequest, responseWrapper); // 讀取響應內(nèi)容 byte[] bytes = responseWrapper.getContent(); // 這里可以對響應內(nèi)容進行修改等操作 // 模擬Filter拋出異常 if (true) { throw new IOException("Filter error"); } // 內(nèi)容重新寫入原響應對象中 httpServletResponse.getOutputStream().write(bytes); } catch (Exception e) { handlerExceptionResolver.resolveException(httpServletRequest, httpServletResponse, null, e); } } }
注入的HandlerExceptionResolver其實是HandlerExceptionResolverComposite異常處理器,最終是使用異常處理器中的ExceptionHandlerExceptionResolver異常處理器處理的。
方式2:HandlerExceptionResolver+Filter
HandlerExceptionResolver的實現(xiàn)參考:常用異常處理實現(xiàn)方案5。
Filter的實現(xiàn):注入的MyExceptionResolver是我們自定義的異常處理器。
- 方式1:參考常用異常處理實現(xiàn)方案6。
- 方式2:借助異常處理器處理異常。
@Slf4j public class ExceptionFilter extends OncePerRequestFilter { @Autowired private MyExceptionResolver myExceptionResolver; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException,IOException { try { // 封裝Response,得到代理對象 ResponseWrapper responseWrapper = new ResponseWrapper(httpServletResponse); // 使用代理對象 filterChain.doFilter(httpServletRequest, responseWrapper); // 讀取響應內(nèi)容 byte[] bytes = responseWrapper.getContent(); // 這里可以對響應內(nèi)容進行修改等操作 // 模擬Filter拋出異常 if (true) { throw new IOException("Filter error"); } // 內(nèi)容重新寫入原響應對象中 httpServletResponse.getOutputStream().write(bytes); } catch (Exception e) { myExceptionResolver.resolveException(httpServletRequest, httpServletResponse, null, e); } } }
注意事項
- 2、4兩種通過組合的方式進行異常處理需要考慮到的問題:對于一個請求,如果兩個地方都捕捉到了異常,要考慮兩次異常處對response響應信息的重復寫入問題。
- 比如:異常處理器處理了控制器拋出的異常,寫入響應;過濾器處理了過濾器拋出的異常,寫入響應。這就會出現(xiàn)響應被寫入了兩次的問題或者第二次寫入響應直接報錯。
- 一些處理思路:考慮使用Response代理類。第一次處理時,異常處理器寫入的響應信息是寫入到Response代理對象的,并可以從Response代理類中得到寫入的響應信息;第二次處理,過濾器等寫入的響應寫入到Response原對象中的。
- 過程中發(fā)現(xiàn)一個問題:通過BasicExceptionController+異常處理器處理異常的方式時。Controller拋出了異常,被異常處理器處理,返回的過程中,F(xiàn)ilter又拋出了一個異常,被BasicExceptionController處理,但BasicExceptionController的到的異常信息卻是Controller產(chǎn)生的異常信息,而不是Filter產(chǎn)生的異常信息。但是調(diào)到BasicExceptionController去處理異常又卻是是因為Filter拋出異常產(chǎn)生的。
- 個人猜想:異常處理器在處理異常時,不僅是把響應內(nèi)容部部分寫入了Response,還把異常信息寫入了Response。當因為異常跳轉(zhuǎn)到BasicExceptionController進行處理,BasicExceptionController在獲取異常信息時,會先從Response獲取異常信息,獲取不到才會從異常中獲取異常信息。
方案推薦
請求轉(zhuǎn)發(fā)(推薦
)。
- 完全統(tǒng)一的全局異常處理,自定義異常處理Controller能達到自定義統(tǒng)一響應信息格式目的。
- 但是,現(xiàn)在一般項目需要的響應信息都是自定義統(tǒng)一格式的JSON(code、msg、data)。但對于自定義業(yè)務錯誤碼code不好得到,對于錯誤信息msg有時得到的也不一定是你所想要的。
- 但感覺通過自定義的擴展是能得到業(yè)務狀態(tài)碼和特殊異常描述信息的(沒詳細研究)。
異常處理+請求轉(zhuǎn)發(fā)補充(個人最推薦
)。
- 推薦使用
@ControllerAdvice+@ExceptionHandler+BasicExceptionController
的方式。 - 異常處理器能自定義處理大多異常(包括特殊的異常),剩余處理不到的異常交給異常處理控制器處理。
過濾器(不推薦
)。
- 異常處理全需要手寫代碼實現(xiàn),自己的代碼肯定不會太完美,可能有沒考慮到的情況,容易出問題;
- 且過濾器之前拋出的異常處理不到
異常處理器+過濾器補充(不太推薦
)。
- 推薦使用
@ControllerAdvice+@ExceptionHandler+Filter(借助異常處理器處理異常)
的方式 - 但過濾器之前拋出的異常處理不到
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
java 較大數(shù)據(jù)量取差集,list.removeAll性能優(yōu)化詳解
這篇文章主要介紹了java 較大數(shù)據(jù)量取差集,list.removeAll性能優(yōu)化詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09SpringBoot默認使用HikariDataSource數(shù)據(jù)源方式
這篇文章主要介紹了SpringBoot默認使用HikariDataSource數(shù)據(jù)源方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10Spring Security SecurityContextHolder組件示例說明
SpringSecurity的SecurityContextHolder組件是存儲當前安全上下文的地方,包括認證用戶信息,它支持全局訪問、線程局部存儲和上下文傳播,是SpringSecurity認證和授權(quán)的核心,文章通過示例展示了如何訪問已認證用戶的詳細信息、手動設(shè)置認證信息以及使用認證信息保護方法2024-11-11MyBatis分頁查詢返回list的時候出現(xiàn)null的問題
這篇文章主要介紹了MyBatis分頁查詢返回list的時候出現(xiàn)null的問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07