SpringBoot中如何進(jìn)行全局異常處理方式
前言
在SpringBoot的開(kāi)發(fā)中,為了提高程序運(yùn)行的魯棒性,我們經(jīng)常需要對(duì)各種程序異常進(jìn)行處理,但是如果在每個(gè)出異常的地方進(jìn)行單獨(dú)處理的話(huà),這會(huì)引入大量業(yè)務(wù)不相關(guān)的異常處理代碼,增加了程序的耦合,同時(shí)未來(lái)想改變異常的處理邏輯,也變得比較困難。這篇文章帶大家了解一下如何優(yōu)雅的進(jìn)行全局異常處理。
為了實(shí)現(xiàn)全局?jǐn)r截,這里使用到了Spring中提供的兩個(gè)注解,@RestControllerAdvice和@ExceptionHandler,結(jié)合使用可以攔截程序中產(chǎn)生的異常,并且根據(jù)不同的異常類(lèi)型分別處理。下面我會(huì)先介紹如何利用這兩個(gè)注解,優(yōu)雅的完成全局異常的處理,接著解釋這背后的原理。
如何實(shí)現(xiàn)全局?jǐn)r截?
1.1 自定義異常處理類(lèi)
在下面的例子中,我們繼承了ResponseEntityExceptionHandler并使用@RestControllerAdvice注解了這個(gè)類(lèi),接著結(jié)合@ExceptionHandler針對(duì)不同的異常類(lèi)型,來(lái)定義不同的異常處理方法。
這里可以看到我處理的異常是自定義異常,后續(xù)我會(huì)展開(kāi)介紹。
- ResponseEntityExceptionHandler中包裝了各種SpringMVC在處理請(qǐng)求時(shí)可能拋出的異常的處理,處理結(jié)果都是封裝成一個(gè)ResponseEntity對(duì)象。
- ResponseEntityExceptionHandler是一個(gè)抽象類(lèi),通常我們需要定義一個(gè)用來(lái)處理異常的使用@RestControllerAdvice注解標(biāo)注的異常處理類(lèi)來(lái)繼承自ResponseEntityExceptionHandler。
- ResponseEntityExceptionHandler中為每個(gè)異常的處理都單獨(dú)定義了一個(gè)方法,如果默認(rèn)的處理不能滿(mǎn)足你的需求,則可以重寫(xiě)對(duì)某個(gè)異常的處理。
@Log4j2 @RestControllerAdvice public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { /** * 定義要捕獲的異常 可以多個(gè) @ExceptionHandler({}) * * @param request request * @param e exception * @param response response * @return 響應(yīng)結(jié)果 */ @ExceptionHandler(AuroraRuntimeException.class) public GenericResponse customExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) { AuroraRuntimeException exception = (AuroraRuntimeException) e; if (exception.getCode() == ResponseCode.USER_INPUT_ERROR) { response.setStatus(HttpStatus.BAD_REQUEST.value()); } else if (exception.getCode() == ResponseCode.FORBIDDEN) { response.setStatus(HttpStatus.FORBIDDEN.value()); } else { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); } return new GenericResponse(exception.getCode(), null, exception.getMessage()); } @ExceptionHandler(NotLoginException.class) public GenericResponse tokenExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) { log.error("token exception", e); response.setStatus(HttpStatus.FORBIDDEN.value()); return new GenericResponse(ResponseCode.AUTHENTICATION_NEEDED); } }
1.2 定義異常碼
這里定義了常見(jiàn)的幾種異常碼,主要用在拋出自定義異常時(shí),對(duì)不同的情形進(jìn)行區(qū)分。
@Getter public enum ResponseCode { SUCCESS(0, "Success"), INTERNAL_ERROR(1, "服務(wù)器內(nèi)部錯(cuò)誤"), USER_INPUT_ERROR(2, "用戶(hù)輸入錯(cuò)誤"), AUTHENTICATION_NEEDED(3, "Token過(guò)期或無(wú)效"), FORBIDDEN(4, "禁止訪(fǎng)問(wèn)"), TOO_FREQUENT_VISIT(5, "訪(fǎng)問(wèn)太頻繁,請(qǐng)休息一會(huì)兒"); private final int code; private final String message; private final Response.Status status; ResponseCode(int code, String message, Response.Status status) { this.code = code; this.message = message; this.status = status; } ResponseCode(int code, String message) { this(code, message, Response.Status.INTERNAL_SERVER_ERROR); } }
1.3 自定義異常類(lèi)
這里我定義了一個(gè)AuroraRuntimeException的異常,就是在上面的異常處理函數(shù)中,用到的異常。
每個(gè)異常實(shí)例會(huì)有一個(gè)對(duì)應(yīng)的異常碼,也就是前面剛定義好的。
@Getter public class AuroraRuntimeException extends RuntimeException { private final ResponseCode code; public AuroraRuntimeException() { super(String.format("%s", ResponseCode.INTERNAL_ERROR.getMessage())); this.code = ResponseCode.INTERNAL_ERROR; } public AuroraRuntimeException(Throwable e) { super(e); this.code = ResponseCode.INTERNAL_ERROR; } public AuroraRuntimeException(String msg) { this(ResponseCode.INTERNAL_ERROR, msg); } public AuroraRuntimeException(ResponseCode code) { super(String.format("%s", code.getMessage())); this.code = code; } public AuroraRuntimeException(ResponseCode code, String msg) { super(msg); this.code = code; } }
1.4 自定義返回類(lèi)型
為了保證各個(gè)接口的返回統(tǒng)一,這里專(zhuān)門(mén)定義了一個(gè)返回類(lèi)型。
@Getter @Setter public class GenericResponse<T> { private int code; private T data; private String message; public GenericResponse() {}; public GenericResponse(int code, T data) { this.code = code; this.data = data; } public GenericResponse(int code, T data, String message) { this(code, data); this.message = message; } public GenericResponse(ResponseCode responseCode) { this.code = responseCode.getCode(); this.data = null; this.message = responseCode.getMessage(); } public GenericResponse(ResponseCode responseCode, T data) { this(responseCode); this.data = data; } public GenericResponse(ResponseCode responseCode, T data, String message) { this(responseCode, data); this.message = message; } }
實(shí)際測(cè)試異常
下面的例子中,我們想獲取到用戶(hù)的信息,如果用戶(hù)的信息不存在,可以直接拋出一個(gè)異常,這個(gè)異常會(huì)被我們上面定義的全局異常處理方法所捕獲,然后根據(jù)不同的異常編碼,完成不同的處理和返回。
public User getUserInfo(Long userId) { // some logic User user = daoFactory.getExtendedUserMapper().selectByPrimaryKey(userId); if (user == null) { throw new AuroraRuntimeException(ResponseCode.USER_INPUT_ERROR, "用戶(hù)id不存在"); } // some logic .... }
以上就完成了整個(gè)全局異常的處理過(guò)程,接下來(lái)重點(diǎn)說(shuō)說(shuō)為什么@RestControllerAdvice和@ExceptionHandler結(jié)合使用可以攔截程序中產(chǎn)生的異常?
全局?jǐn)r截的背后原理?
下面會(huì)提到@ControllerAdvice注解,簡(jiǎn)單地說(shuō),@RestControllerAdvice與@ControllerAdvice的區(qū)別就和@RestController與@Controller的區(qū)別類(lèi)似,@RestControllerAdvice注解包含了@ControllerAdvice注解和@ResponseBody注解。
public class DispatcherServlet extends FrameworkServlet { // ...... protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); initHandlerMappings(context); initHandlerAdapters(context); // 重點(diǎn)關(guān)注 initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context); } // ...... }
在initHandlerExceptionResolvers(context)方法中,會(huì)取得所有實(shí)現(xiàn)了HandlerExceptionResolver接口的bean并保存起來(lái),其中就有一個(gè)類(lèi)型為ExceptionHandlerExceptionResolver的bean,這個(gè)bean在應(yīng)用啟動(dòng)過(guò)程中會(huì)獲取所有被@ControllerAdvice注解標(biāo)注的bean對(duì)象做進(jìn)一步處理,關(guān)鍵代碼在這里:
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver implements ApplicationContextAware, InitializingBean { // ...... private void initExceptionHandlerAdviceCache() { // ...... List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); AnnotationAwareOrderComparator.sort(adviceBeans); for (ControllerAdviceBean adviceBean : adviceBeans) { ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType()); if (resolver.hasExceptionMappings()) { // 找到所有ExceptionHandler標(biāo)注的方法并保存成一個(gè)ExceptionHandlerMethodResolver類(lèi)型的對(duì)象緩存起來(lái) this.exceptionHandlerAdviceCache.put(adviceBean, resolver); if (logger.isInfoEnabled()) { logger.info("Detected @ExceptionHandler methods in " + adviceBean); } } // ...... } } // ...... }
當(dāng)Controller拋出異常時(shí),DispatcherServlet通過(guò)ExceptionHandlerExceptionResolver來(lái)解析異常,而ExceptionHandlerExceptionResolver又通過(guò)ExceptionHandlerMethodResolver 來(lái)解析異常, ExceptionHandlerMethodResolver 最終解析異常找到適用的@ExceptionHandler標(biāo)注的方法是這里:
public class ExceptionHandlerMethodResolver { // ...... private Method getMappedMethod(Class<? extends Throwable> exceptionType) { List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>(); // 找到所有適用于Controller拋出異常的處理方法,例如Controller拋出的異常 // 是AuroraRuntimeException(繼承自RuntimeException),那么@ExceptionHandler(AuroraRuntimeException.class)和 // @ExceptionHandler(Exception.class)標(biāo)注的方法都適用此異常 for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) { if (mappedException.isAssignableFrom(exceptionType)) { matches.add(mappedException); } } if (!matches.isEmpty()) { /* 這里通過(guò)排序找到最適用的方法,排序的規(guī)則依據(jù)拋出異常相對(duì)于聲明異常的深度,例如 Controller拋出的異常是是AuroraRuntimeException(繼承自RuntimeException),那么AuroraRuntimeException 相對(duì)于@ExceptionHandler(AuroraRuntimeException.class)聲明的AuroraRuntimeException.class其深度是0, 相對(duì)于@ExceptionHandler(Exception.class)聲明的Exception.class其深度是2,所以 @ExceptionHandler(BizException.class)標(biāo)注的方法會(huì)排在前面 */ Collections.sort(matches, new ExceptionDepthComparator(exceptionType)); return this.mappedMethods.get(matches.get(0)); } else { return null; } } // ...... }
整個(gè)@RestControllerAdvice處理的流程就是這樣,結(jié)合@ExceptionHandler就完成了對(duì)不同異常的靈活處理。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
springboot結(jié)合mysql主從來(lái)實(shí)現(xiàn)讀寫(xiě)分離的方法示例
這篇文章主要介紹了springboot結(jié)合mysql主從來(lái)實(shí)現(xiàn)讀寫(xiě)分離的方法示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04Spring Boot整合QueryDSL的實(shí)現(xiàn)示例
這篇文章主要介紹了Spring Boot整合QueryDSL的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09spring中@Bean和@Component的區(qū)別及說(shuō)明
文章主要介紹了@Bean和@Component兩個(gè)注解在Spring框架中的定義、作用范圍、創(chuàng)建方式、掃描和識(shí)別機(jī)制以及使用場(chǎng)景和建議2024-12-12Spring AI與DeepSeek實(shí)戰(zhàn)一之快速打造智能對(duì)話(huà)應(yīng)用
本文詳細(xì)介紹了如何通過(guò)SpringAI框架集成DeepSeek大模型,實(shí)現(xiàn)普通對(duì)話(huà)和流式對(duì)話(huà)功能,步驟包括申請(qǐng)API-KEY、項(xiàng)目搭建、配置API-KEY、創(chuàng)建ChatClient對(duì)象、創(chuàng)建對(duì)話(huà)接口、切換模型、使用prompt模板、流式對(duì)話(huà)等,感興趣的朋友一起看看吧2025-03-03springbean的加載過(guò)程及應(yīng)用場(chǎng)景分析
Bean已經(jīng)被實(shí)例化、屬性注入、初始化,并且注冊(cè)到容器中,可以被其他Bean或應(yīng)用程序使用,這篇文章主要介紹了springbean的加載過(guò)程以及應(yīng)用場(chǎng)景,需要的朋友可以參考下2024-04-04idea插件之mybatis log plugin控制臺(tái)sql的問(wèn)題
這篇文章主要介紹了idea插件之mybatis log plugin控制臺(tái)sql,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09