JSR 303 進行后台數據校驗


一、JSR 303

1、什么是 JSR 303?

  JSR 是 Java Specification Requests 的縮寫,即 Java 規范提案。
  存在各種各樣的 JSR,簡單的理解為 JSR 是一種 Java 標准。
  JSR 303 就是數據檢驗的一個標准(Bean Validation (JSR 303))。
參考:
  https://www.jianshu.com/p/554533f88370

2、為什么使用 JSR 303?

  處理一段業務邏輯,首先要確保數據輸入的正確性,所以需要先對數據進行檢查,保證數據在語義上的正確性,再根據數據進行下一步的處理。
  前端可以通過 js 程序校驗數據是否合法,后端同樣也需要進行校驗。而后端最簡單的實現就是直接在業務方法中對數據進行處理,但是不同的業務方法可能會出現同樣的校驗操作,這樣就出現了數據的冗余。為了解決這個情況,JSR 303 出現了。
  JSR 303 使用 Bean Validation,即在 Bean 上添加相應的注解,去實現數據校驗。這樣在執行業務方法前,都會根據注解對數據進行校驗,從而減少自定義的校驗邏輯,減少代碼冗余。

3、JSR 303 常見操作?

(1)可以通過簡單的注解校驗 Bean 屬性,比如 @NotNull、@Null 等。
(2)可以通過 Group 分組自定義需要執行校驗的屬性。
(3)可以自定義注解並指定校驗規則。
(4)支持基於 JSR 303 的實現,比如 Hibernate Validator(額外添加一些注解)。

 

二、演示 JSR303 的簡單使用

1、構建一個 SpringBoot 項目用來演示

(1)構建一個 SpringBoot 項目,以及使用 EasyCode 逆向生成相關的代碼。
參考地址:
  https://www.cnblogs.com/l-y-h/p/12781586.html
模板代碼地址:
  https://gitee.com/lyh-man/fast-template.git

(2)工具使用詳情:
  SpringBoot 2.2.6 + JDK 1.8 + mysql 1.8 搭建基本開發環境
  IDEA + EasyCode + Lombok 插件 逆向生成基本代碼
  Postman 發送請求,測試接口

2、未使用 JSR303 相關注解時

  沒用 JSR 303 相關注解時,需要手動在業務方法里寫處理數據的邏輯。
  修改 Controller ,簡單測試一下未使用 JSR 303 相關注解時的做法。

@RestController
@RequestMapping("api")
public class EmpController {
    @Resource
    private EmpService empService;

    @PostMapping("/emp")
    public Result createEmp(@RequestBody Emp emp) {
        if (emp.getId() == null || emp.getName() == null) {
            return Result.error().message("數據不存在");
        }
        return Result.ok().data("items", emp).message("數據插入成功");
    }
}

 

 

 

使用 postman 測試該接口,當 id 不存在時,會被檢測到。

 

 

 

id,name 都存在時,不會被捕獲。

 

 

 

  這里只是簡單的測試一下邏輯,真實的數據檢測肯定比這復雜的多,然后每個方法都需要寫不同的數據處理邏輯,這樣就會造成數據的冗余。而使用 JSR303 的相關注解,就很簡單,繼續往下看。

 

3、使用 JSR 303 相關注解處理邏輯

(1)使用步驟:
Step1:
  在相關的 Bean 上標注需要處理的注解,並指定需要提示的信息(若不指定,會從默認配置文件中讀取默認的信息)。

Step2:
  在相關的方法上,使用 @Valid 注解(或者 @Validated 指定組名)標記需要被校驗的數據,否則會不生效。
注意:
  檢測到數據異常后,系統會向外拋出異常,如果做了統一異常處理,可以根據 postman 測試的結果,找到控制台打印出的 相應的異常,並處理。

Step3:
  處理異常。使用 BindingResult 可以獲取到檢測結果,然后進行處理。
  也可以使用 全局統一異常 處理(@RestControllerAdvice 與 @ExceptionHandler),處理檢測結果。
