java参数校验

Validator 结合 @InitBinder 注解

https://blog.csdn.net/qq_40813329/article/details/121969898

1 实现一个Validator

这种方法不需要我们的bean里面有任何注解之类的东西

实现Validator 
重写两个方法

@Component
public class UserValidator implements Validator {
    @Override
    public boolean supports(Class clazz) {
        return User2.class.equals(clazz);
    }
    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "name", "name.empty");
        User2 p = (User2) target;
        if (p.getId() == 0) {
            errors.rejectValue("id", "can not be zero");
        }
    }
}

2 修改Controller代码

@InitBinder 注解添加在initBinder 方法
initBinder 方法 binder 加入 validator
注入上面的UserValidator实例
并给Controller的方法参数加上@Validated注解

@RestController
public class UserController {
    @Autowired
    UserValidator validator;
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.setValidator(validator);
    }
    @RequestMapping(value = "/user/post", method = RequestMethod.POST)
    public ServiceResponse handValidatePost(@Validated @RequestBody User user) {
        ServiceResponse serviceResponse = new ServiceResponse();
        serviceResponse.setCode(0);
        serviceResponse.setMessage("test");
        return serviceResponse;
    }
}

注解校验

1 对于简单类型参数(非Bean)直接在参数前,使用注解添加约束规则。比如 @NotNull @Length 等
2 在类名前追加 @Validated 注解,否则添加的约束规则不生效
3 方法被调用时,如果传入的实际参数与约束规则不符,会直接抛出 ConstraintViolationException ,表明参数校验失败
4 对于Bean类型的参数,在Bean内部的各个字段上面追加约束注解,然后在方法的参数前面添加 @Valid 注解即可。
5 对于Bean里面套Bean的,同样在外层Bean里面写@Valid即可

