【全網最全】springboot整合JSR303引數校驗與全域性例外處理

2022-09-22 12:02:09

一、前言

我們在日常開發中,避不開的就是引數校驗,有人說前端不是會在表單中進行校驗的嗎?在後端中,我們可以直接不管前端怎麼樣判斷過濾,我們後端都需要進行再次判斷,為了安全。因為前端很容易拜託,當測試使用PostMan來測試,如果後端沒有校驗,不就亂了嗎?肯定會有很多異常的。今天小編和大家一起學習一下JSR303專門用於引數校驗的,算是一個工具吧!

二、JSR303簡介

JSR-303 是 JAVA EE 6 中的一項子規範,叫做 Bean Validation,官方參考實現是Hibernate Validator。
Hibernate Validator 提供了 JSR 303 規範中所有內建 constraint 的實現,除此之外還有一些附加的 constraint。

Hibernate官網

官網介紹:

驗證資料是一項常見任務,它發生在從表示層到持久層的所有應用程式層中。通常在每一層都實現相同的驗證邏輯,這既耗時又容易出錯。為了避免重複這些驗證,開發人員經常將驗證邏輯直接捆綁到域模型中,將域類與驗證程式碼混在一起,而驗證程式碼實際上是關於類本身的後設資料。

Jakarta Bean Validation 2.0 - 為實體和方法驗證定義了後設資料模型和 API。預設後設資料源是註釋,能夠通過使用 XML 覆蓋和擴充套件後設資料。API 不依賴於特定的應用程式層或程式設計模型。它特別不依賴於 Web 或持久層,並且可用於伺服器端應用程式程式設計以及富使用者端 Swing 應用程式開發人員。

三、匯入依賴

<dependency>
   <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

四、常用註解

約束註解名稱 約束註解說明
@Null 用於驗證物件為null
@NotNull 用於物件不能為null,無法查檢長度為0的字串
@NotBlank 只用於String型別上,不能為null且trim()之後的size>0
@NotEmpty 用於集合類、String類不能為null,且size>0。但是帶有空格的字串校驗不出來
@Size 用於物件(Array,Collection,Map,String)長度是否在給定的範圍之內
@Length 用於String物件的大小必須在指定的範圍內
@Pattern 用於String物件是否符合正規表示式的規則
@Email 用於String物件是否符合郵箱格式
@Min 用於Number和String物件是否大等於指定的值
@Max 用於Number和String物件是否小等於指定的值
@AssertTrue 用於Boolean物件是否為true
@AssertFalse 用於Boolean物件是否為false

所有的大家參考jar包

五、@Validated、@Valid區別

@Validated:

  • Spring提供的
  • 支援分組校驗
  • 可以用在型別、方法和方法引數上。但是不能用在成員屬性(欄位)上
  • 由於無法加在成員屬性(欄位)上,所以無法單獨完成級聯校驗,需要配合@Valid

@Valid:

  • JDK提供的(標準JSR-303規範)
  • 不支援分組校驗
  • 可以用在方法、建構函式、方法引數和成員屬性(欄位)上
  • 可以加在成員屬性(欄位)上,能夠獨自完成級聯校驗

總結:@Validated用到分組時使用,一個學校物件裡還有很多個學生物件需要使用@Validated在Controller方法引數前加上,@Valid加在學校中的學生屬性上,不加則無法對學生物件裡的屬性進行校驗!

區別參考部落格地址

例子:

@Data
public class School{

    @NotBlank
    private String id;
    private String name;
    @Valid                // 需要加上,否則不會驗證student類中的校驗註解
    @NotNull 			  // 且需要觸發該欄位的驗證才會進行巢狀驗證。
    private List<Student> list;
}

@Data
public class Student {

    @NotBlank
    private String id;
    private String name;
    private int age;
    
}

@PostMapping("/test")
public Result test(@Validated @RequestBody School school){

}

六、常用使用測試

1. 實體類新增校驗

import lombok.Data;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.io.Serializable;

@Data
public class BrandEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 品牌id
	 */
	@NotNull(message = "修改必須有品牌id")
	private Long brandId;
	/**
	 * 品牌名F
	 */
	@NotBlank(message = "品牌名必須提交")
	private String name;
	/**
	 * 品牌logo地址
	 */
	@NotBlank(message = "地址必須不為空")
	private String logo;
	/**
	 * 介紹
	 */
	private String descript;
	
	/**
	 * 檢索首字母
	 */
	//正規表示式
	@Pattern(regexp = "^[a-zA-Z]$",message = "檢索的首字母必須是字母")
	private String firstLetter;
	/**
	 * 排序
	 */
	@Min(value = 0,message = "排序必須大於等於0")
	private Integer sort;

}

2. 統一返回型別

import com.alibaba.druid.util.StringUtils;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

//統一返回結果
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel
public class Result<T> {
    @ApiModelProperty("響應碼")
    private Integer code;
    @ApiModelProperty("相應資訊")
    private String msg;
    @ApiModelProperty("返回物件或者集合")
    private T data;

    //成功碼
    public static final Integer SUCCESS_CODE = 200;
    //成功訊息
    public static final String SUCCESS_MSG = "SUCCESS";

    //失敗
    public static final Integer ERROR_CODE = 201;
    public static final String ERROR_MSG = "系統異常,請聯絡管理員";
    //沒有許可權的響應碼
    public static final Integer NO_AUTH_COOD = 999;