注:
  統一異常處理參考:https://www.cnblogs.com/l-y-h/p/12781586.html#_label2

 

(2)使用:
Step1:
  在相關的 Bean 上標注注解,並寫上指定信息。

import lombok.Data;

import javax.validation.constraints.NotNull;
import java.io.Serializable;

@Data
public class Emp implements Serializable {
    private static final long serialVersionUID = 281903912367009575L;

    @NotNull(message = "id 不能為 null")
    private Integer id;

    @NotNull(message = "name 不能為 null")
    private String name;
    
    private Double salary;
    
    private Integer age;
    
    private String email;
}

 

 

Step2:
  修改 Controller 方法,使用 @Valid 注解標記需要檢測的數據。

@RestController
@RequestMapping("api")
public class EmpController {
    @Resource
    private EmpService empService;

    @PostMapping("/emp")
    public Result createEmp(@Valid @RequestBody Emp emp) {
        return Result.ok().data("items", emp).message("數據插入成功");
    }
}

 

 

Step3:
  使用 postman 測試一下。會拋出 MethodArgumentNotValidException 異常。

 

 

控制台打印的信息:

 

 

Step4:
  可以使用 BindingResult 去處理捕獲到的數據並進行相關處理。

@RestController
@RequestMapping("api")
public class EmpController {
    @Resource
    private EmpService empService;

    @PostMapping("/emp")
    public Result createEmp(@Valid @RequestBody Emp emp, BindingResult result) {
        if (result.hasErrors()) {
            Map<String, String> map = new HashMap<>();
            // 獲取校驗結果,遍歷獲取捕獲到的每個校驗結果
            result.getFieldErrors().forEach(item ->{
                // 獲取校驗的信息
                String message = item.getDefaultMessage();
                String field = item.getField();
                // 存儲得到的校驗結果
                map.put(field, message);
            });
            return Result.error().message("數據不合法").data("items", map);
        }
        return Result.ok().data("items", emp).message("數據插入成功");
    }
}

 

 

使用 Postman 測試。

 

 

Step5:
  通過上面的步驟,已經可以捕獲異常、處理異常,但是每次都是在業務方法中手動處理邏輯,這樣的實現,代碼肯定會冗余。可以將其抽出,使用 統一異常處理,每次異常發生時,將其捕獲。
  根據 Step3 可以看到會拋出 MethodArgumentNotValidException 異常,所以需要將其捕獲。
  需要使用 @RestControllerAdvice 與 @ExceptionHandler。

@RestControllerAdvice
public class GlobalExceptionHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handlerValidException(MethodArgumentNotValidException e) {
        logger.error(e.getMessage(), e);
        BindingResult result = e.getBindingResult();
        Map<String, String> map = new HashMap<>();
        // 獲取校驗結果,遍歷獲取捕獲到的每個校驗結果
        result.getFieldErrors().forEach(item ->{
            // 存儲得到的校驗結果
            map.put(item.getField(), item.getDefaultMessage());
        });
        return Result.error().message("數據校驗不合法").data("items", map);
    }
}

 

 

相應的業務方法里,不需要再用 BindingResult 去處理數據了(即 Step2 的狀態)。

@RestController
@RequestMapping("api")
public class EmpController {
    @Resource
    private EmpService empService;

    @PostMapping("/emp")
    public Result createEmp(@Valid @RequestBody Emp emp) {
        return Result.ok().data("items", emp).message("數據插入成功");
    }
}

 

 

使用 Postman 測試。

 

 

4、JSR 303 分組校驗

(1)為什么使用 分組校驗?
  通過上面的過程,可以了解到單個方法的校驗規則。
  如果出現多個方法,都需要校驗 Bean,且校驗規則不同的時候,怎么辦呢?
  分組校驗就可以去解決該問題,每個分組指定不同的校驗規則,不同的方法執行不同的分組,就可以得到不同的校驗結果。