public class CreateProjectReqVO extends BaseVO {
    /**
     * 请求序列号
     */
    @NotNull(message = "请求序列号不可为空")
    private Integer requestNo;
    /**
     * 项目名称
     */
    @NotNull(message = "项目名称不可为空")
    private String projectName;
    ……
}    
 注意 参数前面的 @Valid 注解
 public CreateProjectRespVO createProject(@Valid CreateProjectReqVO reqVO) {
 ……

常用校验注解

@AssertTrue / @AssertFalse 
验证适用字段:boolean
注解说明:验证值是否为true / false

@DecimalMax / @DecimalMin
验证适用字段:BigDecimal,BigInteger,String,byte,short,int,long
注解说明:验证值是否小于或者等于指定的小数值,要注意小数存在精度问题

@Digits
验证适用字段:BigDecimal,BigInteger,String,byte,short,int,long
注解说明:验证值的数字构成是否合法
属性说明:integer:指定整数部分的数字的位数。fraction: 指定小数部分的数字的位数。

@Future / @Past
验证适用字段:Date,Calendar
注解说明:验证值是否在当前时间之后 / 之前
属性说明:公共

@Max / @Min
验证适用字段:BigDecimal,BigInteger,String,byte,short,int,long
注解说明:验证值是否小于或者等于指定的整数值
属性说明:公共
注意事项:建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单提交的值为“”时无法转换为int

@NotNull / @Null
验证适用字段:引用数据类型
注解说明:验证值是否为非空 / 空
属性说明:公共

@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格. 
@NotEmpty 检查约束元素是否为Null或者是EMPTY.
@NotBlank@NotEmpty 的区别:空格(" ")对于 NotEmpty 是合法的,而 NotBlank 会抛出校验异常

@Pattern
验证适用字段:String
注解说明:验证值是否配备正则表达式
属性说明:regexp:正则表达式flags: 指定Pattern.Flag 的数组,表示正则表达式的相关选项。

@Size
验证适用字段:String,Collection,Map,数组
注解说明:验证值是否满足长度要求
属性说明:max:指定最大长度,min:指定最小长度。

@Length(min=, max=):专门应用于String类型

@Valid
验证适用字段:递归的对关联对象进行校验
注解说明:如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验(是否进行递归验证)
属性说明:无

@Range(min=, max=) 被指定的元素必须在合适的范围内 

@CreditCardNumber信用卡验证 

@Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。 

@URL(protocol=,host=, port=,regexp=, flags=)

注意

1 约束用的注解,一般需要带上message参数方便自定义错误信息,比如前面例子中的“@NotNull(message = “项目名称不可为空”)”。而这个message参数是支持EL表达式的

@NotNull(message = "${member.id.null}") 

再定义一个比如叫做 messages.properties 的配置文件来统一管理错误信息

member.id.null=用户编号不能为空

2 约束规则支持正则表达式
3 注解不能(只)放在实现类上

public interface IProjectService {
    /**
     * 项目创建
     */
    CreateProjectRespVO createProject(CreateProjectReqVO reqVO);
}


@Service
public class ProjectServiceImpl implements IProjectService {

    /**
     * 项目创建
     */
    @Override
    public CreateProjectRespVO createProject(@Valid CreateProjectReqVO reqVO) {
    ……
 }

在进行校验时会发生“javax.validation.ConstraintDeclarationException”异常(注意跟校验不通过发生的异常不是一个)

解决方法:

@Override父类/接口的方法,入参约束只能写在父类/接口上面。
或者两边都写上也可(但是这样维护时容易出问题,不推荐)。
另外 @Validated 这个注解写在哪边都可以。

效验实验

使用spring validation完成数据后端校验

public class Foo {

    @NotBlank
    private String name;

    @Min(18)
    private Integer age;

    @Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$",message = "手机号码格式错误")
    @NotBlank(message = "手机号码不能为空")
    private String phone;

    @Email(message = "邮箱格式错误")
    private String email;

    //... getter setter

}
@Controller
public class FooController {

    @RequestMapping("/foo")
    public String foo(@Validated Foo foo <1>, BindingResult bindingResult <2>) {
        if(bindingResult.hasErrors()){
            for (FieldError fieldError : bindingResult.getFieldErrors()) {
                //...
            }
            return "fail";
        }
        return "success";
    } 
}
<1> 参数Foo前需要加上 @Validated 注解,表明需要spring对其进行校验,而校验的信息会存放到其后的BindingResult中。
	注意,必须相邻,如果有多个参数需要校验,形式可以如下
	foo(@Validated Foo foo, BindingResult fooBindingResult ,@Validated Bar bar, BindingResult barBindingResult);
	即一个校验类对应一个校验结果。

<2> 校验结果会被自动填充,在controller中可以根据业务逻辑来决定具体的操作,如跳转到错误页面。

访问 http://localhost:8080/foo?name=xujingfeng&email=000&age=19 可以得到如下的debug信息:
在这里插入图片描述

当发生多个错误,spring validation不会在第一个错误发生后立即停止,而是继续试错,告诉我们所有的错误

分组效验初步- Not注解 中 groups 属性

Not注解

作用

groups验证分组,比如我有的验证只有更新的时候做有的只有添加的时候做,就用这个

javax.validate里有提供一个默认分组Default.class是个接口不指定分组时都会执行这个

分组 class 是个空的接口

在字段上面指定他们的分组

@NotNull(message = "primary is not null",groups = {GroupInterface1.class})
private Long id;

public @interface GroupInterface1(){}

此时controller应该要加上@Valid , 否则不会验证

@RequestMapping(value = "/test")
public void test(@Validated(GroupInterface1.class) User user) {
 
}

基本写法

Class Foo{
    //只有在Adult分组下,18岁的限制才会起作用
    @Min(value = 18,groups = {Adult.class})
    private Integer age;

    public interface Adult{}

    public interface Minor{}
} 
@RequestMapping("/drink")
public String drink(@Validated({Foo.Adult.class}) Foo foo, BindingResult bindingResult) {
    if(bindingResult.hasErrors()){
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            //...
        }
        return "fail";
    }
    return "success";
}

@RequestMapping("/live")
public String live(@Validated Foo foo, BindingResult bindingResult) {
    if(bindingResult.hasErrors()){
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            //...
        }
        return "fail";
    }
    return "success";
} 

drink方法限定需要进行Adult校验,而live方法则不做限制

使用JSR提供的@GroupSequence注解控制校验顺序

上面是Hibernate Validation提供的能力,而不是JSR标准提供的。@GroupSequence 它是JSR标准提供的注解(只是没有provider强大而已,但也有很适合它的使用场景)

// Defines group sequence.  定义组序列(序列:顺序执行的)
@Target({ TYPE })
@Retention(RUNTIME)
@Documented
public @interface GroupSequence {
    Class<?>[] value();
}

顾名思义,它表示Group组序列。默认情况下,不同组别的约束验证是无序的,在某些情况下,约束验证的顺序是非常的重要的,比如如下两个场景:

  • 第二个组的约束验证依赖于 第一个约束执行完成的结果必须第一个约束正确了,第二个约束执行才有意义
  • 某个Group组的校验非常耗时,并且会消耗比较大的CPU/内存。那么我们的做法应该是把这种校验放到最后,所以对顺序提出了要求