    //執行成功
    public static <T> Result<T> success(T data){
        return new Result<>(SUCCESS_CODE,SUCCESS_MSG,data);
    }
    //執行失敗
    public static <T> Result failed(String msg){
        msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg;
        return new Result(ERROR_CODE,msg,"");
    }
    //傳入錯誤碼的方法
    public static <T> Result failed(int code,String msg){
        msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg;
        return new Result(code,msg,"");
    }
    //傳入錯誤碼的資料
    public static <T> Result failed(int code,String msg,T data){
        msg = StringUtils.isEmpty(msg)? ERROR_MSG : msg;
        return new Result(code,msg,data);
    }
}

3. 測試類

@PostMapping("/add")
public Result add(@Valid @RequestBody BrandEntity brandEntity)  {

    return Result.success("成功");
}

遇到的坑:小編在公司的專案中新增沒什麼問題,但是就是無法觸發校驗,看到的是Springboot版本太高了,所有要新增下面的依賴才觸發。

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.18.Final</version>
</dependency>

4. 普通測試結果

5. 我們把異常返回給頁面

@PostMapping("/add")
public Result add(@Valid @RequestBody BrandEntity brandEntity, BindingResult bindingResult){

    if (bindingResult.hasErrors()){
        Map<String,String> map = new HashMap<>();
        bindingResult.getFieldErrors().forEach(item ->{
            map.put(item.getField(),item.getDefaultMessage());
        });
        return Result.failed(400,"提交的資料不合規範",map);
    }
    
    return Result.success("成功");
}

6. 例外處理結果

{
    "code": 400,
    "data": {
        "name": "品牌名必須提交",
        "logo": "地址必須不為空"
    },
    "msg": "提交的資料不合規範"
}

七、抽離全域性例外處理

1. 心得體會

上面我們要在每個校驗的介面上面寫,所以我們要抽離出來做個全域性異常。並且要改進一下,原來的是把錯誤資訊放到data裡,但是正常情況下的data是返回給前端的資料。我們這樣把異常資料放進去,會使data的資料有二義性。這樣對於前端就不知道里面是資料還是報錯資訊了哈,這樣就可以直接前端展示msg裡面的提示即可!

2. 書寫ExceptionControllerAdvice

import com.wang.test.demo.response.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice(basePackages = "com.wang.test.demo.controller")
public class ExceptionControllerAdvice {

    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result handleVaildException(MethodArgumentNotValidException e){

        log.error("資料校驗出現問題:{},異常型別:{}",e.getMessage(),e.getClass());
        BindingResult bindingResult = e.getBindingResult();
        StringBuffer stringBuffer = new StringBuffer();
        bindingResult.getFieldErrors().forEach(item ->{
            //獲取錯誤資訊
            String message = item.getDefaultMessage();
            //獲取錯誤的屬性名字
            String field = item.getField();
            stringBuffer.append(field + ":" + message + " ");
        });
        return Result.failed(400, stringBuffer + "");

    }

    @ExceptionHandler(value = Throwable.class)
    public Result handleException(Throwable throwable){

        log.error("錯誤",throwable);
        return Result.failed(400, "系統異常");
    }
}

3. 測試結果

{
    "code": 400,
    "data": "",
    "msg": "logo:地址必須不為空 name:品牌名必須提交 "
}

八、分組校驗

1. 需求

我們在做校驗的時候,通常會遇到一個實體類的新增和修改,他們的校驗規則是不同的,所以分組顯得尤為重要。他可以幫助我們少建一個冗餘的實體類,所以我們必須要會的。

2. 建立分組介面(不需寫任何內容)

public interface EditGroup {
}
public interface AddGroup {
}

3. 在需要二義性的欄位上新增分組

/**
 * 品牌id
 */
@NotNull(message = "修改必須有品牌id",groups = {EditGroup.class})
@Null(message = "新增不能指定id",groups = {AddGroup.class})
private Long brandId;
// 其餘屬性我們不變

4. 不同Controller新增校驗規則

注意:我們要進行分組,所以@Valid不能使用了,要使用@Validated。相信大家已經看到上面的他倆區別了哈!

@PostMapping("/add")
public Result add(@Validated({AddGroup.class}) @RequestBody BrandEntity brandEntity){

    return Result.success("成功");
}

@PostMapping("/edit")
public Result edit(@Validated({EditGroup.class}) @RequestBody BrandEntity brandEntity){

    return Result.success("成功");
}

5. 測試


九、自定義校驗

1.定義自定義校驗器

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;

//編寫自定義的校驗器
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {

    private Set<Integer> set=new HashSet<Integer>();

    //初始化方法
    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] value = constraintAnnotation.vals();
        for (int i : value) {
            set.add(i);
        }
    }
    /**
     * 判斷是否校驗成功
     * @param value  需要校驗的值
     * @param context
     * @return
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return  set.contains(value);
    }
}

2. 定義一個註解配合校驗器使用

@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
    // 使用該屬性去Validation.properties中取
    String message() default "{com.atguigu.common.valid.ListValue.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    int[] vals() default {};

}

3. 實體類新增一個新的校驗屬性

注意:我們上面做了分組,如果屬性不指定分組,則不會生效,現在我們的部分屬性校驗已沒有起作用,現在只有brandId和showStatus起作用。

/**
 * 顯示狀態[0-不顯示;1-顯示]
 */
@NotNull(groups = {AddGroup.class, EditGroup.class})
@ListValue(vals = {0,1},groups = {AddGroup.class, EditGroup.class},message = "必須為0或者1")
private Integer showStatus;

4. 測試


十、總結

這樣就差不多對JSR303有了基本瞭解,滿足基本開發沒有什麼問題哈!看到這裡了,收藏點贊一波吧,整理了將近一天!!謝謝大家了!!


歡迎大家關注小編的微信公眾號!!

有緣人才能看到,自己網站,歡迎存取!!!

點選存取!歡迎存取,裡面也是有很多好的文章哦!