使用切面管理異常的原因:
今天的內容干貨滿滿哦~並且是我自己在平時工作中的一些問題與解決途徑,對實際開發的作用很大,好,閑言少敘,讓我們開始吧~~
我們先看一張錯誤信息在APP中的展示圖: 
是不是體驗很差,整個后台錯誤信息都在APP上打印了。
作為后台開發人員,我們總是在不停的寫各種接口提供給前端調用,然而不可避免的,當后台出現BUG時,前端總是丑陋的講錯誤信息直接暴露給用戶,這樣的用戶體驗想必是相當差的(不過后台開發一看就知道問題出現在哪里)。同時,在解決BUG時,我們總是要問前端拿到參數去調適,排除各種問題(網絡,Json體錯誤,接口名寫錯……BaLa……BaLa……BaLa)。在不考慮前端容錯的情況下。我們自己后台有沒有優雅的解決這個問題的方法呢,今天這篇我們就來使用AOP統一對異常進行記錄以及返回。
SpringBoot引入AOP
在SpringBoot中引入AOP是一件很方便的事,和其他引入依賴一樣,我們只需要在POM中引入starter就可以了:
1 <!--spring切面aop依賴--> 2 <dependency> 3 <groupId>org.springframework.boot</groupId> 4 <artifactId>spring-boot-starter-aop</artifactId> 5 </dependency>
返回體報文定義
接下來我們先想一下,一般我們返回體是什么樣子的呢?或者你覺得一個返回的報文應該具有哪些特征。
-
成功標示:可以用boolean型作為標示位。
-
錯誤代碼:一般用整型作為標示位,羅列的越詳細,前端的容錯也就能做的更細致。
-
錯誤信息:使用String作為錯誤信息的描述,留給前端是否展示給用戶或者進入其他錯誤流程的使用。
-
結果集:在無錯誤信息的情況下所得到的正確數據信息。一般是個Map,前端根據Key取值。
以上是對一個返回體報文一個粗略的定義了,如果再細致點,可以使用簽名進行驗簽功能活着對明文數據進行對稱加密等等。這些我們今天先不討論,我們先完成一個能夠使用的接口信息定義。
我們再對以上提到這些信息做一個完善,去除冗余的字段,對差不多的類型進行合並於封裝。這樣的想法下,我們創建一個返回體報文的實體類。
1 public class Result<T> { 2 3 // error_code 狀態值:0 極為成功,其他數值代表失敗 4 private Integer status; 5 6 // error_msg 錯誤信息,若status為0時,為success 7 private String msg; 8 9 // content 返回體報文的出參,使用泛型兼容不同的類型 10 private T data; 11 12 public Integer getStatus() { 13 return status; 14 } 15 16 public void setStatus(Integer code) { 17 this.status = code; 18 } 19 20 public String getMsg() { 21 return msg; 22 } 23 24 public void setMsg(String msg) { 25 this.msg = msg; 26 } 27 28 public T getData(Object object) { 29 return data; 30 } 31 32 public void setData(T data) { 33 this.data = data; 34 } 35 36 public T getData() { 37 return data; 38 } 39 40 @Override 41 public String toString() { 42 return "Result{" + 43 "status=" + status + 44 ", msg='" + msg + '\'' + 45 ", data=" + data + 46 '}'; 47 }
現在我們已經有一個返回體報文的定義了,那接下來我們可以來創建一個枚舉類,來記錄一些我們已知的錯誤信息,可以在代碼中直接使用。
1 public enum ExceptionEnum { 2 UNKNOW_ERROR(-1,"未知錯誤"), 3 USER_NOT_FIND(-101,"用戶不存在"), 4 ; 5 6 private Integer code; 7 8 private String msg; 9 10 ExceptionEnum(Integer code, String msg) { 11 this.code = code; 12 this.msg = msg; 13 } 14 15 public Integer getCode() { 16 return code; 17 } 18 19 public String getMsg() { 20 return msg; 21 } 22 }
我們在這里把對於不再預期內的錯誤統一設置為-1,未知錯誤。以避免返回給前端大段大段的錯誤信息。
接下來我們只需要創建一個工具類在代碼中使用:
1 public class ResultUtil { 2 3 /** 4 * 返回成功,傳入返回體具體出參 5 * @param object 6 * @return 7 */ 8 public static Result success(Object object){ 9 Result result = new Result(); 10 result.setStatus(0); 11 result.setMsg("success"); 12 result.setData(object); 13 return result; 14 } 15 16 /** 17 * 提供給部分不需要出參的接口 18 * @return 19 */ 20 public static Result success(){ 21 return success(null); 22 } 23 24 /** 25 * 自定義錯誤信息 26 * @param code 27 * @param msg 28 * @return 29 */ 30 public static Result error(Integer code,String msg){ 31 Result result = new Result(); 32 result.setStatus(code); 33 result.setMsg(msg); 34 result.setData(null); 35 return result; 36 } 37 38 /** 39 * 返回異常信息,在已知的范圍內 40 * @param exceptionEnum 41 * @return 42 */ 43 public static Result error(ExceptionEnum exceptionEnum){ 44 Result result = new Result(); 45 result.setStatus(exceptionEnum.getCode()); 46 result.setMsg(exceptionEnum.getMsg()); 47 result.setData(null); 48 return result; 49 } 50 }
以上我們已經可以捕獲代碼中那些在編碼階段我們已知的錯誤了,但是卻無法捕獲程序出的未知異常信息。我們的代碼應該寫得漂亮一點,雖然很多時候我們會說時間太緊了,等之后我再來好好優化。可事實是,我們再也不會回來看這些代碼了。項目總是一個接着一個,時間總是不夠用的。如果真的需要你完善重構原來的代碼,那你一定會非常痛苦,死得相當難看。所以,在第一次構建時,就將你的代碼寫完善了。
一般系統拋出的錯誤是不含錯誤代碼的,除去部分的404,400,500錯誤之外,我們如果想把錯誤代碼定義的更細致,就需要自己繼承RuntimeException這個類后重新定義一個構造方法來定義我們自己的錯誤信息:
1 public class DescribeException extends RuntimeException{ 2 3 private Integer code; 4 5 /** 6 * 繼承exception,加入錯誤狀態值 7 * @param exceptionEnum 8 */ 9 public DescribeException(ExceptionEnum exceptionEnum) { 10 super(exceptionEnum.getMsg()); 11 this.code = exceptionEnum.getCode(); 12 } 13 14 /** 15 * 自定義錯誤信息 16 * @param message 17 * @param code 18 */ 19 public DescribeException(String message, Integer code) { 20 super(message); 21 this.code = code; 22 } 23 24 public Integer getCode() { 25 return code; 26 } 27 28 public void setCode(Integer code) { 29 this.code = code; 30 } 31 }
同時,我們使用一個Handle來把Try,Catch中捕獲的錯誤進行判定,是一個我們已知的錯誤信息,還是一個未知的錯誤信息,如果是未知的錯誤信息,那我們就用log記錄它,便於之后的查找和解決:
1 @ControllerAdvice 2 public class ExceptionHandle { 3 4 private final static Logger LOGGER = LoggerFactory.getLogger(ExceptionHandle.class); 5 6 /** 7 * 判斷錯誤是否是已定義的已知錯誤,不是則由未知錯誤代替,同時記錄在log中 8 * @param e 9 * @return 10 */ 11 @ExceptionHandler(value = Exception.class) 12 @ResponseBody 13 public Result exceptionGet(Exception e){ 14 if(e instanceof DescribeException){ 15 DescribeException MyException = (DescribeException) e; 16 return ResultUtil.error(MyException.getCode(),MyException.getMessage()); 17 } 18 19 LOGGER.error("【系統異常】{}",e); 20 return ResultUtil.error(ExceptionEnum.UNKNOW_ERROR); 21 } 22 }
這里我們使用了 @ControllerAdvice ,使Spring能加載該類,同時我們將所有捕獲的異常統一返回結果Result這個實體。
此時,我們已經完成了對結果以及異常的統一返回管理,並且在出現異常時,我們可以不返回錯誤信息給前端,而是用未知錯誤進行代替,只有查看log我們才會知道真實的錯誤信息。
可能有小伙伴要問了,說了這么久,並沒有使用到AOP啊。不要着急,我們繼續完成我們剩余的工作。
我們使用接口若出現了異常,很難知道是誰調用接口,是前端還是后端出現的問題導致異常的出現,那這時,AOP就發揮作用了,我們之前已經引入了AOP的依賴,現在我們編寫一個切面類,切點如何配置不需要我多說了吧:
1 @Aspect 2 @Component 3 public class HttpAspect { 4 5 private final static Logger LOGGER = LoggerFactory.getLogger(HttpAspect.class); 6 7 @Autowired 8 private ExceptionHandle exceptionHandle; 9 10 @Pointcut("execution(public * com.zzp.controller.*.*(..))") 11 public void log(){ 12 13 } 14 15 @Before("log()") 16 public void doBefore(JoinPoint joinPoint){ 17 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 18 HttpServletRequest request = attributes.getRequest(); 19 20 //url 21 LOGGER.info("url={}",request.getRequestURL()); 22 //method 23 LOGGER.info("method={}",request.getMethod()); 24 //ip 25 LOGGER.info("id={}",request.getRemoteAddr()); 26 //class_method 27 LOGGER.info("class_method={}",joinPoint.getSignature().getDeclaringTypeName() + "," + joinPoint.getSignature().getName()); 28 //args[] 29 LOGGER.info("args={}",joinPoint.getArgs()); 30 } 31 32 @Around("log()") 33 public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { 34 Result result = null; 35 try { 36 37 } catch (Exception e) { 38 return exceptionHandle.exceptionGet(e); 39 } 40 if(result == null){ 41 return proceedingJoinPoint.proceed(); 42 }else { 43 return result; 44 } 45 } 46 47 @AfterReturning(pointcut = "log()",returning = "object")//打印輸出結果 48 public void doAfterReturing(Object object){ 49 LOGGER.info("response={}",object.toString()); 50 } 51 }
我們使用@Aspect來聲明這是一個切面,使用@Pointcut來定義切面所需要切入的位置,這里我們是對每一個HTTP請求都需要切入,在進入方法之前我們使用@Before記錄了調用的接口URL,調用的方法,調用方的IP地址以及輸入的參數等。在整個接口代碼運作期間,我們使用@Around來捕獲異常信息,並用之前定義好的Result進行異常的返回,最后我們使用@AfterReturning來記錄我們的出參。
以上全部,我們就完成了異常的統一管理以及切面獲取接口信息,接下來我們心新寫一個ResultController來測試一下:
1 @RestController 2 @RequestMapping("/result") 3 public class ResultController { 4 5 @Autowired 6 private ExceptionHandle exceptionHandle; 7 8 /** 9 * 返回體測試 10 * @param name 11 * @param pwd 12 * @return 13 */ 14 @RequestMapping(value = "/getResult",method = RequestMethod.POST) 15 public Result getResult(@RequestParam("name") String name, @RequestParam("pwd") String pwd){ 16 Result result = ResultUtil.success(); 17 try { 18 if (name.equals("zzp")){ 19 result = ResultUtil.success(new UserInfo()); 20 }else if (name.equals("pzz")){ 21 result = ResultUtil.error(ExceptionEnum.USER_NOT_FIND); 22 }else{ 23 int i = 1/0; 24 } 25 }catch (Exception e){ 26 result = exceptionHandle.exceptionGet(e); 27 } 28 return result; 29 } 30 }
在上面我們設計了一個controller,如果傳入的name是zzp的話,我們就返回一個用戶實體類,如果傳入的是pzz的話,我們返回一個沒有該用戶的錯誤,其他的,我們讓他拋出一個by zero的異常。
我們用POSTMAN進行下測試:


我們可以看到,前端收到的返回體報文已經按我們要求同意了格式,並且在控制台中我們打印出了調用該接口的一些接口信息,我們繼續測試另外兩個會出現錯誤情況的請求:



我們可以看到,如是我們之前在代碼中定義完成的錯誤信息,我們可以直接返回錯誤碼以及錯誤信息,如果是程序出現了我們在編碼階段不曾預想到的錯誤,則統一返回未知錯誤,並在log中記錄真實錯誤信息。
以上就是我們統一管理結果集以及使用切面來記錄接口調用的一些真實情況,在平時的使用中,大家要清楚切點的優先級以及在不同的切點位置該使用哪些注解來幫助我們完成開發,並且在切面中,如果遇到同步問題該如何解決等等。
轉自:https://blog.csdn.net/qq_31001665/article/details/71357825
