項目總結63:使用Spring AOP和BindingResult實現對接口的請求數據校驗,並用@ExceptionHandler返回校驗結果
問題
合格的接口,應該在接口的內部對請求參數進行校驗,但是在接口內部通過業務代碼進行校驗,顯得十分冗余,參數越多,代碼就越混亂;
思考:可以將接口請求參數的校驗封裝成一個全局的方法,進行統一處理。
目的
使用Spring AOP 和 @ExceptionHandler 對接口的數據校驗進行全局封裝,從而做到只要在請求數據的實體類中進行注解說明,就可以進行數據校驗;
具體可以:
1- 避免在接口內部,通過代碼方式進行冗余的數據校驗;比如:if(data is empty){retur ...}
2- 可以在請求數據的實體類中進行注解說明,比如:@NotEmpty(message = "手機號不能為空");就可以進行數據校驗;
3- 可以將具體的校驗結果直接返回給前端;比如: {...,"msg": "手機號不能為空",...}
解決思路
1- 用@Valid 和 BindingResult分析請求參數校驗情況
2- 用AOP對BindingResult中的校驗結果進行處理,如果校驗出問題,拋出異常;
3- 使用@ExceptionHandler注解捕獲校驗異常,並返回給前端
具體實現方案(源碼示例)(以修改登陸密碼接口為例)
第一步:用@Valid 和 BindingResult分析請求參數校驗
具體邏輯:被@Validated的實體類,會自動根據實體類中的參數的@NotEmpty注解,進行數據校驗;並將數據校驗結果封裝到BindingResult類中;如果檢驗有問題,BindingResult數據如下
源碼如下:
1- controller層接口
import javax.validation.Valid;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
@RestController @RequestMapping(value="/api") public class ApiLoginController extends ApiBaseController { @ValidAnn //AOP切入點 @PostMapping(value="/password/update") public ResultModel<Object> updatePassword(@Valid @RequestBody UpdatePasswordReq updatePasswordReq, BindingResult bindingResult){ //1-校驗驗證碼 //2-更新密碼 return ResultUtil.success(); } }
2- 請求數據實體類
//使用了lombok @Data @AllArgsConstructor @NoArgsConstructor public class UpdatePasswordReq { @NotEmpty(message = "手機號不能為空") private String mobile; @NotEmpty(message = "新密碼1不能為空") private String newPassword; @NotEmpty(message = "新密碼2不能為空") private String newPasswordAgain; @NotEmpty(message = "驗證碼不能為空") private String code; }
3- 返回參數實體類
@Data public class ResultModel<T> { // Http網絡碼 private Integer code; // 提示消息 private String msg; // 數據 private T data; }
4- 返回結果通用類和異常枚舉
/** * 工具類 * * @author cjm * @date 2018/2/1 */ public class ResultUtil { /** * 返回異常信息 */ public static ResultModel exception(ResultEnum enums) { ResultModel model = new ResultModel(); model.setCode(enums.getCode()); model.setMsg(enums.getMessage()); return model; } /** * 返回成功信息(只包含提示語) */ public static ResultModel success() { ResultModel model = new ResultModel(); model.setCode(ResultEnum.SUCCESS.getCode()); model.setMsg(ResultEnum.SUCCESS.getMessage()); return model; } } public enum ResultEnum { SUCCESS(200, "success"), FAIL(400, "fail"), VISIT_NOT_AUTHORIZED(401, "未授權"), ARGUMENT_ERROR(402,"參數錯誤"), ARGUMENT_EXCEPTION(407, "參數存在異常"), ARGUMENT_TOKEN_EMPTY(409, "Token為空"), ARGUMENT_TOKEN_INVALID(410, "Token無效"), SERVER_ERROR(501, "服務端異常"), SERVER_SQL_ERROR(503,"數據庫操作出現異常"), SERVER_DATA_REPEAT(504, "服務器數據已存在"), SERVER_DATA_NOTEXIST(505,"數據不存在"), SERVER_DATA_STATUS_ERROR(506, "數據狀態錯誤"), SERVER_SMS_SEND_FAIL(701, "短信發送失敗"), ; private int code; private String message; private ResultEnum(int code, String message) { this.code = code; this.message = message; } public int getCode() { return code; } public String getMessage() { return message; } }
第二步:用AOP對BindingResult中的校驗結果進行處理,如果校驗出問題,拋出異常;
具體實現:AOP根據@ValidAnn定位到每一個需要數據校驗的接口,使用環繞通知,處理BindingResult類的結果,如果數據校驗有問題,將校驗結果交給ServerException,並且拋出ServerException異常
源碼如下
1- ValidAnn 注解,用戶AOP定位
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ValidAnn { }
2- 環繞AOP類,處理判斷並處理校驗結果,如果校驗出問題,則拋出ServerException;
@Aspect @Component @Order(1) public class ValidAop { /** * 所有controller方法都會執行此切面 * 用於檢驗參數 */ @Pointcut("@annotation(com.hs.annotation.ValidAnn)") public void validAop() { } /** * 對切面進行字段驗證 * * @param joinPoint 切點 * @return Object * @throws Throwable */ @Around("validAop()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { Object[] args = joinPoint.getArgs(); List<String> errorList = new ArrayList<>(); if (args != null) { Arrays.stream(args) .forEach(arg -> { if (arg instanceof BindingResult) { BindingResult result = (BindingResult) arg; if (result.hasErrors()) { result.getAllErrors() .forEach(err -> { errorList.add(err.getDefaultMessage()); }); throw new ServerException(Joiner.on(" || ").join(errorList)); } } }); } return joinPoint.proceed(); } }
3- ServerException,當校驗出問題時,拋出當前異常
public class ServerException extends RuntimeException { private Integer code; private Object Data; public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public Object getData() { return Data; } public void setData(Object data) { Data = data; } public ServerException() { super(); } public ServerException(Integer code, String message, Object data) { super(message); this.code = code; this.Data = data; } public ServerException(Integer code, String message) { super(message); this.code = code; } public ServerException(String message) { super(message); this.code = ResultEnum.ARGUMENT_ERROR.getCode(); } public ServerException(String message, Throwable cause) { super(message, cause); } public ServerException(Throwable cause) { super(cause); } protected ServerException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } }
第三步:使用@ExceptionHandler注解捕獲校驗異常,並返回給前端
具體實現:@ControllerAdvice和@ExceptionHandler攔截異常並統一處理。在GlobalExceptionHandler 類中使用@ExceptionHandler(value = ServerException.class)專門捕獲ServerException異常,並且將結果封裝到返回類中,返回給前端
源碼如下:
1- GlobalExceptionHandler 類,用於捕獲異常,並作相關處理
@ControllerAdvice //使用 @ControllerAdvice 實現全局異常處理 @ResponseBody public class GlobalExceptionHandler { protected Logger logger = Logger.getLogger(this.getClass()); /** * 自定義異常捕獲 * * @param exception 捕獲的異常 * @return 返回信息 */ @ExceptionHandler(value = ServerException.class) public ResultModel exceptionHandler(ServerException exception) { logger.warn(exception); ResultModel model = new ResultModel(); model.setCode(exception.getCode()); model.setMsg(exception.getMessage()); model.setData(exception.getData()); //3-以Json格式返回數據 return model; } }
測試結果
END