SpringBoot異常處理?用這兩個(gè)就夠啦!
在日常項(xiàng)目中,我們難免會(huì)遇到系統(tǒng)錯(cuò)誤的情況。如果對(duì)系統(tǒng)異常的情況不做處理,Springboot本身會(huì)默認(rèn)將錯(cuò)誤異常作為接口的請(qǐng)求返回。
?@GetMapping("/testNorError") ?public void testNorError() { ? ? ?try { ? ? ? ? ?throw new MyException(6000, "我的錯(cuò)誤"); ? ? }catch (Exception e){ ? ? ? ? ?throw new MyException(5000, "我的包裝異常", e); ? ? } ?}
從上圖可以看到,Springboot沒有對(duì)異常進(jìn)行處理的情況下,將錯(cuò)誤的堆棧直接當(dāng)做響應(yīng)數(shù)據(jù)返回了。這樣對(duì)用戶既不友好,又可能因?yàn)樾孤┫到y(tǒng)堆棧信息引發(fā)潛在的安全風(fēng)險(xiǎn)。因此,搭建一個(gè)完善的異常處理機(jī)制,對(duì)于維護(hù)系統(tǒng)健壯性是十分必要的。
通用異常處理
要快速的搭建異常處理機(jī)制,那么需要考慮如何對(duì)異常進(jìn)行捕獲并加以處理?最便捷的方法便是用 @ExceptionHandler注解實(shí)現(xiàn)。
? ? ?@ExceptionHandler(MyException.class) ? ? ?protected ResponseEntity<Object> handleException(Exception ex) { ? ? ? ? ?LOGGER.error("Failed to execute,handleException:{}", ex.getMessage(), ex); ? ? ? ? ?return new ResponseEntity<>(new ResultDTO().fail(ResultCodeEnum.ERROR_SERVER), HttpStatus.OK); ? ? }
通過在Controller內(nèi)添加上述的異常處理代碼,Springboot就可以將相關(guān)的錯(cuò)誤信息轉(zhuǎn)義成系統(tǒng)的統(tǒng)一錯(cuò)誤處理,進(jìn)而避免堆棧外露。(這里的ResultDTO是系統(tǒng)內(nèi)自定義的JSON結(jié)構(gòu),可以根據(jù)自己的業(yè)務(wù)自行修改。)
然而,@ExceptionHandler本身存在一個(gè)弊端,就是他作用的范圍必須是Controller,也就意味著有多少個(gè)Controller,你的異常處理代碼便要重復(fù)寫多遍,這無(wú)疑是低效率的。為了減少重復(fù)的代碼冗余,@ControllerAdvance就進(jìn)入了我們的視野。
?@ControllerAdvice ?@Slf4j ?public class ExtGlobalExceptionHandler { ? ? ? ? ? ?@ExceptionHandler(Exception.class) ? ? ?protected ResponseEntity<Object> handleException(Exception ex) { ? ? ? ? ?LOGGER.error("Failed to execute,handleException:{}", ex.getMessage(), ex); ? ? ? ? ?return new ResponseEntity<>(new ResultDTO().fail(ResultCodeEnum.ERROR_SERVER), HttpStatus.OK); ? ? } ?}
簡(jiǎn)單來(lái)說(shuō),@ControllerAdvance是一個(gè)全局處理的注解,其中的代碼會(huì)對(duì)所有的Controller生效,通常會(huì)搭配@ExceptionHandler處理異常,由此以來(lái)就可以實(shí)現(xiàn)只編寫一次異常處理方法就可以處理全局異常的情況。
至于@ControllerAdvance和@ExceptionHandler是如何實(shí)現(xiàn)這個(gè)神奇的功能的,限于篇幅原因,后續(xù)會(huì)考慮單獨(dú)出一篇文章詳細(xì)介紹。(其實(shí)根據(jù)名字,不難推斷ControllerAdvance就是一種針對(duì)于Controller對(duì)象的動(dòng)態(tài)代理罷了。)
個(gè)性化異常處理
用了@ControllerAdvance和ExceptionHandler,幾乎可以解決80%的項(xiàng)目面臨的報(bào)錯(cuò)處理問題。然而,思考一下。如果一個(gè)項(xiàng)目中出現(xiàn)了多組人同時(shí)維護(hù)、迭代一個(gè)系統(tǒng)的時(shí)候(降本增效嘛,懂的都懂),每組人要關(guān)注的報(bào)錯(cuò)自然會(huì)不一樣。如A組人只關(guān)注報(bào)錯(cuò)A,B組人員只關(guān)注報(bào)錯(cuò)B,那么這種通用的異常解決方案是無(wú)法區(qū)分開的。
針對(duì)于這種情況,就不得不請(qǐng)出另外一位大佬了,他就是:AOP,針對(duì)于動(dòng)態(tài)代理有很多的實(shí)現(xiàn)方式和框架,這里我們直接默認(rèn)采用SpringBoot的自帶AOP框架:
?<dependency> ? ? ?<groupId>org.springframework.boot</groupId> ? ? ?<artifactId>spring-boot-starter-aop</artifactId> ? ? ?<version>2.1.11.RELEASE</version> ?</dependency>
不管選擇的AOP實(shí)現(xiàn)框架是什么,要采用AOP編碼都少不了以下兩個(gè)步驟:
1、定義切點(diǎn)和執(zhí)行時(shí)機(jī)(哪些地方要做增強(qiáng))
2、定義通知(要怎么增強(qiáng))
定義切點(diǎn)和執(zhí)行時(shí)機(jī)
對(duì)于Springboot自帶的AOP框架,其執(zhí)行時(shí)機(jī)共有以下五個(gè):
@After | 后置增強(qiáng) | 目標(biāo)方法執(zhí)行之后調(diào)用增強(qiáng)方法 |
@Before | 前置增強(qiáng) | 目標(biāo)方法執(zhí)行之前先調(diào)用增強(qiáng)方法 |
@AfterReturning | 返回增強(qiáng) | 目標(biāo)方法執(zhí)行return之后返回結(jié)果之前調(diào)用增強(qiáng)方法,如果出異常則不執(zhí)行 |
@AfterThrowing | 異常增強(qiáng) | 目標(biāo)方法執(zhí)行產(chǎn)生異常調(diào)用增強(qiáng)方法,需注意的是,處理后異常依舊會(huì)往上拋出,不會(huì)被catch。 |
@Around | 環(huán)繞增強(qiáng) | 環(huán)繞增強(qiáng)包含前面四種增強(qiáng),通過一定的try-catch處理,環(huán)繞類型可以替代上述的任意一種增強(qiáng)。 |
了解了SpringBoot的動(dòng)態(tài)代理的執(zhí)行時(shí)機(jī)之后,我們還需要知道其定義切點(diǎn)的方式。框架定義切點(diǎn)的方式主要有兩個(gè):
- 切點(diǎn)表達(dá)式
- 注解
注釋
我們首先介紹注釋的正確打開方式。要通過注解來(lái)實(shí)現(xiàn)自己的AOP,那么首先需要定義一個(gè)新的注解。這里我簡(jiǎn)單定義了一個(gè)注解:
?@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER}) ?@Retention(RetentionPolicy.RUNTIME) ?@Documented ?public @interface MyAnnotation { ? ? ? ?String SERVER_NAME() default ""; ?? ? ? ?String action() default ""; ?}
在定義了注解以后,將注解定義為方法的入?yún)?,并通過@annotation()標(biāo)注出注解的變量名稱,由此就可以實(shí)現(xiàn)注解AOP的功能。
?//處理注解的地方 ?@Around(value = "@annotation(name)") ?public <T> T test(ProceedingJoinPoint point, MyAnnotation name) throws Throwable { ? ? ?String serverName = name.SERVER_NAME(); ? ? ?//處理異常 ? ? ?return handlerRpcException(point, serverName); ?} ?? ?//具體代碼執(zhí)行處 ?@MyAnnotation(SERVER_NAME = "下游系統(tǒng)", action = "操作處理") ?public <T> T testFunction() { ? ? ?return (T) new ResultDTO<>().success(Boolean.TRUE); ?}
切點(diǎn)表達(dá)式
Springboot的AOP中,還提供了一種十分強(qiáng)大的實(shí)現(xiàn)動(dòng)態(tài)代理切點(diǎn)標(biāo)注的方式,即切點(diǎn)表達(dá)式,其基本模式如下所示:
?execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
注意到modifiers-pattern?、declaring-type-pattern?、throws-pattern?等攜帶問號(hào)的參數(shù)都是非必填的。緊接著我們來(lái)逐一介紹上述參數(shù)的含義:
- modifiers-pattern? :修飾符匹配,主要表示的是切點(diǎn)是public/private/protected/default的哪一種。
- ret-type-pattern:顧名思義,指的是返回值的類型,常見如:void/Boolean/String等
- declaring-type-pattern? :這個(gè)指的是被增強(qiáng)的方法、屬性的類路徑,如com.example.demo.service.aop.MyAspect等
- name-pattern(param-pattern) :這個(gè)是相對(duì)關(guān)鍵的參數(shù),指的是被增強(qiáng)的方法名稱以及其對(duì)應(yīng)的參數(shù)類型。
- throws-pattern:throw-pattern見詞知意,可以知道它是指的方法所拋出的異常類型。
除了了解了上述的表達(dá)式的基本匹配含義以外,還有幾個(gè)特殊的符號(hào)通配指的提一下:
***** :匹配任何數(shù)量字符 .. :匹配任何數(shù)量字符的重復(fù),如在類型模式中匹配任何數(shù)量子包;而在方法參數(shù)模式中匹配任何數(shù)量參數(shù)(0個(gè)或者多個(gè)參數(shù)) + :匹配指定類型及其子類型;僅能作為后綴放在類型模式后邊
也許上面的代碼和介紹讓你一臉懵逼,沒關(guān)系,可以簡(jiǎn)單看下下面兩個(gè)表達(dá)式的含義,你就大致明白他們的含義了:
?// 1、代表【返回值任意】且前綴為【com.example.demo.rpc】的【任意類下】【任意名稱】的【所有參數(shù)】方法 ?execution(* com.example.demo.rpc.*.*(..)) ?? ?// 2、代表【返回值為Boolean】且位于【com.example.demo.rpc及其子包下】的【任意名稱】的【以String為最后一個(gè)入?yún)?shù)】的方法 ?execution(Boolean com.example.demo.rpc..*(.., String))
借助于切面表達(dá)式,我們可以很自由靈活地定義出我們的切點(diǎn),從而通過AOP實(shí)現(xiàn)我們對(duì)于異常的處理
@Pointcut("execution(Boolean com.example.demo.rpc..*(.., String)) || execution(另外一個(gè)表達(dá)式)") private void PointCutOfAnno() { } @Around(value = "PointCutOfAnno()") public <T> T testForAOP(ProceedingJoinPoint point) throws Throwable { //處理對(duì)應(yīng)的異常 return handlerRpcException(point, serverName); }
總結(jié)
本文介紹了兩種Springboot下針對(duì)于異常處理的編寫方法:
一、借助于@ControllerAdvance和@ExceptionHandler實(shí)現(xiàn)的通用異常處理方法
二、借助于AOP實(shí)現(xiàn)的個(gè)性化異常處理機(jī)制。
兩者其實(shí)本質(zhì)上的實(shí)現(xiàn)思路都是一樣的,通過對(duì)執(zhí)行代碼做動(dòng)態(tài)代理,從而將錯(cuò)誤包裝起來(lái),達(dá)到異常不外漏的效果。在實(shí)際業(yè)務(wù)場(chǎng)景中,方法一幾乎可以涵蓋80%的異常處理場(chǎng)景。方案二則主要針對(duì)一個(gè)系統(tǒng)中需要做個(gè)性化處理的情況,可以根據(jù)具體的業(yè)務(wù)需要進(jìn)行選擇。