文章目录
constraint: /kənˈstreɪnt/ : 约束;局促,态度不自然;强制
一.使用hibernate-validator校验框架必要性
数据的校验是网站一个不可或缺的功能,前端的js校验可以涵盖大部分的校验职责,如用户名唯一性,生日格式,邮箱格式校验等等常用的校验。但是为了避免用户绕过浏览器,使用http工具直接向后端请求一些违法数据,服务端的数据校验也是必要的。
hibernate-validator的作用
- 验证逻辑与业务逻辑之间进行了分离,降低了程序耦合度;
- 统一且规范的验证方式,无需你再次编写重复的验证代码;
- 你将更专注于你的业务,将这些繁琐的事情统统丢在一边。
简述JSR303、JSR-349、Hibernate-validation、Spring-validation之间的关系。
- JSR-303 是
JAVA 6
中的一项子规范,叫做Bean Validation
,用于对 Java Bean 中的字段的值进行验证
。官方参考实现是Hibernate Validator。 - JSR-349是JSR303的升级版本,添加了一些新特性,他们规定一些校验规范即校验注解,如
@Null,@NotNull,@Pattern
,他们位于javax.validation.constraints包
下,只提供规范不提供实现。 - Hibernate-validation是对 JSR 303 规范的实现(不要将Hibernate-validation和数据库orm框架联系在一起),并增加了一些其他校验注解,如
@Email,@Length,@Range
等等,他们位于org.hibernate.validator.constraints包
下。 - Spring为了给开发者提供便捷,
对Hibernate-validation
进行了二次封装,显式校验validated bean时,你可以使用Spring-validation
或者Hibernate-validation
,而Spring-validation
另一个特性,就是在Springmvc模块中添加了自动校验,并将校验信息封装进了特定的类中·。这无疑便捷了我们的web开发。
二.引入依赖
本文将要介绍的在SpringMvc中在使用hibernate-validator进行校验。
//使用maven会自动引入javax.validation
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.19.Final</version>
</dependency>
三.常用注解
通过字段上的注解名称即可推断出校验内容,每一个注解都包含了message字段,用于校验失败时作为提示信息,特殊的校验注解,如@Pattern
(正则校验),还可以自己添加正则表达式。
注解 | 说明 |
---|---|
@Null |
限制只能为null |
@NotNull |
限制必须不为null |
@NotEmpty |
验证注解的元素值不为null且不为空 (字符串长度不为0、集合大小不为0 )(主要用于:String,Collection,Map,array) |
@NotBlank |
只支持字符串类型字段,验证注解的元素值不为空 (不为null、去除首位空格后长度为0 ),不同于@NotEmpty,@NotBlank只应用于字符串,且在比较时会去除字符串的空格 |
@Valid |
递归的对关联对象进行校验, 如果关联对象是个集合或者数组, 那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验 |
@Pattern(value) |
限制必须符合指定的正则表达式 |
@Size(max,min) |
限制字符长度必须在min到max之间,(主要用于: String, Collection, Map and array) |
@Range(min, max) |
被注释的元素必须在合适的范围内 (主要用于 : BigDecimal, BigInteger, String, byte, short, int, long ,原始类型的包装类 ) |
@Length(min, max) |
被注解的对象必须是字符串,大小必须在制定的范围内 |
@DecimalMax(value) | 限制必须为一个不大于指定值的数字 |
@DecimalMin(value) | 限制必须为一个不小于指定值的数字 |
@Max(value) | 限制必须为一个不大于指定值的数字 |
@Min(value) | 限制必须为一个不小于指定值的数字 |
@Digits(integer,fraction) | 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction |
@AssertFalse | 限制必须为false |
@AssertTrue | 限制必须为true |
@Future | 限制必须是一个将来的日期 |
@Past | 验证注解的元素值(日期类型)比当前时间早 |
验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 |
四.在@Controller中校验数据
validator校验时机: 在请求没有进入controller中的方法时,就会获取方法形参进行数据校验
4.1基本使用
1.定义测试校验Bean
@Data
public class BaseUserBean {
@NotNull(message = "username为NULL")
private String username;
@NotNull(message = "password为NULL")
private String password;
@NotBlank(message = "userType为BLANK")
private String userType;
private long currentTime = Instant.now().getEpochSecond();
}
2.对需要校验参数使用@Validated
@RestController
public class TestValidatorController {
/**
* 接受前端json字符串数据
*/
@RequestMapping("/test/validator1")
public Object test(@Validated @RequestBody BaseUserBean bean) {
return bean;
}
}
- 参数前需要加上
@Valid的、@Validated
注解,表明需要spring对其进行校验,如果不加,Spring会在校验不通过时直接抛出异常
。
如果有多个参数需要校验,形式如下:
public Objet test(@Validated Object param1, BindingResult param1BindingResult ,@Validated Object param2, BindingResult param2BindingResult)
即一个校验类对应一个校验结果
。
请求参数只标注@Validated弊端 :不能返回自定义异常
。Spring如果验证失败,则直接抛出异常,一般不可控。
请求参数
返回结果: 当username,password,userType都为空时,服务端把所有字段的异常都打印出来了
后台会抛出异常
3.使用@Validated+BindingResult
3.1.自定义异常
/**
* 多数情况下,创建自定义异常需要继承Exception,本例继承Exception的子类RuntimeException
*/
@Data
public class ServiceException extends RuntimeException {
private String code ; //异常对应的返回码
private String message; //异常对应的描述信息
public ServiceException(String code, String message) {
this.code = code;
this.message = message;
}
public ServiceException(String message) {
this.message = message;
}
}
3.2.具体实现
@RestController
public class TestValidatorController {
/**
* 接受前端json字符串数据
*/
@RequestMapping("/test/validator2")
public Object test2(@Validated @RequestBody BaseUserBean bean, BindingResult result) {
if (result.hasErrors()) {
throw new ServiceException("-1",result.getFieldError().getDefaultMessage());
}
return bean;
}
}
- 请求参数标注 @Validated+BindingResult :Spring校验完成之后会将校验结果传给参数
BindingResult
。在方法中可以通过BindingResult的返回结果
控制程序抛出自定义异常
或者返回不同结果
。
具体方式就是通过BindingResult的hasErrors()
判断是否校验通过,
- 校验未通过,通过
BindingResult的getFieldError().getDefaultMessage()
获取校验不通过字段
设置的message,如果没有设置,则返回默认值"javax.validation.constraints.XXX.message"。
响应结果:
后台会抛出异常
4.使用@Validated+BindingResult+切面
4.1.自定义异常
/**
* 多数情况下,创建自定义异常需要继承Exception,本例继承Exception的子类RuntimeException
*/
@Data
public class ServiceException extends RuntimeException {
private String code ; //异常对应的返回码
private String message; //异常对应的描述信息
public ServiceException(String code, String message) {
this.code = code;
this.message = message;
}
public ServiceException(String message) {
this.message = message;
}
}
4.2.具体实现
@RestController
public class TestValidatorController {
/**
* 接受前端json字符串数据
*/
@RequestMapping("/test/validator3")
public Object test3(@Validated @RequestBody BaseUserBean bean, BindingResult result) {
return bean;
}
}
4.3.切面
/**
* 全局拦截校验器
*/
@Aspect
@Component
public class ControllerValidatorAspect {
@Around("execution(* com.*.controller..*.*(..)) && args(..,bindingResult)")
public Object doAround(ProceedingJoinPoint pjp, BindingResult bindingResult) throws Throwable {
Object result = null;
if (bindingResult.hasErrors()) {
throw new ServiceException("-1", bindingResult.getFieldError().getDefaultMessage());
}
result = pjp.proceed();
return result;
}
}
使用aop实现全局拦截校验器,同时在aop里面在借助BindingResult减少controller层校验的代码,让校验逻辑统一处理,更高效。
响应结果
后台会抛出异常
新增一个全局异常捕捉处理,用于拦截自定义异常ServiceException,然后返回指定结果集
/**
* 全局异常捕捉处理
*/
@ControllerAdvice
public class GlobalExceptionController {
@ResponseBody
@ExceptionHandler(value = ServiceException.class)
public JSONObject customerServiceException(Exception ex) {
ServiceException serviceException = (ServiceException) ex;
JSONObject result = new JSONObject();
result.put("code", serviceException.getCode());
result.put("message", serviceException.getMessage());
return result;
}
}
@RestControllerAdvice + @ExceptionHandler
: 捕获指定异常,将返回的数据以json
输出@ControllerAdvice + @ExceptionHandler
: 捕获指定异常,将返回的数据指定格式(xml、json、html)输出
返回结果
4.2.@Validated和@Valid区别
- @Validated :支持
分组校验
功能,可以在入参验证时,根据不同的分组采用不同的验证机制。 可用在类、方法和方法参数
上。但是不能用在成员属性(字段)
上 - @Valid : 目前
没有校验分组
的功能,但可以对校验类内的对象成员属性
,进行嵌套验证
。可用在方法、构造函数、方法参数和成员属性
上
4.3.嵌套验证
在实际的开发中,前台会后台传递一个list,我们不仅要限制每次请求list内的个数,同时还要对list内基本元素的属性值进行校验。这个时候就需要进行嵌套验证了,实现的方式很简单。在list上添加@Vaild就可以实现了。
tips: 所有嵌套验证就是
“校验对象里面的对象”
@Valid加在方法参数时
不能够自动进行嵌套验证
,而是用在需要嵌套验证类内的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证。
1.校验类
@Data
public class UserJsonForm {
@NotNull(message = "loginName为NULL")
private String loginName;
@Valid
@Size(min = 1, max = 10, message = "列表中的元素数量为1~10")
private List<UserInfoForm> userInfoForms;
}
@Data
public class UserInfoForm {
@NotEmpty(message = "姓名不能为空")
private String name;
@Min(value = 1, message = "年龄不能小于1岁")
private Integer age;
@NotNull(message = "性别不能为空")
private Integer sex;
}
2.测试接口
@PostMapping("/test/valid")
public Object valid(@RequestBody @Validated UserJsonForm form) {
return form;
}
测试1: loginName为空,且userInfoForms为空
结论1: loginName与userInfoForms同时校验不通过
测试2: loginName为空,且userInfoForms[0]中的sex为空,userInfoForms[1]中的name为空
结论2: loginName校验不通过,虽然userInfoForms个数校验通过,但由于集合userInfoForms内属性不满足条件导致校验不通过。
4.4.分组校验
如果同一个类
,在不同的使用场景下有不同的校验规则
,那么可以使用分组校验。
- 如: 校验id在更新操作时不能为空,而在新增操作时需要为空的情况,可以使用
校验类内的校验注解的group属性来指定在什么情况下使用哪个验证规则
,同时在Controller方法使用@Validated({xxx.class})来分组验证 - 其实就是几个要分组的空接口,指定属性A属于哪个组,属性B又属于 哪个组,这样在controller验证时就指定我要验证哪个组
1. 创建分组接口
//新增分组
public interface One{
}
//更新分组
public interface Two {
}
2. 校验类
@Data
public class UserForm {
//id
@Null(message = "新增时id必须为空", groups = {
One.class})
@NotNull(message = "更新时id不能为空", groups = {
Two.class})
private String id;
//姓名
@NotEmpty(message = "姓名不能为空" , groups = {
One.class})
private String name;
//年龄
@NotNull(message = "年龄不能为空" , groups = {
One.class})
private Integer age;
}
3. 测试接口
//添加时指定校验分组为 Insert,id必须为null
@PostMapping("/test/addUser")
public String addUser(@RequestBody @Validated({
One.class}) UserForm form){
// 选择对应的分组进行校验
return "添加用户成功";
}
//更新时指定校验分组为 Update,id必不能为null
@PostMapping("/test/updateUser")
public String updateUser(@RequestBody @Validated({
Two.class}) UserForm form){
return "更新用户成功";
}
测试1: 调用新增接口时传id
测试2:调用更新接口时不传id
使用Spring @Validated 进行Groups验证是遇到的坑
4.5.控制分组校验顺序@GroupSequence
@GroupSequence它是JSR标准提供的注解
,可以按指定的分组先后顺序进行验证,前面的验证不通过,后面的分组就不行验证
- 比如:
@GroupSequence({One.class, Two.class, Three.class})
先执行One分组校验,然后执行Two分组校验。如果One分组校验失败了,则不会进行Two分组的校验。即:必须第一个组校验正确了,才执行第二组校验
@Data
@GroupSequence({
One.class, Two.class, UserVO.class})
public class UserVO {
@NotNull(message = "姓名不能为空", groups = {
One.class})
@Size(min = 1, max = 10, message = "姓名的长度在1-10之间", groups = {
Two.class})
private String name;
@NotNull(message = "年龄不能为空", groups = {
One.class})
@Min(value = 1, message = "年龄不能小于1岁", groups = {
Two.class})
@Max(value = 200, message = "年龄不能大于200岁", groups = {
Two.class})
private Integer age;
@NotNull(message = "性别不能为空", groups = {
One.class})
@Min(value = 0, message = "性别取值不能小于0", groups = {
Two.class})
@Max(value = 1, message = "性别取值不能大于1", groups = {
Two.class})
private Integer sex;
}
测试接口
@PostMapping("/test/group")
public String userVO(@RequestBody @Validated UserVO form) {
return "success";
}
上面的验证校验顺序分为2步
- 优先字段值是否为空(One分组)
- 字段值不为空的情况在判断字段的取值范围是否满足(Two分组)
测试1:测试One分组没有校验通过,Two分组是否会顺序执行?
响应1:虽然name的长度超出了10位 ,由于name的分组为Two
,所以优先校验分组One的属性sex的值不能为空
测试2:测试One分组校验通过后,Two分组是否会顺序执行?
响应结果2: 当name.age,sex的取值都不为空时,One分组校验全部通过,开始校验Two分组,判断出name的长度已经超出10位了
测试2: 所有字段依次执行校验
执行顺序为 0、1、2、3、4、5、6、7、8、9、Default
@GroupSequence({
VerifySeq.N0.class, VerifySeq.N1.class, VerifySeq.N2.class, VerifySeq.N3.class,
VerifySeq.N4.class, VerifySeq.N5.class, VerifySeq.N6.class, VerifySeq.N7.class,
VerifySeq.N8.class, VerifySeq.N9.class, Default.class})
public interface VerifySeq {
interface N0 {
}
interface N1 {
}
interface N2 {
}
interface N3 {
}
interface N4 {
}
interface N5 {
}
interface N6 {
}
interface N7 {
}
interface N8 {
}
interface N9 {
}
}
校验顺序为:username非空校验=>username长度校验=>phone非空校验=>remark长度校验=>password长度校验
//会执行顺序为 0、1、2、3、4、5、6、7、8、9、Default
@Data
public class UserDTO {
@NotBlank(message = "username不能为空", groups = VerifySeq.N1.class)
@Length(min = 8, max = 12, message = "username长度为8-12位", groups = VerifySeq.N2.class)
private String username;
@NotBlank(message = "password不能为空", groups = VerifySeq.N5.class)
private String password;
@NotBlank(message = "phone不能为空", groups = VerifySeq.N3.class)
private String phone;
@Length(max = 60, message = "remark长度不能超60位")
@NotBlank(message = "remark长度不能为空", groups = VerifySeq.N4.class)
private String remark;
}
在需要校验的接口处添加@Validated(VerifySeq.class)
@PostMapping("/test/seq")
public Object verifySeq(@RequestBody @Validated(VerifySeq.class) UserDTO form) {
return form;
}
4.6.自定义校验注解
业务需求总是比框架提供的这些简单校验要复杂的多,我们可以自定义校验注解来满足我们的需求。
如: 添加一个用于校验“字符串不能包含空格”的校验注解@CanNotHaveBlank 。
1. 自定义校验注解,并且通过validatedBy
指定了这个注解真正的验证类
@Target({
METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
//指定了当前注解使用哪个校验类来进行校验。
@Constraint(validatedBy = {
CanNotHaveBlankValidator.class})
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();
}
}
2. 编写验证类CanNotHaveBlankValidator
-
所有的验证类都需要实现
ConstraintValidator<注解类型,校验bean类型>接口
,实现该接口必须指定对应的注解类型以及校验Bean的类型 ,接口包含一个初始化事件方法initialize
,和一个判断是否合法的方法isValid
。 -
isValid() 中的
ConstraintValidatorContext
包含了认证中所有的信息,可以利用这个上下文获取默认错误提示信息
,禁用错误提示信息
,改写错误提示信息
等操作。public interface ConstraintValidator<A extends Annotation, T> { default void initialize(A constraintAnnotation) { } boolean isValid(T var1, ConstraintValidatorContext var2); }
public class CanNotHaveBlankValidator implements ConstraintValidator<CanNotHaveBlank, String> {
public void initialize(CannotHaveBlank constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
//null时不进行校验
if (value != null && value.contains(" ")) {
//获取默认提示信息
String defaultConstraintMessageTemplate = context.getDefaultConstraintMessageTemplate();
System.out.println("default message :" + defaultConstraintMessageTemplate);
//禁用默认提示信息
context.disableDefaultConstraintViolation();
//设置提示语
context.buildConstraintViolationWithTemplate("can not contains blank").addConstraintViolation();
return false;
}
return true;
}
}
3. 表单数据
@Data
public class CustomForm {
//电话号码
@CanNotHaveBlank
private String phone;
}
4. 测试接口
@PostMapping("/canNotHaveBlank")
public Object canNotHaveBlank(@RequestBody @Validated CustomForm form){
return form;
}
测试:传一个带有 空格的字符串 123 456
响应结果
4.7.自定义分组校验@GroupSequenceProvider
@GroupSequence只能在类中事先定义校验分组的顺序。
- 如遇到这种需求: 当
type值为A
,paramA值必传。type值为B
,paramB值必须传,单独使用分组校验和控制分组校验顺序都无法满足需求。需要使用@GroupSequenceProvider
1. 定义校验类
@Data
@GroupSequenceProvider(value = CustomSequenceProvider.class)
public class CustomGroupForm {
//类型
@Pattern(regexp = "[A|B]", message = "类型不必须为 A|B")
private String type;
//参数A
@NotEmpty(message = "参数A不能为空", groups = {
WhenTypeIsA.class})
private String paramA;
//参数B
@NotEmpty(message = "参数B不能为空", groups = {
WhenTypeIsB.class})
private String paramB;
//分组A
public interface WhenTypeIsA {
}
//分组B
public interface WhenTypeIsB {
}
}
2. 实现DefaultGroupSequenceProvider接口,编写分组校验逻辑
public class CustomSequenceProvider implements DefaultGroupSequenceProvider<CustomGroupForm> {
@Override
public List<Class<?>> getValidationGroups(CustomGroupForm form) {
List<Class<?>> defaultGroupSequence = new ArrayList<>();
//默认分组
defaultGroupSequence.add(CustomGroupForm.class);
//如果类型值为A 使用A分组WhenTypeIsA
if (form != null && "A".equals(form.getType())) {
defaultGroupSequence.add(CustomGroupForm.WhenTypeIsA.class);
}
//如果类型值为B 使用B分组WhenTypeIsB
if (form != null && "B".equals(form.getType())) {
defaultGroupSequence.add(CustomGroupForm.WhenTypeIsB.class);
}
//返回分组
return defaultGroupSequence;
}
}
测试1: 类型为A时
测试2 类型为B时:
4.8.手动校验
1.获取validator
在某些场景下需要我们手动校验bean
,用校验器对需要被校验的bean发起validate获得校验结果。
- 理论上我们既可以使
用Hibernate Validation提供Validator
,也可以使用Spring Validation的Validator
。
1. 依赖了Hibernate-Validation框架,可以用Hibernate的工厂方法
来获取validator实例,从而校验。
public class ValidationTest {
public static void main(String[] args) {
Foo foo = new Foo();
foo.setUsername(null);
foo.setPassword(null);
foo.setUserType("");
//构建Validator
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
//使用Validator校验bean
Set<ConstraintViolation<Foo>> set = validator.validate(foo);
for (ConstraintViolation<Foo> constraintViolation : set) {
System.out.println(constraintViolation.getMessage());
}
}
@Data
public static class Foo {
@NotNull(message = "username为NULL")
private String username;
@NotNull(message = "password为NULL")
private String password;
@NotBlank(message = "userType为BLANK")
private String userType;
}
}
执行结果
2.使用Spring来获取validator实例,从而校验。
-
Spring对validation同时支持
JSR-303、JSR-349
的标准,并且封装了LocalValidatorFactoryBean 作为validator的实现。兼容了Spring-validation体系和Hibernate-validation体系
,可以被开发者直接调用
,代替上述的从工厂方法中获取的Hibernate-validator。 -
Validator接口有两个,一个是位于
javax.validation包
下,另一个是Spring自己内置的,位于org.springframework.validation包
。 LocalValidatorFactoryBean同时实现了这两个接口。 -
如果使用SpringBoot,LocalValidatorFactoryBean已经成为了Validator的默认实现,使用时只需要自动注入即可。
@Autowired Validator globalValidator;
-
也可以使用配置类初始化LocalValidatorFactoryBean,然后从Spring容器中获取
@Component @Configuration public class GlobalWebConfig { @Bean public Validator validator() { return new LocalValidatorFactoryBean(); } }
2.工具类ValidatorUtils
/**
* 为什么要使用这个工具类呢?
* 1、controller方法中不用加入BindingResult参数
* 2、controller方法中需要校验的参数也不需要加入@Valid或者@Validated注解
* <p>
* 具体使用
* 在controller方法或者全局拦截校验器中调用 ValidatorUtils.validateResultProcess(需要校验的Bean) 直接获取校验的结果。
*
**/
@Component
public class ValidatorUtils implements ApplicationContextAware {
//jackson的对象映射类
private static final ObjectMapper objectMapper = new ObjectMapper();
private static Validator validator;
/*
* 校验bean并返回所有验证失败信息
* @param obj 当前校验对象
* @param groups 当前校验的组,非必传,不传按照默认分组校验
* @return 如: Optional[[{"propertyPath":"Foo.password","message":"password为NULL"},{"propertyPath":"Foo.userType","message":"userType为BLANK"}]]
* @throws ServiceException
*/
public static Optional<String> validateResultProcess(Object obj, Class<?>... groups) throws ServiceException {
// 用验证器执行验证,返回一个验证失败的set集合
Set<ConstraintViolation<Object>> results = validator.validate(obj,groups);
// 判断是否为空,空:说明验证通过,否则就验证失败
if (CollectionUtils.isEmpty(results)) {
return Optional.empty();
}
List<ErrorMessage> errorMessages = results.stream()
//将results转换成 List<ErrorMessage>返回
.map(result -> {
try {
List<ErrorMessage> childErrorMessages = objectMapper.readValue(result.getMessage(), new TypeReference<List<ErrorMessage>>() {
});
return childErrorMessages;
} catch (Exception e) {
ErrorMessage errorMessage = new ErrorMessage();
errorMessage.setPropertyPath(String.format("%s.%s", result.getRootBeanClass().getSimpleName(), result.getPropertyPath().toString()));
errorMessage.setMessage(result.getMessage());
return Arrays.asList(errorMessage);
}
})
//合并 map操作转换成的多个 List<ErrorMessage>为一个
.flatMap(errorMessageList -> errorMessageList.stream())
.collect(Collectors.toList());
try {
return Optional.of(objectMapper.writeValueAsString(errorMessages));
} catch (JsonProcessingException e) {
throw new ServiceException("JsonProcessingException " + e.getMessage());
}
}
/**
* 校验bean校验失败抛出自定义异常 ServiceException
* @param obj 当前校验对象
* @param groups 当前校验的组,非必传,不传按照默认分组校验
* @throws ServiceException
*/
public static void validateResultProcessWithException(Object obj, Class<?>... groups) throws ServiceException {
Optional<String> validateResult = ValidatorUtils.validateResultProcess(obj,groups);
if (validateResult.isPresent()) {
throw new ServiceException(validateResult.get());
}
}
/**
* 初始化validator 对象
* @param applicationContext
* @throws BeansException
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
//获取Hibernate validator 的 validator
//ValidatorUtils.validator = Validation.buildDefaultValidatorFactory().getValidator();
//通过Spring 封装的 LocalValidatorFactoryBean获取validator
ValidatorUtils.validator = (Validator) applicationContext.getBean("validator");
/*
@Bean
public Validator validator() {
return new LocalValidatorFactoryBean();
}
*/
}
/**
* 校验分组
*/
public static class ValidatorGroup {
public interface First extends Default {
}
public interface Second extends Default {
}
public interface Third extends Default {
}
}
/**
* 错误信息封装
*/
public static class ErrorMessage {
private String propertyPath;
private String message;
public String getPropertyPath() {
return propertyPath; }
public void setPropertyPath(String propertyPath) {
this.propertyPath = propertyPath; }
public String getMessage() {
return message; }
public void setMessage(String message) {
this.message = message; }
}
}
异常ServiceException 在上文
4.1.自定义异常
3.使用ValidatorUtils校验bean
//新增时id必须为空
UserForm addForm = new UserForm();
addForm.setId("1");
addForm.setName("张三");
addForm.setAge(12);
//校验bean并返回所有验证失败信息
Optional<String> addResult = ValidatorUtils.validateResultProcess(addForm, One.class);
System.out.println(addResult);
//更新时id不能为空
UserForm updateForm = new UserForm();
updateForm.setId(null);
addForm.setName("张三");
updateForm.setAge(12);
//校验bean并返回所有验证失败信息
Optional<String> updateResult = ValidatorUtils.validateResultProcess(updateForm, Two.class);
System.out.println(updateResult);
返回结果
Optional[[{
"propertyPath":"UserForm.id","message":"新增时id必须为空"}]]
Optional[[{
"propertyPath":"UserForm.id","message":"更新时id不能为空"}]]
4.9.(拓展)-使用SpringValidation校验容器中的Bean
BeanPostProcessor
能够校验Spring容器中的Bean,对所有的Bean在初始化前/后进行校验,从而决定允不允许它初始化完成。
- 比如我们有些Bean某些字段是不允许为空的,比如数据的链接,用户名密码等等,这个时候用上它处理就非常的优雅和高级了~ 若校验不通过的情况下就会抛出异常,阻止容器的正常启动~
@Component
public class BeanValidationPostProcessor implements BeanPostProcessor, InitializingBean {
// 这就是我们熟悉的校验器
// 请注意这里是javax.validation.Validator,而不是org.springframework.validation.Validator
@Nullable
private Validator validator;
// true:表示在Bean初始化之后完成校验
// false:表示在Bean初始化之前就校验
private boolean afterInitialization = false;
// 省略get/set
public Validator getValidator() {
return validator;
}
public void setValidator(Validator validator) {
this.validator = validator;
}
public boolean isAfterInitialization() {
return afterInitialization;
}
public void setAfterInitialization(boolean afterInitialization) {
this.afterInitialization = afterInitialization;
}
// 由此可见使用的是默认的校验器(当然还是Hibernate的)
@Override
public void afterPropertiesSet() {
if (this.validator == null) {
this.validator = Validation.buildDefaultValidatorFactory().getValidator();
}
}
// 这个实现太简单了~~~
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (!this.afterInitialization) {
doValidate(bean);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (this.afterInitialization) {
doValidate(bean);
}
return bean;
}
protected void doValidate(Object bean) {
Assert.state(this.validator != null, "No Validator set");
Object objectToValidate = AopProxyUtils.getSingletonTarget(bean);
if (objectToValidate == null) {
objectToValidate = bean;
}
Set<ConstraintViolation<Object>> result = this.validator.validate(objectToValidate);
// 拼接错误消息最终抛出
if (!result.isEmpty()) {
StringBuilder sb = new StringBuilder("Bean state is invalid: ");
for (Iterator<ConstraintViolation<Object>> it = result.iterator(); it.hasNext(); ) {
ConstraintViolation<Object> violation = it.next();
sb.append(violation.getPropertyPath()).append(" - ").append(violation.getMessage());
if (it.hasNext()) {
sb.append("; ");
}
}
throw new BeanInitializationException(sb.toString());
}
}
}