(2)基本認識
  JSR 303 的每個注解都默認具備三個屬性:
    message 用來定義數據校驗失敗后的提示消息,默認讀取配置文件的內容。
      全局搜索 ValidationMessages.properties,可以看到默認的信息。

    groups 用來定義分組,其是一個 class 數組,可以指定多個分組。

String message() default "{javax.validation.constraints.NotNull.message}";

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

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

 

(3)使用分組步驟:
Step1:
  定義一個空接口,用於指定分組,內部不需要任何實現。

Step2:
  指定 注解時,通過 groups 指定分組。用於指定在某個分組條件下,才去執行校驗規則。

Step3:
  在相關的業務方法上,通過 @Validated 注解指定分組,去指定校驗。
注:
  使用分組校驗后,Bean 注解上若不指定分組,則不會執行校驗規則。

(4)使用:
Step1:
  創建分組接口。
  創建兩個分組接口 AddGroup、UpdateGroup。
其中:
  AddGroup 用於指定 添加數據 時的校驗規則(比如:id、name 均不為 null)。
  UpdateGroup 用於指定 修改數據 時的校驗規則(比如:name 不允許為 null)。

 

 

Step2:
  給 Bean 添加注解,並指定分組信息。

@Data
public class Emp implements Serializable {
    private static final long serialVersionUID = 281903912367009575L;

    @NotNull(message = "id 不能為 null", groups = {AddGroup.class})
    private Integer id;

    @NotNull(message = "name 不能為 null", groups = {AddGroup.class, UpdateGroup.class})
    private String name;

    private Double salary;

    private Integer age;

    private String email;
}

 

 

Step3:
  在業務方法上,通過 @Validated 注解指定分組,去指定校驗。
如下例,定義兩個方法,Post 請求會觸發 createEmp 方法,Put 請求會觸發 UpdateEmp 方法。

@RestController
@RequestMapping("api")
public class EmpController {
    @Resource
    private EmpService empService;

    @PostMapping("/emp")
    public Result createEmp(@Validated({AddGroup.class}) @RequestBody Emp emp) {
        return Result.ok().data("items", emp).message("數據插入成功");
    }

    @PutMapping("/emp")
    public Result UpdateEmp(@Validated({UpdateGroup.class}) @RequestBody Emp emp) {
        return Result.ok().data("items", emp).message("數據插入成功");
    }
}

 

 

Step4:
  使用 Postman 測試,發送 Post 請求,觸發 createEmp 方法,執行 AddGroup 校驗規則。
  檢測 id、name 是否合法。

 

 

發送 Put 請求,觸發 UpdateEmp 方法,執行 UpdateGroup 校驗規則。
只檢測 name 是否合法。

 

 

5、JSR 303 自定義校驗注解

(1)為什么使用自定義校驗注解?
  上面的注解滿足不了業務需求時,可以自定義校驗注解,自定義校驗規則。

(2)步驟:
Step1:
  需要自定義一個校驗注解。
  可以創建一個 ValidationMessages.properties 用於保存默認的 message 信息。

Step2:
  需要自定義一個校驗器,即自定義校驗規則。
  實現 ConstraintValidator 接口,並重寫相關方法。
注:
  initialize 方法用於初始化,可以獲取 自定義的屬性的值。
  isValid 方法用於校驗,可以獲取到實際的值,然后與自定義的屬性值進行比較。

Step3:
  將校驗注解 與 校驗器 關聯起來。
  @Constraint(validatedBy = {TestValidConstraintValidator.class})

(3)使用:
  如下例,自定義一個校驗規則,判斷數據長度是否合法。
  默認為 String 屬性,當 String 為 Null 或者 長度大於 5 時,校驗不通過。
  可以自定義 長度。
