目录

JSR303-参数检验

JSR303

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

http://static.imlgw.top///20190513/qa5DNu5Nsq26.png?imageslim

参数校验是一个成熟的网站必须的功能,然而有的时候为了校验参数也要费好大的劲,免不了写很多 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

http://static.imlgw.top///20190513/ILDAz6Bdj3cz.png?imageslim

在 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 密码就没有长度的限制了。

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