Spring - JSR303數據校驗

在做項目的時候需要對錶單的值進行校驗,只有校驗通過才能提交,一般來說前端和後端都需要做校驗,JSR303是做後端校驗的一種方式。

JSR303簡介

JSRJava Specification Requests的縮寫,意思是Java 規範提案。是指向JCP(Java Community Process)提出新增一個標準化技術規範的正式請求。任何人都可以提交JSR,以向Java平臺增添新的API和服務,JSR已成爲Java界的一個重要標準。
JSR-303JAVA EE 6 中的一項子規範,叫做Bean ValidationHibernate ValidatorBean Validation 的參考實現 . Hibernate Validator 提供了 JSR 303 規範中所有內置 constraint 的實現,除此之外還有一些附加的 constraint

一、Bean Validation 中內置的 constraint

在這裏插入圖片描述

二、Hibernate Validator 附加的 constraint

在這裏插入圖片描述
更多的constraint請見百度。

JSR303的使用

下文需要的基礎返回類型 - R
public class R extends HashMap<String, Object> {
	private static final long serialVersionUID = 1L;
	
	public R() {
		put("code", 0);
		put("msg", "success");
	}
	
	public static R error() {
		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知異常,請聯繫管理員");
	}
	
	public static R error(String msg) {
		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
	}
	
	public static R error(int code, String msg) {
		R r = new R();
		r.put("code", code);
		r.put("msg", msg);
		return r;
	}

	public static R ok(String msg) {
		R r = new R();
		r.put("msg", msg);
		return r;
	}
	
	public static R ok(Map<String, Object> map) {
		R r = new R();
		r.putAll(map);
		return r;
	}
	
	public static R ok() {
		return new R();
	}

	public R put(String key, Object value) {
		super.put(key, value);
		return this;
	}
}

一、常規使用

1、標註校驗註解

給Bean添加校驗註解:javax.validation.constraints,並定義自己的message提示。

// Brand對象裏的屬性
@NotBlank(message = "品牌名至少包含一個非空字符")
private String name;  // 品牌名

@NotNull(message = "必須指定品牌Id")
private Long id;      // 品牌id

@URL(message = "logo必須是一個合法的url地址")       // URL校驗
private String logo;  // 品牌logo的url

@NotEmpty
@Pattern(regexp = "^[a-zA-Z]", message = "檢索首字母必須是一個字母")   // 正則校驗
Private String firstLetter;   // 品牌首字母,如小米就是X

@NotNull
@Min(value = 0, message = "排序必須大於等於0")
private Integer sort;        // 排序字段
2、開啓校驗功能

在需要校驗的對象前添加@Valid註解開啓校驗功能,在被校驗的對象之後添加BindingResult對象可以獲取校驗結果。

// Controller層,返回的是JSON數據(即@RestController)
@RequestMapping("/save")
public R save(@Valid @RequestBody Brand brand, BindingResult result){
	if(result.hasErrors()){   // 獲取校驗的錯誤結果
		Map<String, String> resultMap = new HashMap<>();
		result.getFieldErrors().forEach((item) -> {
			// 獲取錯誤的校驗消息
			String message = item.getDefaultMessage();
			// 獲取錯誤的屬性名
			String field = item.getField();
			resultMap.put(field,message);
		});
		return R.error(400,"提交的數據不合法").put("data",map);
	}
	// 正常保存 - 調用Service層進行保存
	brandService.save(brand);
	return R.ok();
}

3、全局異常處理

上面的步驟已經完成了基本校驗,但是有點複雜,需要在Controller方法裏添加重複代碼。
錯誤需要返回一個錯誤碼及錯誤消息,因此使用一個枚舉類型來定義,然後這種方式也挺適合項目裏使用。

/***
 * 錯誤碼和錯誤信息定義類
 * 1. 錯誤碼定義規則爲5爲數字
 * 2. 前兩位表示業務場景,最後三位表示錯誤碼。例如:100001。10:通用 001:系統未知異常
 * 3. 維護錯誤碼後需要維護錯誤描述,將他們定義爲枚舉形式
 * 錯誤碼列表:
 *  10: 通用
 *      001:參數格式校驗
 *  11: 商品
 *  12: 訂單
 *  13: 購物車
 *  14: 物流
 */
public enum BizCodeEnum {
    UNKNOW_EXCEPTION(10000,"系統未知異常"),
    VAILD_EXCEPTION(10001,"參數格式校驗失敗");