Step1:
  自定義一個校驗注解,@TestValid,用於判斷一個值的長度是否合法。

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 用於判斷一個值的長度是否合法
 */
@Target({FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {TestValidConstraintValidator.class})
public @interface TestValid {
    String message() default "{com.lyh.test.TestValid.message}";

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

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

    /**
     * 返回一個長度
     * @return 默認為 5
     */
    int length() default 5;
}

 

 

配置文件內容:

 

 

Step2:
  自定義一個校驗器TestValidConstraintValidator, 用於檢測值是否合法。

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
 * 實現 ConstraintValidator 接口,
 * 其中 ConstraintValidator 的泛型,一個需要指定自定義的注解,一個需要指定需要獲取的值的類型。
 * 比如:
 *  ConstraintValidator<TestValid, String> 中
 *      TestValid   表示自定義注解
 *      String      表示獲取的值的類型
 * 即定義規則,判斷一個 String 的值的長度是否滿足條件
 */
public class TestValidConstraintValidator implements ConstraintValidator<TestValid, String> {

    /**
     * 用於保存自定義的(默認)長度
     */
    private int length;

    /**
     * 初始化方法,獲取默認數據
     * @param constraintAnnotation 注解對象
     */
    @Override
    public void initialize(TestValid constraintAnnotation) {
        length = constraintAnnotation.length();
    }

    /**
     * 自定義校驗規則,如果 String 為 Null 或者 長度大於 5,則校驗失敗(返回 false)
     * @param value 需要校驗的值
     * @param context
     * @return true 表示校驗成功,false 表示校驗失敗
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value == null ? false : length > value.length();
    }

}

 

 

Step3:
  使用注解。

@Data
public class Emp implements Serializable {
    private static final long serialVersionUID = 281903912367009575L;

    @NotNull(message = "id 不能為 null", groups = {AddGroup.class})
    private Integer id;

    @TestValid(groups = {AddGroup.class})
    @NotNull(message = "name 不能為 null", groups = {AddGroup.class, UpdateGroup.class})
    private String name;

    private Double salary;

    private Integer age;

    @TestValid(length = 10, message = "值不能為 Null 且長度不超過 10", groups = {AddGroup.class})
    private String email;
}

 

 

使用 Postman 測試。
  name、email 都不存在時,會被捕獲數據異常。

 

 

name 數據不合法、email 數據合法時,name 會被捕獲。

 

 

三、JSR 303 相關注解

1、空檢查相關注解

注解                      注解詳情
@Null                  被指定的注解元素必須為 Null
@NotNull               任意類型,不能為 Null,但可以為空,比如空數組、空字符串。
@NotBlank              針對字符串,不能為 Null,且去除前后空格后的字符串長度要大於 0。
@NotEmpty              針對字符串、集合、數組,不能為 Null,且長度要大於 0。

 

 

 

 

 

 

 

 

2、長度檢查

注解                      注解詳情
@Size                   針對字符串、集合、數組,判斷長度是否在給定范圍內。
@Length                 針對字符串,判斷長度是否在給定范圍內。

 

 

 

 

3、布爾值檢查

注解                      注解詳情
@AssertTrue             針對布爾值,用來判斷布爾值是否為 true
@AssertFalse            針對布爾值,用來判斷布爾值是否為 false

 

 

 

 

4、日期檢查

注解                      注解詳情
@Past                  針對日期,用來判斷當前日期是否為 過去的日期
@Future                針對日期,用來判斷當前日期是否為 未來的日期

 

5、數值檢查

注解                      注解詳情
@Max(value)         針對字符串、數值,用來判斷是否小於等於某個指定值
@Min(value)         針對字符串、數值,用來判斷是否大於等於某個指定值

 

 

 

 

6、其他

注解                      注解詳情
@Pattern             驗證字符串是否滿足正則表達式
@Email               驗證字符串是否滿足郵件格式
@Url                 驗證是否滿足 url 格式

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM