【SpringBoot】使用hibernate-validator实现优雅的参数校验


constraint: /kənˈstreɪnt/ : 约束;局促,态度不自然;强制

一.使用hibernate-validator校验框架必要性

数据的校验是网站一个不可或缺的功能,前端的js校验可以涵盖大部分的校验职责,如用户名唯一性,生日格式,邮箱格式校验等等常用的校验。但是为了避免用户绕过浏览器,使用http工具直接向后端请求一些违法数据,服务端的数据校验也是必要的。

hibernate-validator的作用

  • 验证逻辑与业务逻辑之间进行了分离,降低了程序耦合度;
  • 统一且规范的验证方式,无需你再次编写重复的验证代码;
  • 你将更专注于你的业务,将这些繁琐的事情统统丢在一边。

简述JSR303、JSR-349、Hibernate-validation、Spring-validation之间的关系

  • JSR-303JAVA 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 验证注解的元素值是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步

  1. 优先字段值是否为空(One分组)
  2. 字段值不为空的情况在判断字段的取值范围是否满足(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值为AparamA值必传type值为BparamB值必须传单独使用分组校验和控制分组校验顺序都无法满足需求。需要使用 @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());
        }
    }
}

猜你喜欢

转载自blog.csdn.net/qq877728715/article/details/113795082