在做項目的時候需要對錶單的值進行校驗,只有校驗通過才能提交,一般來說前端和後端都需要做校驗,JSR303是做後端校驗的一種方式。
JSR303簡介
JSR是Java Specification Requests的縮寫,意思是Java 規範提案。是指向JCP(Java Community Process)提出新增一個標準化技術規範的正式請求。任何人都可以提交JSR,以向Java平臺增添新的API和服務,JSR已成爲Java界的一個重要標準。
JSR-303
是JAVA EE 6 中的一項子規範,叫做Bean Validation,Hibernate Validator 是 Bean 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);
}
}