1. 程式人生 > >SpringBoot開發詳解(六)-- 異常統一管理以及AOP的使用

SpringBoot開發詳解(六)-- 異常統一管理以及AOP的使用

AOP在SpringBoot中的使用

使用切面管理異常的原因:

今天的內容乾貨滿滿哦~並且是我自己在平時工作中的一些問題與解決途徑,對實際開發的作用很大,好,閒言少敘,讓我們開始吧~~

我們先看一張錯誤資訊在APP中的展示圖:
這裡寫圖片描述

是不是體驗很差,整個後臺錯誤資訊都在APP上列印了。
作為後臺開發人員,我們總是在不停的寫各種介面提供給前端呼叫,然而不可避免的,當後臺出現BUG時,前端總是醜陋的講錯誤資訊直接暴露給使用者,這樣的使用者體驗想必是相當差的(不過後臺開發一看就知道問題出現在哪裡)。同時,在解決BUG時,我們總是要問前端拿到引數去調適,排除各種問題(網路,Json體錯誤,介面名寫錯……BaLa……BaLa……BaLa)。在不考慮前端容錯的情況下。我們自己後臺有沒有優雅的解決這個問題的方法呢,今天這篇我們就來使用AOP統一對異常進行記錄以及返回。

SpringBoot引入AOP

在SpringBoot中引入AOP是一件很方便的事,和其他引入依賴一樣,我們只需要在POM中引入starter就可以了:

<!--spring切面aop依賴-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

返回體報文定義

接下來我們先想一下,一般我們返回體是什麼樣子的呢?或者你覺得一個返回的報文應該具有哪些特徵。

  • 成功標示:可以用boolean型作為標示位。

  • 錯誤程式碼:一般用整型作為標示位,羅列的越詳細,前端的容錯也就能做的更細緻。

  • 錯誤資訊:使用String作為錯誤資訊的描述,留給前端是否展示給使用者或者進入其他錯誤流程的使用。

  • 結果集:在無錯誤資訊的情況下所得到的正確資料資訊。一般是個Map,前端根據Key取值。

以上是對一個返回體報文一個粗略的定義了,如果再細緻點,可以使用簽名進行驗籤功能活著對明文資料進行對稱加密等等。這些我們今天先不討論,我們先完成一個能夠使用的介面資訊定義。

我們再對以上提到這些資訊做一個完善,去除冗餘的欄位,對差不多的型別進行合併於封裝。這樣的想法下,我們建立一個返回體報文的實體類。

public class Result<T> {

   //    error_code 狀態值:0 極為成功,其他數值代表失敗
   private Integer status;

   //    error_msg 錯誤資訊,若status為0時,為success
   private String msg;

   //    content 返回體報文的出參,使用泛型相容不同的型別
   private T data;

   public Integer getStatus() {
       return status;
   }

   public void setStatus(Integer code) {
       this.status = code;
   }

   public String getMsg() {
       return msg;
   }

   public void setMsg(String msg) {
       this.msg = msg;
   }

   public T getData(Object object) {
       return data;
   }

   public void setData(T data) {
       this.data = data;
   }

   public T getData() {
       return data;
   }

   @Override
   public String toString() {
       return "Result{" +
               "status=" + status +
               ", msg='" + msg + '\'' +
               ", data=" + data +
               '}';
   }

現在我們已經有一個返回體報文的定義了,那接下來我們可以來建立一個列舉類,來記錄一些我們已知的錯誤資訊,可以在程式碼中直接使用。

public enum ExceptionEnum {
    UNKNOW_ERROR(-1,"未知錯誤"),
    USER_NOT_FIND(-101,"使用者不存在"),
;

    private Integer code;

    private String msg;

    ExceptionEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

我們在這裡把對於不再預期內的錯誤統一設定為-1,未知錯誤。以避免返回給前端大段大段的錯誤資訊。

接下來我們只需要建立一個工具類在程式碼中使用:

public class ResultUtil {

    /**
     * 返回成功,傳入返回體具體出參
     * @param object
     * @return
     */
    public static Result success(Object object){
        Result result = new Result();
        result.setStatus(0);
        result.setMsg("success");
        result.setData(object);
        return result;
    }

