JSR303-参数检验
JSR303
目前最新的规范已经是 JSR380 了,也就是 Bean Validation 2.0.
参数校验是一个成熟的网站必须的功能,然而有的时候为了校验参数也要费好大的劲,免不了写很多 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
在 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 密码就没有长度的限制了。
其实还有一些校验的方法,基于方法校验,基于少量参数的校验,以后用到再来记录