在做项目的时候需要对表单的值进行校验,只有校验通过才能提交,一般来说前端和后端都需要做校验,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);
}
}