一个组可以定义为其他组的序列,使用它进行验证的时候必须符合该序列规定的顺序。在使用组序列验证的时候,如果序列前边的组验证失败,则后面的组将不再给予验证

public class User {

    @NotEmpty(message = "firstname may be empty")
    private String firstname;
    @NotEmpty(message = "middlename may be empty", groups = Default.class)
    private String middlename;
    @NotEmpty(message = "lastname may be empty", groups = GroupA.class)
    private String lastname;
    @NotEmpty(message = "country may be empty", groups = GroupB.class)
    private String country;


    public interface GroupA {
    }
    public interface GroupB {
    }
    // 组序列
    @GroupSequence({Default.class, GroupA.class, GroupB.class})
    public interface Group {
    }
}

测试一

public static void main(String[] args)  {
    User user = new User();
    // 此处指定了校验组是:User.Group.class
    Set<ConstraintViolation<User>> result = Validation.buildDefaultValidatorFactory().getValidator().validate(user, User.Group.class);

    // 对结果进行遍历输出
    result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
}
middlename middlename may be empty: null
firstname firstname may be empty: null

只有Default这个Group的校验了,序列上其它组并没有执行校验

测试二

        User user = new User();
        user.setFirstname("f");
        user.setMiddlename("s");
lastname lastname may be empty: null

Default组都校验通过后,执行了GroupA组的校验。
但GroupA组校验木有通过,GroupB组的校验也就不执行了
@GroupSequence提供的组序列顺序执行以及短路能力,在很多场景下是非常非常好用的

注意
顺序只能控制在分组级别无法控制约束注解级别。因为一个类内的约束(同一分组内),它的顺序是<Set> metaConstraints来保证的,所以可以认为同一分组内的校验器木有执行的先后顺序的(不管是类、属性、方法、构造器…)

校验分组处理器 @DefaultGroupSequenceProvider

DefaultGroupSequenceProvider用来做多字段联合校验

某个嘉年华俱乐部门票对不同人群存在不同的定价

年龄必须大于 12 岁;
12 ~ 18 岁半价;
18 岁以上全价;
coser 则可以免费;

换句话来说,任何年龄大于 12 岁的人都可以购买全价,对于半价票需要校验年龄是否满足未成年,对于免费票需要校验是否是 coser。

@Getter
@Setter
@GroupSequenceProvider(Person.FareTypeGroupSequenceProvider.class)
public class Person {

    /**
	 * 年龄
     */
    @Min(value = 12, message = "年龄最小不低于 12 岁")
    @Max(value = 18, message = "年龄大于 18 岁需要购买全价门票", groups=WhenIsHalf.class)
    private Integer age;

    /**
     * 票价[0=免费,1=半价,2=全价]
     */
    @NotNull(message = "票价类型不能为空")
    private Integer fareType;

    /**
     * 是否是 coser
     */
    @AssertTrue(message = "只有 coser 才可以免费进去", groups = WhenIsFree.class)
    private Boolean coser;

    public interface WhenIsFree {}

    public interface WhenIsHalf {}

    /**
     * 校验分组处理器
     */
    public static class FareTypeGroupSequenceProvider implements DefaultGroupSequenceProvider<Person> {


        @Override
        public List<Class<?>> getValidationGroups(Person person) {
            ArrayList<Class<?>> list = Lists.newArrayList();
            list.add(Person.class);
            // 判空
            if (Objects.nonNull(person)) {
                // 当 fareType = 0 时
                if (Objects.equals(person.getFareType(), 0)) {
                	list.add(WhenIsFree.class);
                } else (Objects.equals(person.getFareType(), 1)) {
                	list.add(WhenIsHalf.class);
                }
            }
            return list;
        }
    }
}

分开写