    private int code;
    private String msg;
    BizCodeEnum(int code,String msg){
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

處理全局異常 - 處理校驗錯誤異常:

@Slf4j
@RestControllerAdvice(basePackages = "top.liuchengyin.product.controller")
public class ExceptionControllerAdice {
	@ExceptionHandler(value = MethodArgumentNotValidException.class)
	 public R handleVaildException(MethodArgumentNotValidException e){
        log.error("數據校驗出現問題{},異常類型:{}",e.getMessage(),e.getClass());
        // 獲取錯誤結果
        BindingResult bindingResult = e.getBindingResult();
        Map<String,String> errorMap = new HashMap<>();
        bindingResult.getFieldErrors().forEach((fieldError)->{
            errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
        });
        // 返回錯誤信息
        return R.error(BizCodeEnum.VAILD_EXCEPTION.getCode(),BizCodeEnum.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
    }
}

這樣,Controller層的代碼就變得十分簡略了:

@RequestMapping("/save")
public R save(@Valid @RequestBody Brand brand){
	brandService.save(brand);
	return R.ok();
}

二、分組校驗

分組校驗用於多場景的複雜校驗,比如說我們在做新增功能的時候,這是不需要校驗ID的(ID是自動生成的),但是做修改的時候就需要校驗ID了(沒有ID,就沒辦法判斷修改的誰)。也就是說,給校驗註解標註什麼情況需要進行校驗。

1、創建分組標識

分組標識是一個空的接口即可,只是用來標識是那種情況。這裏創建兩個標識表示:新增、修改。

// 修改分組標識
public interface UpdateGroup {

}

// 新增分組標識
public interface AddGroup {

}
2、在Bean中添加分組
// Brand對象
@NotNull(message = "修改必須指定品牌id",groups = {UpdateGroup.class})
@Null(message = "新增不能指定id",groups = {AddGroup.class})
private Long brandId;      // 品牌ID

@NotBlank(message = "品牌名必須提交",groups = {AddGroup.class,UpdateGroup.class})
private String name;	 // 品牌名

@NotBlank(groups = {AddGroup.class})
@URL(message = "logo必須是一個合法的url地址",groups={AddGroup.class,UpdateGroup.class})
private String logo;     // logo的url地址
3、在Controller方法中指定分組

注意:上面的Bean中,如果@NotBlank沒有指定分組的話,那麼在分組校驗@Validated({AddGroup.class})的情況下是不會生效的,只會在@Validated生效,因此需要指定分組。即默認沒有指定分組的校驗註解在有分組註解Vlaidated指定分組時不會生效。

@RequestMapping("/save")
public R save(@Validated({AddGroup.class}) @RequestBody Brand brand){
	brandService.save(brand);
	return R.ok();
}

@RequestMapping("/update")
public R update(@Validated(UpdateGroup.class) @RequestBody Brand brand){
	brandService.updateDetail(brand);
	return R.ok();
}

三、自定義校驗

上面的校驗可能有時候滿足不了我們的需求,因此我們也可以自己自定義一個校驗註解,編寫一個校驗器進行關聯。

1、引入依賴
<!-- 校驗 -->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>
2、Bean上加入自定義校驗註解
@ListValue(vals={0,1},groups = {AddGroup.class, UpdateGroup.class})
private Integer showStatus;
3、編寫自定義校驗註解
@Documented
// 指定自定義校驗器,可以指定多個類型的,使用逗號隔開
@Constraint(validatedBy = { ListValueConstraintValidator.class })   
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
	// 錯誤信息默認會從配置文件中獲取即下面的top.xxxx的值(自定義的)
    String message() default "{top.liuchengyin.product.valid.ListValue.message}";

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

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

    int[] vals() default { };   // 對應註解中的vals
}
4、編寫自定義校驗器

自定義校驗器,需要實現ConstraintValidator接口。

// ConstraintValidator<校驗註解,類型>
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
    private Set<Integer> set = new HashSet<>();
    // 初始化方法
    @Override
    public void initialize(ListValue constraintAnnotation) {
        // 取出註解中的值,放入Set
        int[] vals = constraintAnnotation.vals();
        for (int val : vals) {
            set.add(val);
        }

    }
    
    /**
     * 判斷是否校驗成功
     * @param value 需要校驗的值
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return set.contains(value);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章