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 这个注解写在哪边都可以。
效验实验
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 属性
作用
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 + 自定义效验
见 百度网盘 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";
}
}