天天看點

Spring Boot 統一RESTful接口響應和統一異常處理

一、簡介

基于Spring Boot 架構開發的應用程式,大部分都是以提供RESTful接口為主要的目的。前端或者移動端開發人員通過調用後端提供的RESTful接口完成資料的交換。

統一的RESTful接口響應資料結構是基本的開發規範。能夠減少團隊内部不必要的溝通;減輕接口消費者校驗資料的負擔;降低其他同僚接手代碼的難度;提高接口的健壯性和可擴充性。

常見的統一響應資料結構如下所示:

public class GlobalResponseEntity<T>{
    private Boolean success = true;
    private String code = "000000";
    private String message = "request successfully";
    private T data;
 }
           

統一的異常處理,是系統完備性的基本象征。通過對全局異常資訊的捕獲,能夠避免将異常資訊和系統敏感資訊直接抛給用戶端;針對特定類型異常捕獲之後可以重新對輸出資料做編排,提高互動友好度,同時可以記錄異常資訊以便監控和分析。

一般,在統一異常處理處會手動修改傳回給用戶端的http狀态碼,并編排響應給用戶端的資料結構為GlobalResponseEntity,保證始終統一響應。

二、如何實作

使用RestControllerAdvice注解(或者ControllerAdvice注解)結合ResponseBodyAdvice接口

RestControllerAdvice注解導入了ControllerAdvice注解

@ControllerAdvice是在類上聲明的注解,其用法主要有三點:

  • 和@ExceptionHandler注解配合使用,@ExceptionHandler标注的方法可以捕獲Controller中抛出的的異常,進而達到異常統一處理的目的
  • 和@InitBinder注解配合使用,@InitBinder标注的方法可在請求中注冊自定義參數的解析器,進而達到自定義請求參數格式化的目的
  • 和@ModelAttribute注解配合使用,@ModelAttribute标注的方法會在執行目标Controller方法之前執行,可在入參上增加自定義資訊

實作ResponseBodyAdvice接口來自定義響應給前端的内容

用法舉例:

// 這裡@RestControllerAdvice等同于@ControllerAdvice + @ResponseBody
@RestControllerAdvice
public class GlobalHandler {
    private final Logger logger = LoggerFactory.getLogger(GlobalHandler.class);
    // 這裡@ModelAttribute("loginUserInfo")标注的modelAttribute()方法表示會在Controller方法之前
    // 執行,傳回目前登入使用者的UserDetails對象
    @ModelAttribute("loginUserInfo")
    public UserDetails modelAttribute() {
        return (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
    // @InitBinder标注的initBinder()方法表示注冊一個Date類型的類型轉換器,用于将類似這樣的2019-06-10
    // 日期格式的字元串轉換成Date對象
    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    } 
    // 這裡表示Controller抛出的MethodArgumentNotValidException異常由這個方法處理
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result exceptionHandler(MethodArgumentNotValidException e) {
        Result result = new Result(BizExceptionEnum.INVALID_REQ_PARAM.getErrorCode(),
                BizExceptionEnum.INVALID_REQ_PARAM.getErrorMsg());
        logger.error("req params error", e);
        return result;
    }
    // 這裡表示Controller抛出的BizException異常由這個方法處理
    @ExceptionHandler(BizException.class)
    public Result exceptionHandler(BizException e) {
        BizExceptionEnum exceptionEnum = e.getBizExceptionEnum();
        Result result = new Result(exceptionEnum.getErrorCode(), exceptionEnum.getErrorMsg());
        logger.error("business error", e);
        return result;
    }
    // 這裡就是通用的異常處理器了,所有預料之外的Exception異常都由這裡處理
    @ExceptionHandler(Exception.class)
    public Result exceptionHandler(Exception e) {
        Result result = new Result(1000, "網絡繁忙,請稍後再試");
        logger.error("application error", e);
        return result;
    }

}
           

在Controller裡取出@ModelAttribute标注的方法傳回的UserDetails對象。(這裡隻是用UserDetails舉例,實際開發過程中,建議按照spring security的做法,将使用者資訊存放到spring上下文中,然後在controller層進行消費)

當入參為examOpDate=2019-06-10時,Spring會使用我們上面@InitBinder注冊的時間類型轉換器将2019-06-10轉換examOpDate對象

RestController
@RequestMapping("/json/exam")
@Validated
public class ExamController {
    @Autowired
    private IExamService examService;
    // ......
    @PostMapping("/getExamListByOpInfo")
    public Result<List<GetExamListResVo>> getExamListByOpInfo( @NotNull Date examOpDate,
                                                              @ModelAttribute("loginUserInfo") UserDetails userDetails) {
        List<GetExamListResVo> resVos = examService.getExamListByOpInfo(examOpDate, userDetails);
        Result<List<GetExamListResVo>> result = new Result(resVos);
        return result;
    }

}
           

@ExceptionHandler标注的多個方法分别表示隻處理特定的異常。這裡需要注意的是當Controller抛出的某個異常多個@ExceptionHandler标注的方法都适用時,Spring會選擇最具體的異常處理方法來處理,也就是說@ExceptionHandler(Exception.class)這個标注的方法優先級最低,隻有當其它方法都不适用時,才會來到這裡處理。

這裡僅列舉了RestControllerAdvice簡單用法,為了學習RestControllerAdvice注解使用

三、統一的響應處理

工程目錄結構如下:

Spring Boot 統一RESTful接口響應和統一異常處理

GlobalResponse是一個處理器類(handle),用來處理統一響應。