    /**
     * 提供給部分不需要出參的介面
     * @return
     */
    public static Result success(){
        return success(null);
    }

    /**
     * 自定義錯誤資訊
     * @param code
     * @param msg
     * @return
     */
    public static Result error(Integer code,String msg){
        Result result = new Result();
        result.setStatus(code);
        result.setMsg(msg);
        result.setData(null);
        return result;
    }

    /**
     * 返回異常資訊,在已知的範圍內
     * @param exceptionEnum
     * @return
     */
    public static Result error(ExceptionEnum exceptionEnum){
        Result result = new Result();
        result.setStatus(exceptionEnum.getCode());
        result.setMsg(exceptionEnum.getMsg());
        result.setData(null);
        return result;
    }
}

以上我們已經可以捕獲程式碼中那些在編碼階段我們已知的錯誤了,但是卻無法捕獲程式出的未知異常資訊。我們的程式碼應該寫得漂亮一點,雖然很多時候我們會說時間太緊了,等之後我再來好好優化。可事實是,我們再也不會回來看這些程式碼了。專案總是一個接著一個,時間總是不夠用的。如果真的需要你完善重構原來的程式碼,那你一定會非常痛苦,死得相當難看。所以,在第一次構建時,就將你的程式碼寫完善了。

一般系統丟擲的錯誤是不含錯誤程式碼的,除去部分的404,400,500錯誤之外,我們如果想把錯誤程式碼定義的更細緻,就需要自己繼承RuntimeException這個類後重新定義一個構造方法來定義我們自己的錯誤資訊:

public class DescribeException extends RuntimeException{

    private Integer code;

    /**
     * 繼承exception,加入錯誤狀態值
     * @param exceptionEnum
     */
    public DescribeException(ExceptionEnum exceptionEnum) {
        super(exceptionEnum.getMsg());
        this.code = exceptionEnum.getCode();
    }

    /**
     * 自定義錯誤資訊
     * @param message
     * @param code
     */
    public DescribeException(String message, Integer code) {
        super(message);
        this.code = code;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }
}

同時,我們使用一個Handle來把Try,Catch中捕獲的錯誤進行判定,是一個我們已知的錯誤資訊,還是一個未知的錯誤資訊,如果是未知的錯誤資訊,那我們就用log記錄它,便於之後的查詢和解決:

    @ControllerAdvice
    public class ExceptionHandle {

      private final static Logger LOGGER = LoggerFactory.getLogger(ExceptionHandle.class);

      /**
       * 判斷錯誤是否是已定義的已知錯誤,不是則由未知錯誤代替,同時記錄在log中
       * @param e
       * @return
       */
      @ExceptionHandler(value = Exception.class)
      @ResponseBody
      public Result exceptionGet(Exception e){
          if(e instanceof DescribeException){
              DescribeException MyException = (DescribeException) e;
              return ResultUtil.error(MyException.getCode(),MyException.getMessage());
          }

          LOGGER.error("【系統異常】{}",e);
          return ResultUtil.error(ExceptionEnum.UNKNOW_ERROR);
      }
    }

這裡我們使用了 @ControllerAdvice ,使Spring能載入該類,同時我們將所有捕獲的異常統一返回結果Result這個實體。

此時,我們已經完成了對結果以及異常的統一返回管理,並且在出現異常時,我們可以不返回錯誤資訊給前端,而是用未知錯誤進行代替,只有檢視log我們才會知道真實的錯誤資訊。

可能有小夥伴要問了,說了這麼久,並沒有使用到AOP啊。不要著急,我們繼續完成我們剩餘的工作。

我們使用介面若出現了異常,很難知道是誰呼叫介面,是前端還是後端出現的問題導致異常的出現,那這時,AOP久發揮作用了,我們之前已經引入了AOP的依賴,現在我們編寫一個切面類,切點如何配置不需要我多說了吧:

@Aspect
@Component
public class HttpAspect {

    private final static Logger LOGGER = LoggerFactory.getLogger(HttpAspect.class);

    @Autowired
    private ExceptionHandle exceptionHandle;

