JSR303

目前最新的规范已经是JSR380了,也就是Bean Validation 2.0.

mark

参数校验是一个成熟的网站必须的功能,然而有的时候为了校验参数也要费好大的劲,免不了写很多if-else,一点也不优雅。

上手

引入依赖

 <!--SpringBootWeb 这个包里面自带了hibernate的校验包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

JSR提供的注解

@Null   被注释的元素必须为 null    
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max=, min=) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式

Hibernate提供的

@NotBlank(message =)   验证字符串非null,且长度必须大于0    
@Email 被注释的元素必须是电子邮箱地址
@Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range(min=,max=,message=) 被注释的元素必须在合适的范围内

自定义校验注解

@IsMobile注解

package top.imlgw.spike.validator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* @author imlgw.top
* @date 2019/5/13 17:50
*/
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = {IsMobileValidator.class} //指定真正校验的类
)
public @interface IsMobile {
boolean required() default true;

String message() default "号码格式错误";

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

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

}

IsMobileValidator类

自定义的校验器要实现 ConstraintValidator接口

package top.imlgw.spike.validator;

import org.apache.commons.lang3.StringUtils;
import top.imlgw.spike.utils.ValidatorUtil;

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

/**
* @author imlgw.top
* @date 2019/5/13 18:12
*/
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {

private boolean required=false;

@Override
public void initialize(IsMobile constraintAnnotation) {
required=constraintAnnotation.required();
}

@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if(required){
return ValidatorUtil.isMobile(s);
}else {
if(StringUtils.isEmpty(s)){
return true;
}else {
return ValidatorUtil.isMobile(s);
}
}
}
}

字段上加注解

package top.imlgw.spike.entity;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotNull;
/**
* @author imlgw.top
* @date 2019/5/11 15:56
*/
public class User {
@NotNull(message = "id不能为空")
private Integer id;
@NotNull(message = "名字不能为空")
private String name;
private Integer age;
@Length(min = 6,message = "密码长度至少6位")
private String password;

@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", password='" + password + '\'' +
'}';
}

public User(Integer id, String name, Integer age, String password) {
this.name = name;
this.age = age;
this.password = password;
this.id=id;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public long getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}
}

@Valid 加BindingResult

mark

在Controlle层待校验的的参数上加上@Valid注解,然后在后面紧跟一个 BindingResult,校验的结果会封装在这个对象里面,BindingResult 的作用是当参数不合法时能够捕捉到错误,不会直接抛异常,感觉还是有点麻烦。

@Valid加全局异常捕获

上面的方法如果不加后面的BindingResult在校验失败后就会抛一个BindException

org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'user' on field 'id': rejected value [null]; codes [NotNull.user.id,NotNull.id,NotNull.long,NotNull]; arguments .......

那我们就可以利用@ControllerAdvice+@ExceptionHandler来定义一个全局的异常处理器来处理这个异常,@ExceptionHandle,针对的仅仅是单个controller,加上@ControllerAdvice就可以对所有的Controller层异常进行捕获,这里的全局仅仅指的是controller层。

自定义异常

package top.imlgw.spike.exception;
import top.imlgw.spike.result.CodeMsg;

/**
* 全局通用异常
* @author imlgw.top
* @date 2019/5/14 20:51
*/
public class GlobalException extends RuntimeException {
private static final long serialVersionUID = 1L;
private CodeMsg cm;

public GlobalException(CodeMsg cm) {
super(cm.toString());
this.cm = cm;
}

public CodeMsg getCm() {
return cm;
}

}

全局异常处理器

/**
* Controller层异常处理器
*
* @author imlgw.top
* @date 2019/5/13 18:21
*/
@ControllerAdvice
@ResponseBody //直接返回给客户端,需要json的转换
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class) //处理controller层所有异常
public Result<String> exceptionHandle(HttpServletRequest request, Exception e) {
e.printStackTrace();
if (e instanceof GlobalException) {
//全局异常
GlobalException ex = (GlobalException) e;
return Result.error(ex.getCm());
} else if (e instanceof BindException) {
//@Validated 检验器的异常
BindException ex = (BindException) e;
List<ObjectError> errors = ex.getAllErrors();
ObjectError error = errors.get(0);
String msg = error.getDefaultMessage();
return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));
} else {
return Result.error(CodeMsg.SERVER_ERROR);
}
}

//测试Demo 看先处理那个
@ExceptionHandler(value = GlobalException.class) //处理controller层所有异常
public Result<String> GLe(HttpServletRequest request, Exception e) {
System.out.println("优先处理了这个GlobalException");
e.printStackTrace();
//全局异常
GlobalException ex = (GlobalException) e;
return Result.error(ex.getCm());
}
}

这里也做了个小测试,可以看到我定义了两个@ExceptionHandle,一个是另一个的子类,看会先处理那个,测试后发现会先处理小异常,那个最大的异常其实相当于’’兜底’’的。其实后面我为了跟精细的处理,将绑定异常和自定义的异常分开处理了。

@ControllerAdvice :

It is typically used to define {@link ExceptionHandler @ExceptionHandler},

{@link InitBinder @InitBinder}, and {@link ModelAttribute @ModelAttribute}

methods that apply to all {@link RequestMapping @RequestMapping} methods.

@author Rossen Stoyanchev

@since 3.2

分组校验

只需要在vo里加上对应分组的接口然后在注解上加上就可以了

public class LoginVo {
@NotNull(message = "手机号不能为空")
@IsMobile(groups = Test1.class) //只有在Test1分组下才生效
private String mobile;

@NotNull(message = "密码不能为空")
@Length(min=6 ,groups = Test2.class,message = "密码长度过短") //只有在Test2分组下才生效
private String password;

public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "LoginVo [mobile=" + mobile + ", password=" + password + "]";
}

public interface Test1{}

public interface Test2{}
}

使用时注意用@Validated,这个其实是Spring对Hibernate的二次封装,增加了一些功能。

@RequestMapping("/jsr303-2")
@ResponseBody
public void testJSR(@Validated({LoginVo.Test2.class}) LoginVo vo){
//........
}

这样在这个Controller里post密码就没有长度的限制了。

其实还有一些校验的方法,基于方法校验,基于少量参数的校验,以后用到再来记录