```java
@Getter
@Setter
@GroupSequenceProvider(FareTypeGroupSequenceProvider.class)
public class Person {

    /**
	 * 年龄
     */
    @Min(value = 12, message = "年龄最小不低于 12 岁")
    @Max(value = 18, message = "年龄大于 18 岁需要购买全价门票", groups=WhenIsHalf.class)
    private Integer age;

    /**
     * 票价[0=免费,1=半价,2=全价]
     */
    @NotNull(message = "票价类型不能为空")
    private Integer fareType;

    /**
     * 是否是 coser
     */
    @AssertTrue(message = "只有 coser 才可以免费进去", groups = WhenIsFree.class)
    private Boolean coser;

    public interface WhenIsFree {}

    public interface WhenIsHalf {}
}
/**
 * 校验分组处理器
 */
public class FareTypeGroupSequenceProvider implements DefaultGroupSequenceProvider<Person> {

     @Override
     public List<Class<?>> getValidationGroups(Person person) {
         ArrayList<Class<?>> list = Lists.newArrayList();
         list.add(Person.class);
         // 判空
         if (Objects.nonNull(person)) {
             // 当 fareType = 0 时
             if (Objects.equals(person.getFareType(), 0)) {
             	list.add(WhenIsFree.class);
             } else (Objects.equals(person.getFareType(), 1)) {
             	list.add(WhenIsHalf.class);
             }
         }
         return list;
     }
}

注意

DefaultGroupSequenceProvider 类型是泛型类,必须指定为需要参数校验的 Bean 类型;
DefaultGroupSequenceProvider.getValidationGroups 方法返回的 list 中必须包含 Bean 类型;
DefaultGroupSequenceProvider 只能增强 Default.class 分组;

自定义效验

添加一个“字符串不能包含空格”的限制

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {CannotHaveBlankValidator.class})<1>
public @interface CannotHaveBlank {

    //默认错误消息
    String message() default "不能包含空格";

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

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

    //指定多个时使用
    @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        CannotHaveBlank[] value();
    }

}

我们不需要关注太多东西,使用spring validation的原则便是便捷我们的开发,例如payload,List ,groups,都可以忽略。

<1> 自定义注解中指定了这个注解真正的验证者类

public class CannotHaveBlankValidator implements <1> ConstraintValidator<CannotHaveBlank, String> {

    @Override
    public void initialize(CannotHaveBlank constraintAnnotation) {
    }


    @Override
    public boolean isValid(String value, ConstraintValidatorContext context <2>) {
        //null时不进行校验
        if (value != null && value.contains(" ")) {
            <3>
            //获取默认提示信息
            String defaultConstraintMessageTemplate = context.getDefaultConstraintMessageTemplate();
            System.out.println("default message :" + defaultConstraintMessageTemplate);
            //禁用默认提示信息
            context.disableDefaultConstraintViolation();
            //设置提示语
            context.buildConstraintViolationWithTemplate("can not contains blank").addConstraintViolation();
            return false;
        }
        return true;
    }
}

也可以合并起来写

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {CannotHaveBlank.CannotHaveBlankValidator.class})<1>
public @interface CannotHaveBlank {

    //默认错误消息
    String message() default "不能包含空格";

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

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

    class CannotHaveBlankValidator implements <1> ConstraintValidator<CannotHaveBlank, String> {

	    @Override
	    public void initialize(CannotHaveBlank constraintAnnotation) {
	    }
	
	    @Override
	    public boolean isValid(String value, ConstraintValidatorContext context <2>) {
	        ...
	    }
	}  
} 
public class User{

	@CannotHaveBlank(groups=ValidCannotHaveBlank.class)
	private String name;
	
	public interface ValidCannotHaveBlank {
    }
}

自定义效验的验证者编写规则

所有的验证者都需要实现ConstraintValidator接口,它的接口也很形象,包含一个初始化事件方法,和一个判断是否合法的方法

public interface ConstraintValidator<A extends Annotation, T> {

    void initialize(A constraintAnnotation);

    boolean isValid(T value, ConstraintValidatorContext context);
}

<2> ConstraintValidatorContext 这个上下文 包含了认证中所有的信息,我们可以利用这个上下文实现获取默认错误提示信息禁用错误提示信息改写错误提示信息等操作

<3> 一些典型校验操作,或许可以对你产生启示作用。

值得注意的一点是,自定义注解 可以用在METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER之上,ConstraintValidator的第二个泛型参数T,是需要被校验的类型

@DefaultGroupSequenceProvider + 自定义效验

解决多字段联合逻辑校验问题【享学Spring MVC】

见 百度网盘 AopDemo
在这里插入图片描述

问题

@NotNull @NotBlank 注解无效

1 外部调用类没用@Valid 或@Validated注解(比如controller方法参数中没加
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述2 尝试hibnerate的包

@javax.validation.constraints.NotBlank
尝试下面
@org.hibernate.validator.constraints.NotBlank

注意

接口要效验的参数前 @Valid

注解效验的类 使用时 controller 接口,要效验的参数前 @Valid
@PostMapping("/register")
public String handlePostRequest(@RequestBody @Valid UserInfo user, BindingResult bindingResult) {
	...
}

controller上加@Validated 就能报错

@Slf4j
@RestController
@Api(description = "用户管理")
@Validated
@RequestMapping("user")
public class UserController { 

    @PostMapping("/register")
    public String handlePostRequest(@RequestBody @Valid UserInfo user, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "register fail";
        }
        log.info("user: {}", user.toString());
        return "register success";
    }

}

效验的执行顺序

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值