    @Pointcut("execution(public * com.zzp.controller.*.*(..))")
    public void log(){

    }

    @Before("log()")
    public void doBefore(JoinPoint joinPoint){
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        //url
        LOGGER.info("url={}",request.getRequestURL());
        //method
        LOGGER.info("method={}",request.getMethod());
        //ip
        LOGGER.info("id={}",request.getRemoteAddr());
        //class_method
        LOGGER.info("class_method={}",joinPoint.getSignature().getDeclaringTypeName() + "," + joinPoint.getSignature().getName());
        //args[]
        LOGGER.info("args={}",joinPoint.getArgs());
    }

    @Around("log()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Result result = null;
        try {

        } catch (Exception e) {
            return exceptionHandle.exceptionGet(e);
        }
        if(result == null){
            return proceedingJoinPoint.proceed();
        }else {
            return result;
        }
    }

    @AfterReturning(pointcut = "log()",returning = "object")//列印輸出結果
    public void doAfterReturing(Object object){
        LOGGER.info("response={}",object.toString());
    }
}

我們使用@Aspect來宣告這是一個切面,使用@Pointcut來定義切面所需要切入的位置,這裡我們是對每一個HTTP請求都需要切入,在進入方法之前我們使用@Before記錄了呼叫的介面URL,呼叫的方法,呼叫方的IP地址以及輸入的引數等。在整個介面程式碼運作期間,我們使用@Around來捕獲異常資訊,並用之前定義好的Result進行異常的返回,最後我們使用@AfterReturning來記錄我們的出參。
以上全部,我們就完成了異常的統一管理以及切面獲取介面資訊,接下來我們心新寫一個ResultController來測試一下:

@RestController
@RequestMapping("/result")
public class ResultController {

    @Autowired
    private ExceptionHandle exceptionHandle;

    /**
     * 返回體測試
     * @param name
     * @param pwd
     * @return
     */
    @RequestMapping(value = "/getResult",method = RequestMethod.POST)
    public Result getResult(@RequestParam("name") String name, @RequestParam("pwd") String pwd){
        Result result = ResultUtil.success();
        try {
            if (name.equals("zzp")){
                result =  ResultUtil.success(new UserInfo());
            }else if (name.equals("pzz")){
                result =  ResultUtil.error(ExceptionEnum.USER_NOT_FIND);
            }else{
                int i = 1/0;
            }
        }catch (Exception e){
            result =  exceptionHandle.exceptionGet(e);
        }
        return result;
    }
}

在上面我們設計了一個controller,如果傳入的name是zzp的話,我們就返回一個使用者實體類,如果傳入的是pzz的話,我們返回一個沒有該使用者的錯誤,其他的,我們讓他丟擲一個by zero的異常。
我們用POSTMAN進行下測試:

這裡寫圖片描述

這裡寫圖片描述

我們可以看到,前端收到的返回體報文已經按我們要求同意了格式,並且在控制檯中我們打印出了呼叫該介面的一些介面資訊,我們繼續測試另外兩個會出現錯誤情況的請求:

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

我們可以看到,如是我們之前在程式碼中定義完成的錯誤資訊,我們可以直接返回錯誤碼以及錯誤資訊,如果是程式出現了我們在編碼階段不曾預想到的錯誤,則統一返回未知錯誤,並在log中記錄真實錯誤資訊。

以上就是我們統一管理結果集以及使用切面來記錄介面呼叫的一些真實情況,在平時的使用中,大家要清楚切點的優先順序以及在不同的切點位置該使用哪些註解來幫助我們完成開發,並且在切面中,如果遇到同步問題該如何解決等等。希望這篇文章能讓你更好的思考如何設計好介面,我們在實際開發中又是怎樣一步步完善我們的功能與程式碼的。也希望大家能好好梳理這些內容,如果有疑惑的地方,還請留言,我如果看到,一定會解答的。這裡預告下:下週,我們將使用ORM框架來做資料庫的互動~~~

以上所有的程式碼我已經上傳到GitHub

如果心急的小夥伴也可以去clone我已經完成的專案,這個專案中把一些常用功能都寫了,並且都寫註釋啦!!!