Spring Boot + validation + AOP 請求參數校驗
一、validation 校驗註解
通用
@Null
被註釋的屬性必須爲 null@NotNull
被註釋的屬性必須不爲 null@AssertTrue
被註釋的屬性必須爲 true@AssertFalse
被註釋的屬性必須爲 false
字符串/數組/集合檢查:(字符串本身就是個數組)
@Pattern(regexp="reg")
驗證字符串滿足正則@Size(max, min)
驗證字符串、數組、集合長度範圍@NotEmpty
驗證註解的元素值不爲null且不爲空(字符串長度不爲0、集合大小不爲0)@NotBlank
驗證註解的元素值不爲空(不爲null、去除首位空格後長度爲0),不同於@NotEmpty,@NotBlank只應用於字符串且在比較時會去除字符串的空格
數值檢查:同時能驗證一個字符串是否是滿足限制的數字的字符串
@Min(value)
被註釋的屬性必須是一個數字,其值必須大於等於指定的最小值@Max(value)
被註釋的屬性必須是一個數字,其值必須小於等於指定的最大值@DecimalMin(value)
被註釋的屬性必須是一個數字,其值必須大於等於指定的最小值@DecimalMax(value)
被註釋的屬性必須是一個數字,其值必須小於等於指定的最大值@Size(max=, min=)
被註釋的屬性的大小必須在指定的範圍內@Digits(integer, fraction)
被註釋的屬性必須是一個數字,其值必須在可接受的範圍內@Pattern(regex=,flag=)
被註釋的屬性必須符合指定的正則表達式
日期檢查:Date/Calendar
@Past
限定一個日期,日期必須是過去的日期@Future
限定一個日期,日期必須是未來的日期
Hibernate Validator 附加的 constraint
@Email
被註釋的屬性必須是電子郵箱地址@Length(min=,max=)
被註釋的字符串的大小必須在指定的範圍內@Range(min=,max=,message=)
被註釋的屬性必須在合適的範圍內
二、依賴
<!-- 包含有 validation 依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- aop 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
三、代碼實例
1.SpmkApproveSummaryDto.java
用 validation 校驗
import java.util.List;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import cn.hutool.json.JSONObject;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 審批匯總
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SpmkApproveSummaryDto{
@NotBlank(message = "title爲空")
@ApiModelProperty(value = "標題 ", example = "標題", required = true)
private String title;
@NotBlank(message = "approveName爲空")
@ApiModelProperty(value = "審批名稱 ", example = "審批名稱", required = true)
private String approveName;
@NotBlank(message = "initiator爲空")
@ApiModelProperty(value = "發起人名稱 ", example = "發起人名稱", required = true)
private String initiator;
@NotNull(message = "assoType爲空")
@DecimalMax(value = "9",message = "assoType 只能爲 0無 1轉正 2離職 3調崗 4加班 5請假 6出差 7外出 8補卡 9調薪")
@DecimalMin(value = "0",message = "assoType 只能爲 0無 1轉正 2離職 3調崗 4加班 5請假 6出差 7外出 8補卡 9調薪")
@ApiModelProperty(value = "關聯類型 0無 1轉正 2離職 3調崗 4加班 5請假 6出差 7外出 8補卡 9調薪", example = "1")
private Integer assoType;
@NotNull(message = "requestData爲空")
@ApiModelProperty(value = "申請數據 ", example = "申請數據", required = true)
private JSONObject requestData;
@NotNull(message = "froms爲空")
@ApiModelProperty(value = "審批表單 ", example = "數組", required = true)
private List<JSONObject> froms;
@NotNull(message = "router爲空")
@ApiModelProperty(value = "審批流程", example = "審批流程", required = true)
private Router router;
}
2.ValidationError.java
校驗錯誤提示類
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ValidationError {
private String field;
private String msg;
}
3.controller類
/**
* 發起審批
* @throws Exception
*/
@PostMapping(value = "/start_approval")
public Result<Object> saveCa(@Valid @RequestBody SpmkApproveSummaryDto spmkApproveSummaryDto,BindingResult bindingResult) throws Exception{
// 判斷校驗結果 bindingResult,hasErrors 爲 true 則解析 返回給前端 校驗錯誤提示信息
if(bindingResult != null && bindingResult.hasErrors()){
List<ObjectError> ls = bindingResult.getAllErrors();
// 創建List 存放 校驗錯誤提示信息
List<ValidationError> listVe = new ArrayList<ValidationError>();
ValidationError ve;
for (ObjectError one : ls) {
String fieldString = one.getCodes().length >= 1 ? one.getCodes()[0] : "";
if (fieldString != null) {
fieldString = fieldString.substring(fieldString.lastIndexOf(".") + 1);
}
ve = ValidationError.builder().field(fieldString).msg(one.getDefaultMessage()).build();
listVe.add(ve);
}
// 返回 校驗校驗錯誤提示信息
return ResultUtil.error(listVe);
}
// 業務邏輯省略
// ...
}
3.1.調試
3.2.校驗失敗 返回數據
{
"code": "400",
"data": [
{
"field": "assoType",
"msg": "assoType爲空"
},
{
"field": "title",
"msg": "title爲空"
}
],
"message": "參數錯誤",
"result": true,
"timestamp": 1588867983720
}
- @Valid和BindingResult配套使用,@Valid用在參數前,BindingResult作爲校驗結果綁定返回
- 後端通過對 BindingResult 校驗結果 的 再次解析和封裝爲 ValidationError列表 再返回給 客戶端
- 如果不設置 BindingResult 參數, 校驗失敗後 後端會直接 返回BindingResult校驗結果,錯誤提示 有冗餘
4.不設置 BindingResult 參數
/**
* 發起審批
* @throws Exception
*/
@PostMapping(value = "/start_approval")
public Result<Object> saveCa(@Valid @RequestBody SpmkApproveSummaryDto spmkApproveSummaryDto) throws Exception{
// ...
// 業務邏輯省略
// ...
}
4.1.調試
4.2.校驗失敗 返回數據
{
"timestamp": "2020-05-08 00:18:13",
"status": 400,
"error": "Bad Request",
"errors": [
{
"arguments": [
{
"code": "title",
"codes": [
"spmkApproveSummaryDto.title",
"title"
],
"defaultMessage": "title"
}
],
"bindingFailure": false,
"code": "NotBlank",
"codes": [
"NotBlank.spmkApproveSummaryDto.title",
"NotBlank.title",
"NotBlank.java.lang.String",
"NotBlank"
],
"defaultMessage": "title爲空",
"field": "title",
"objectName": "spmkApproveSummaryDto"
},
{
"arguments": [
{
"code": "assoType",
"codes": [
"spmkApproveSummaryDto.assoType",
"assoType"
],
"defaultMessage": "assoType"
}
],
"bindingFailure": false,
"code": "NotNull",
"codes": [
"NotNull.spmkApproveSummaryDto.assoType",
"NotNull.assoType",
"NotNull.java.lang.Integer",
"NotNull"
],
"defaultMessage": "assoType爲空",
"field": "assoType",
"objectName": "spmkApproveSummaryDto"
}
],
"message": "Validation failed for object='spmkApproveSummaryDto'. Error count: 2",
"path": "/spmk/start_approval"
}
四、AOP切面 處理 BindingResult 校驗結果
1.BindingResultAspect.java
(定義切面類,加上@Component @Aspect這兩個註解)
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.ObjectError;
import com.alibaba.fastjson.JSONObject;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Console;
import cn.hutool.core.util.ClassUtil;
import cn.timer.api.aspect.lang.annotation.BindingResultCtrol;
import cn.timer.api.aspect.lang.bean.ValidationError;
import cn.timer.api.utils.ResultUtil;
/**
* 校驗信息返回
*
* @author TZQ
*/
@Aspect
@Component
public class BindingResultAspect {
// execution 配置織入點 -匹配 cn.timer.api.controller 包下的所有子包的類的方法
@Pointcut("execution(* cn.timer.api.controller.*.*.*(..))")
public void clazzPointCut(){
}
@Around("clazzPointCut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Long startTime = System.currentTimeMillis();
Object retVal;
Object[] objs = joinPoint.getArgs();
List<Object> listObj = CollectionUtil.toList(objs);
// 從 joinPoint 中 取得 BeanPropertyBindingResult的對象 (相當與上面的 bindingResult)
BeanPropertyBindingResult optional = (BeanPropertyBindingResult)listObj.stream()
.filter(p -> "BeanPropertyBindingResult".equals(ClassUtil.getClassName(p, true)))
.findFirst()
.orElse(null);
// 把 controller 方法裏 對bindingResult的解析邏輯 遷移到這來做統一處理,這樣每個被 切點 clazzPointCut() 匹配的方法都會做 一下處理
if(optional != null && optional.hasErrors()){
List<ObjectError> ls = optional.getAllErrors();
List<ValidationError> listVe = new ArrayList<ValidationError>();
ValidationError ve;
for (ObjectError one : ls) {
String fieldString = one.getCodes().length >= 1 ? one.getCodes()[0] : "";
if (fieldString != null) {
fieldString = fieldString.substring(fieldString.lastIndexOf(".") + 1);
}
ve = ValidationError.builder().field(fieldString).msg(one.getDefaultMessage()).build();
listVe.add(ve);
}
retVal = ResultUtil.error(listVe);
}else {
retVal = joinPoint.proceed(joinPoint.getArgs());
}
Console.log("返回內容 {}: " ,JSONObject.toJSONString(retVal));
Long endtime = System.currentTimeMillis();
Console.log("執行耗時爲{}:" ,endtime-startTime + "ms");
return retVal;
}
}
2.簡要說明
-
@Aspect
(切面)
通常是一個類,裏面可以定義切入點和通知 -
@Pointcut
(切點)
配置切入點Pointcut的幾種方式: 例-
當切入點爲所有的public方法時: execution(public**(…))
-
當切入點爲所有的set開始方法時:execution(set(…))
-
當切入點爲xxx類種所有的方法時:execution(xxx(…))
-
當切入點爲com.abc包下所有的方法時:execution(*com.abc…(…))
-
當切入點爲com.abc包及其子包下面所有的方法時:execution(*com.abc…(…))
-
-
@Befor
(前置通知)
在方法調用之前 調用 -
@After
(後置通知)
在方法返回之後調用 -
@Afterthrow
在方法出現異常後調用 -
@Around
環繞通知
3.效果一樣 返回校驗錯誤提示
感謝
感謝您的閱讀,有收穫?希望兄弟姐妹三叔六嬸大姨大媽阿公阿婆來個三連擊,給更多的同學看到 這篇文章,感謝
你的每一次回眸都是永恆,每一次鼓勵都是我前進的動力,每一次分享都是對我的認可。