  • GlobalResponse類需要實作ResponseBodyAdvice接口
  • 重寫supports方法,可對響應進行過濾。實際開發中不一定所有的方法傳回值都是相同的模闆,這裡可以根據MethodParameter進行過濾,此方法傳回true則會走過濾,即會調用beforeBodyWrite方法,否則不會調用。
  • 重寫beforeBodyWrite方法,編寫具體的響應資料邏輯

代碼如下:

package com.naylor.globalresponsebody.handler.response;

import com.alibaba.fastjson.JSON;
import com.naylor.globalresponsebody.handler.GlobalResponseEntity;
import org.springframework.core.MethodParameter;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
 * @BelongsProject: debris-app
 * @BelongsPackage: com.naylor.globalresponsebody.response
 * @Author: Chenml
 * @CreateTime: 2020-09-02 15:26
 * @Description: 全局響應
 */

@RestControllerAdvice("com.naylor")
public class GlobalResponse implements ResponseBodyAdvice<Object> {

    /**
     * 攔截之前業務處理,請求先到supports再到beforeBodyWrite
     * <p>
     * 用法1:自定義是否攔截。若方法名稱(或者其他次元的資訊)在指定的常量範圍之内,則不攔截。
     *
     * @param methodParameter
     * @param aClass
     * @return 傳回true會執行攔截;傳回false不執行攔截
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        //TODO 過濾
        return true;
    }

    /**
     * 向用戶端傳回響應資訊之前的業務邏輯處理
     * <p>
     * 用法1:無論controller傳回什麼類型的資料,在寫入用戶端響應之前統一包裝,用戶端永遠接收到的是約定格式的内容
     * <p>
     * 用法2:在寫入用戶端響應之前統一加密
     *
     * @param responseObject     響應内容
     * @param methodParameter
     * @param mediaType
     * @param aClass
     * @param serverHttpRequest
     * @param serverHttpResponse
     * @return
     */
    @Override
    public Object beforeBodyWrite(Object responseObject, MethodParameter methodParameter,
                                  MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> aClass,
                                  ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        //responseObject是否為null
        if (null == responseObject) {
            return new GlobalResponseEntity<>("55555", "response is empty.");
        }
        //responseObject是否是檔案
        if (responseObject instanceof Resource) {
            return responseObject;
        }
        //該方法傳回值類型是否是void
        //if ("void".equals(methodParameter.getParameterType().getName())) {
        //  return new GlobalResponseEntity<>("55555", "response is empty.");
        //}
        if (methodParameter.getMethod().getReturnType().isAssignableFrom(Void.TYPE)) {
            return new GlobalResponseEntity<>("55555", "response is empty.");
        }
        //該方法傳回值類型是否是GlobalResponseEntity。若是直接傳回,無需再包裝一層
        if (responseObject instanceof GlobalResponseEntity) {
            return responseObject;
        }
        //處理string類型的傳回值
        //當傳回類型是String時,用的是StringHttpMessageConverter轉換器,無法轉換為Json格式
        //必須在方法體上标注RequestMapping(produces = "application/json; charset=UTF-8")
        if (responseObject instanceof String) {
            String responseString = JSON.toJSONString(new GlobalResponseEntity<>(responseObject));
            return responseString;
        }
        //該方法傳回的媒體類型是否是application/json。若不是,直接傳回響應内容
        if (!mediaType.includes(MediaType.APPLICATION_JSON)) {
            return responseObject;
        }

        return new GlobalResponseEntity<>(responseObject);
    }
}

           

GlobalResponseEntity是一個實體類,用來封裝統一響應和統一異常處理的傳回值模闆

  • GlobalResponseEntity類為一個泛型類,T為接口具體的傳回資料
  • success表示接口響應是否成功,一般的,這個是業務叫法,和http狀态碼無關
  • code表示接口響應狀态碼,可以根據特定業務場景自己定義
  • message是描述資訊
  • 實際開發中code和mesage的具體值可以用枚舉來維護

    具體代碼如下:

@Data
@Accessors(chain = true)
public class GlobalResponseEntity<T> implements Serializable {

    private Boolean success = true;
    private String code = "000000";
    private String message = "request successfully";
    private T data;

    public GlobalResponseEntity() {
        super();
    }

    public GlobalResponseEntity(T data) {
        this.data = data;
    }

    public GlobalResponseEntity(String code, String message) {
        this.code = code;
        this.message = message;
        this.data = null;
    }

    public GlobalResponseEntity(String code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public GlobalResponseEntity(Boolean success, String code, String message) {
        this.success = success;
        this.code = code;
        this.message = message;
    }

    public GlobalResponseEntity(Boolean success, String code, String message, T data) {
        this.success = success;
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public static GlobalResponseEntity<?> badRequest(String code, String message) {
        return new GlobalResponseEntity<>(false, code, message);
    }

    public static GlobalResponseEntity<?> badRequest() {
        return new GlobalResponseEntity<>(false, "404", "無法找到您請求的資源");
    }

}

           

四、統一的異常處理

新增GlobalException類,編寫統一異常處理。類上面添加

@RestControllerAdvice("com.naylor")和

@ResponseBody注解,ResponseBody用來對響應内容進行編排,如http狀态碼。代碼如下:

@RestControllerAdvice("com.naylor")
@ResponseBody
@Slf4j
public class GlobalException {

    /**
     * 捕獲一般異常
     * 捕獲未知異常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleException(Exception e) {
        return new ResponseEntity<>(
                new GlobalResponseEntity<>(false, "555",
                        e.getMessage() == null ? "未知異常" : e.getMessage()),
                HttpStatus.INTERNAL_SERVER_ERROR);
    }

    /**
     * 處理404異常
     *
     * @return
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException e) {
        return new ResponseEntity<>(
                new GlobalResponseEntity(false, "4040",
                        e.getMessage() == null ? "請求的資源不存在" : e.getMessage()),
                HttpStatus.NOT_FOUND);
    }

    /**
     * 捕獲運作時異常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<Object> handleRuntimeException(RuntimeException e) {
        log.error("handleRuntimeException:", e);
        return new ResponseEntity<>(
                new GlobalResponseEntity(false, "rt555",
                        e.getMessage() == null ? "運作時異常" : e.getMessage().replace("java.lang.RuntimeException: ", "")),
                HttpStatus.INTERNAL_SERVER_ERROR);
    }


    /**
     * 捕獲業務異常
     * 捕獲自定義異常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(BizServiceException.class)
    public ResponseEntity<Object> handleBizServiceException(BizServiceException e) {
        return new ResponseEntity<>(
                new GlobalResponseEntity(false, e.getErrorCode(), e.getMessage()),
                HttpStatus.INTERNAL_SERVER_ERROR);
    }


    /**
     * 捕獲參數校驗異常
     * javax.validation.constraints
     *
     * @param e
     * @return
     */
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public ResponseEntity<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        String msg = "參數校驗失敗";
        List<FieldFailedValidate> fieldFailedValidates = this.extractFailedMessage(e.getBindingResult().getFieldErrors());
        if (null != fieldFailedValidates && fieldFailedValidates.size() > 0) {
            msg = fieldFailedValidates.get(0).getMessage();
        }
        return new ResponseEntity<>(
                new GlobalResponseEntity<>(false, "arg555", msg, null),
                HttpStatus.BAD_REQUEST);
    }

    /**
     * 組裝validate錯誤資訊
     *
     * @param fieldErrors
     * @return
     */
    private List<FieldFailedValidate> extractFailedMessage(List<FieldError> fieldErrors) {
        List<FieldFailedValidate> fieldFailedValidates = new ArrayList<>();
        if (null != fieldErrors && fieldErrors.size() > 0) {
            FieldFailedValidate fieldFailedValidate = null;
            for (FieldError fieldError : fieldErrors) {
                fieldFailedValidate = new FieldFailedValidate();
                fieldFailedValidate.setMessage(fieldError.getDefaultMessage());
                fieldFailedValidate.setName(fieldError.getField());

                fieldFailedValidates.add(fieldFailedValidate);
            }
        }

        return fieldFailedValidates;
    }
}

           

五、解決無法捕獲404異常的問題

五、解決因增加了ResponseBodyAdvice導緻Swagger2-UI無法通路的問題

報錯提示:

Unable to infer base url. 
This is common when using dynamic servlet registration or when the API is behind an API Gateway. 
The base url is the root of where all the swagger resources are served. 
For e.g. if the api is available at http://example.org/api/v2/api-docs 
then the base url is http://example.org/api/. Please enter the location manually:
           

原因:swagger相當于是寄宿在應用程式中的一個web服務,統一響應處理器攔截了應用所有的響應,對swagger-ui的響應産生了影響。

解決方案:修改統一響應處理器攔截的範圍,配置包路徑。

@RestControllerAdvice(value={"com.naylor","org.spring"})
public class GlobalResponseHandler implements ResponseBodyAdvice<Object> {


//......

}


           

六、解決RestControllerAdvice優先級問題

若在項目中寫了好幾個處理器類,都添加了@RestControllerAdvice的注解,由于加載存在先後順序,可能會導緻部分攔截器沒有按照既定的方式工作,甚至出現一些奇奇怪怪的問題,此時可以在标注了RestControllerAdvice的類上增加@Order注解,來指定加載順序。

引用

@RestControllerAdvice詳解: https://zhuanlan.zhihu.com/p/73087879

@ResponseBodyAdvice詳解:https://my.oschina.net/diamondfsd/blog/3069546/print

郵箱:[email protected]

技術交流QQ群:1158377441

繼續閱讀