Spring官网阅读(十七)Spring中的数据校验(2)

简介: Spring官网阅读(十七)Spring中的数据校验(2)

@Validated跟@Valid的区别


关于二者的区别网上有很多文章,但是实际二者的区别大家不用去记,我们只要看一看两个注解的申明变一目了然了。


@Validated

// Target代表这个注解能使用在类/接口/枚举上,方法上以及方法的参数上
// 注意注意!!!! 它不能注解到字段上
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
// 在运行时期仍然生效(注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在)
@Retention(RetentionPolicy.RUNTIME)
// 这个注解应该被 javadoc工具记录. 默认情况下,javadoc是不包括注解的. 但如果声明注解时指定了 @Documented,则它会被 javadoc 之类的工具处理, 所以注解类型信息也会被包括在生成的文档中,是一个标记注解,没有成员。
@Documented
public @interface Validated {
  // 校验时启动的分组
  Class<?>[] value() default {};
}

@Valid

// 可以作用于类,方法,字段,构造函数,参数,以及泛型类型上(例如:Main<@Valid T> )
// 简单来说,哪里都可以放
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface Valid {
    //没有提供任何属性
}

我们通过上面两个注解的定义就能很快的得出它们的区别:


1.来源不同,@Valid是JSR的规范,来源于javax.validation包下,而@Validated是Spring自身定义的注解,位于org.springframework.validation.annotation包下


2.作用范围不同,@Validated无法作用在字段上,正因为如此它就无法完成对级联属性的校验。而@Valid的

没有这个限制。


3.注解中的属性不同,@Validated注解中可以提供一个属性去指定校验时采用的分组,而@Valid没有这个功能,因为@Valid不能进行分组校验


我相信通过这个方法的记忆远比看博客死记要好~


实际生产应用


我们将分为两部分讨论


1.对JavaBean的校验

2.对普通参数的校验

这里说的普通参数的校验是指参数没有被封装到JavaBean中,而是直接使用,例如:

test(String name,int age),这里的name跟age就是简单的参数。

而将name跟age封装到JavaBean中,则意味着这是对JavaBean的校验。

同时,按照校验的层次,我们可以将其分为


1.对controller层次(接口层)的校验

2.对普通方法的校验

接下来,我们就按这种思路一一进行分析


子所以按照层次划分是因为Spring在对接口上的参数进行校验时,跟对普通的方法上的参数进行校验采用的是不同的形式(虽然都是依赖于JSR的实现来完成的,但是调用JSR的手段不一样)


对JavaBean的校验


待校验的类

@Data
public class Person {
    // 错误消息message是可以自定义的
    @NotNull//(groups = Simple.class)
    public String name;
    @Positive//(groups = Default.class)
    public Integer age;
    @NotNull//(groups = Complex.class)
    @NotEmpty//(groups = Complex.class)
    private List<@Email String> emails;
    // 定义两个组 Simple组和Complex组
    public interface Simple {
    }
    public interface Complex {
    }
}
// 用于进行嵌套校验
@Data
public class NestPerson {
    @NotNull
    String name;
    @Valid
    Person person;
}

对controller(接口)层次上方法参数的校验


用于测试的接口

// 用于测试的接口
@RestController
@RequestMapping("/test")
public class Main {
    // 测试 @Valid对JavaBean的校验效果
    @RequestMapping("/valid")
    public String testValid(
            @Valid @RequestBody Person person) {
        System.out.println(person);
        return "OK";
    }
    // 测试 @Validated对JavaBean的校验效果
    @RequestMapping("/validated")
    public String testValidated(
            @Validated @RequestBody Person person) {
        System.out.println(person);
        return "OK";
    }
    // 测试 @Valid对JavaBean嵌套属性的校验效果
    @RequestMapping("/validNest")
    public String testValid(@Valid @RequestBody NestPerson person) {
        System.out.println(person);
        return "OK";
    }
    // 测试 @Validated对JavaBean嵌套属性的校验效果
    @RequestMapping("/validatedNest")
    public String testValidated(@Validated @RequestBody NestPerson person) {
        System.out.println(person);
        return "OK";
    }
}

测试用例

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringFxApplication.class)
public class MainTest {
    @Autowired
    private WebApplicationContext context;
    @Autowired
    ObjectMapper objectMapper;
    MockMvc mockMvc;
    Person person;
    NestPerson nestPerson;
    @Before
    public void init() {
        person = new Person();
        person.setAge(-1);
        person.setName("");
        person.setEmails(new ArrayList<>());
        nestPerson = new NestPerson();
        nestPerson.setPerson(person);
        mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
    }
    @Test
    public void testValid() throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post("/test/valid")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(person));
        MvcResult mvcResult = mockMvc.perform(builder).andReturn();
        Exception resolvedException = mvcResult.getResolvedException();
        System.out.println(resolvedException.getMessage());
        assert mvcResult.getResponse().getStatus()==200;
    }
    @Test
    public void testValidated() throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post("/test/validated")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(person));
        MvcResult mvcResult = mockMvc.perform(builder).andReturn();
        Exception resolvedException = mvcResult.getResolvedException();
        System.out.println(resolvedException.getMessage());
        assert mvcResult.getResponse().getStatus()==200;
    }
    @Test
    public void testValidNest() throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post("/test/validatedNest")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(nestPerson));
        MvcResult mvcResult = mockMvc.perform(builder).andReturn();
        Exception resolvedException = mvcResult.getResolvedException();
        System.out.println(resolvedException.getMessage());
        assert mvcResult.getResponse().getStatus()==200;
    }
    @Test
    public void testValidatedNest() throws Exception {
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post("/test/validatedNest")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(nestPerson));
        MvcResult mvcResult = mockMvc.perform(builder).andReturn();
        Exception resolvedException = mvcResult.getResolvedException();
        System.out.println(resolvedException.getMessage());
        assert mvcResult.getResponse().getStatus()==200;
    }
}

测试结果

微信图片_20221113133636.png

我们执行用例时会发现,四个用例均断言失败并且控制台打印:Validation failed for argument …。


另外细心的同学可以发现,Spring默认有一个全局异常处理器DefaultHandlerExceptionResolver


同时观察日志我们可以发现,全局异常处理器处理的异常类型为:org.springframework.web.bind.MethodArgumentNotValidException


使用注意要点


如果想使用分组校验的功能必须使用@Validated

不考虑分组校验的情况,@Validated跟@Valid没有任何区别

网上很多文章说@Validated不支持对嵌套的属性进行校验,这种说法是不准确的,大家可以对第三,四个接口方法做测试,运行的结果是一样的。更准确的说法是@Validated不能作用于字段上,而@Valid可以。


对普通方法的校验


待测试的方法

@Service
//@Validated
//@Valid
public class DmzService {
    public void testValid(@Valid Person person) {
        System.out.println(person);
    }
    public void testValidated(@Validated Person person) {
        System.out.println(person);
    }
}

测试用例

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringFxApplication.class)
public class DmzServiceTest {
    @Autowired
    DmzService dmzService;
    Person person;
    @Before
    public void init(){
        person = new Person();
        person.setAge(-1);
        person.setName("");
        person.setEmails(new ArrayList<>());
    }
    @Test
    public void testValid() {
        dmzService.testValid(person);
    }
    @Test
    public void testValidated() {
        dmzService.testValidated(person);
    }
}

我们分为三种情况测试

1.类上不添加任何注解

微信图片_20221113133828.png

2.类上添加@Validated注解

微信图片_20221113133854.png

3.类上添加@Valid注解

微信图片_20221113133925.png


使用注意要点


通过上面的例子,我们可以发现,只有类上添加了@Vlidated注解,并且待校验的JavaBean上添加了@Valid的情况下校验才会生效。


所以当我们要对普通方法上的JavaBean参数进行校验必须满足下面两个条件


方法所在的类上添加@Vlidated

待校验的JavaBean参数上添加@Valid


对简单参数校验


对普通方法的校验


用于测试的方法

@Service
@Validated
//@Valid
public class IndexService {
    public void testValid(@Max(10) int age,@NotBlank String name) {
        System.out.println(age+"     "+name);
    }
    public void testValidated(@Max(10) int age,@NotBlank String name) {
        System.out.println(age+"     "+name);
    }
    public void testValidNest(@Max(10) int age,@NotBlank String name) {
        System.out.println(age+"     "+name);
    }
    public void testValidatedNest(@Max(10) int age,@NotBlank String name) {
        System.out.println(age+"     "+name);
    }
}

测试用例

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringFxApplication.class)
public class IndexServiceTest {
    @Autowired
    IndexService indexService;
    int age;
    String name;
    @Before
    public void init(){
        age=100;
        name = "";
    }
    @Test
    public void testValid() {
        indexService.testValid(age,name);
    }
    @Test
    public void testValidated() {
        indexService.testValidated(age,name);
    }
    @Test
    public void testValidNest() {
        indexService.testValidNest(age,name);
    }
    @Test
    public void testValidatedNest() {
        indexService.testValidatedNest(age,name);
    }
}

这里的测试结果我就不再放出来了,大家猜也能猜到答案


使用注意要点


方法所在的类上添加@Vlidated(@Valid注解无效),跟JavaBean的校验是一样的


对controller(接口)层次的校验

@RestController
@RequestMapping("/test/simple")
// @Validated
public class ValidationController {
    @RequestMapping("/valid")
    public String testValid(
            @Valid @Max(10) int age, @Valid @NotBlank String name) {
        System.out.println(age + "      " + name);
        return "OK";
    }
    @RequestMapping("/validated")
    public String testValidated(
            @Validated @Max(10) int age, @Valid @NotBlank String name) {
        System.out.println(age + "      " + name);
        return "OK";
    }
}

在测试过程中会发现,不论是在参数前添加了@Valid或者@Validated校验均不生效。这个时候不得不借助Spring提供的普通方法的校验功能来完成数据校验,也就是在类级别上添加@Valivdated(参数前面的@Valid或者@Validated可以去除)


使用注意要点


对于接口层次简单参数的校验需要借助Spring对于普通方法校验的功能,必须在类级别上添加@Valiv=dated注解。


注意


在上面的所有例子中我都是用SpringBoot进行测试的,如果在单纯的SpringMVC情况下,如果对于普通方法的校验不生效请添加如下配置:

@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
    return new MethodValidationPostProcessor();
}

实际上对于普通方法的校验,就是通过这个后置处理器来完成的,它会生成一个代理对象帮助我们完成校验。SpringBoot中默认加载了这个后置处理器,而SpringMVC需要手动配置


结合BindingResult使用


在上面的例子中我们可以看到,当对于接口层次的JavaBean进行校验时,如果校验失败将会抛出org.springframework.web.bind.MethodArgumentNotValidException异常,这个异常将由Spring默认的全局异常处理器进行处理,但是有时候我们可能想在接口中拿到具体的错误进行处理,这个时候就需要用到BindingResult了


如下:

微信图片_20221113134618.png

可以发现,错误信息已经被封装到了BindingResult,通过BindingResult我们能对错误信息进行自己的处理。请注意,这种做法只对接口中JavaBean的校验生效,对于普通参数的校验是无效的。


实际上经过上面的学习我们会发现,其实Spring中的校验就是两种(前面的分类是按场景分的)


1.Spring在接口上对JavaBean的校验

2.Spring在普通方法上的校验

第一种校验失败将抛出org.springframework.web.bind.MethodArgumentNotValidException异常,而第二种校验失败将抛出javax.validation.ConstraintViolationException异常


为什么会这样呢?


这是因为,对于接口上JavaBean的校验是Spring在对参数进行绑定时做了一层封装,大家可以看看org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#resolveArgument这段代码

  public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    parameter = parameter.nestedIfOptional();
    Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
    String name = Conventions.getVariableNameForParameter(parameter);
    if (binderFactory != null) {
            // 获取一个DataBinder
      WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
      if (arg != null) {
                // 进行校验,实际上就是调用DataBinder完成校验
        validateIfApplicable(binder, parameter);
                // 如果校验出错并且没有提供BindingResult直接抛出一个MethodArgumentNotValidException
        if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
          throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
        }
      }
      if (mavContainer != null) {
        mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
      }
    }
    return adaptArgumentIfNecessary(arg, parameter);
  }

但是对于普通方法的校验时,Spring完全依赖于动态代理来完成参数的校验。具体细节在本文中不多赘述,大家可以关注我后续文章,有兴趣的同学可以看看这个后置处理器:MethodValidationPostProcessor


结合全局异常处理器使用


在实际应用中,更多情况下我们结合全局异常处理器来使用数据校验的功能,实现起来也非常简单,如下:

@RestControllerAdvice
public class MethodArgumentNotValidExceptionHandler {
  // 另外还有一个javax.validation.ConstraintViolationException异常处理方式也类似,这里不再赘述
    // 关于全局异常处理器的部分因为是跟SpringMVC相关的,另外牵涉到动态代理,所以目前我也不想做过多介绍
    // 大家只要知道能这么用即可,实际的使用可自行百度,非常简单
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder stringBuilder = new StringBuilder();
        for (FieldError error : bindingResult.getFieldErrors()) {
            String field = error.getField();
            Object value = error.getRejectedValue();
            String msg = error.getDefaultMessage();
            String message = String.format("错误字段:%s,错误值:%s,原因:%s;", field, value, msg);
            stringBuilder.append(message).append("\r\n");
        }
        return Result.error(MsgDefinition.ILLEGAL_ARGUMENTS.codeOf(), stringBuilder.toString());
    }
}

总结


关于数据校验我们就介绍到这里了,其实我自己之前对Spring中具体的数据校验的使用方法及其原理都非常的模糊,但是经过这一篇文章的学习,现在可以说知道自己用了什么了并且知道怎么用,也知道为什么。这也是我写这篇文章的目的。按照惯例,我们还是总结了一张图,如下:

微信图片_20221113134813.png

相关文章
|
6月前
|
Java 数据库连接 Spring
Spring之数据校验:Validation
【1月更文挑战第17天】 一、Spring Validation概述 二、实验一:通过Validator接口实现 三、实验二:Bean Validation注解实现 四、实验三:基于方法实现校验 五、实验四:实现自定义校验
145 2
Spring之数据校验:Validation
|
6月前
|
Java 应用服务中间件 Spring
Spring5源码(50)-SpringMVC源码阅读环境搭建
Spring5源码(50)-SpringMVC源码阅读环境搭建
75 0
|
Java Nacos Spring
Nacos spring-cloud 版本没找到共享配置文件的说明,Nacos服务中共享,并且可以被多个应用获取和使用。这个在官网哪里有说明啊
Nacos spring-cloud 版本没找到共享配置文件的说明,Nacos服务中共享,并且可以被多个应用获取和使用。这个在官网哪里有说明啊
73 1
|
4月前
|
前端开发 JavaScript Java
Spring Boot中的数据校验
Spring Boot中的数据校验
|
5月前
|
存储 Java 程序员
Spring 注册BeanPostProcessor 源码阅读
Spring 注册BeanPostProcessor 源码阅读
|
4月前
|
Java 数据库连接 测试技术
在Spring Boot中实现数据校验与验证
在Spring Boot中实现数据校验与验证
|
6月前
|
Java 数据库连接 Maven
【Spring】掌握 Spring Validation 数据校验
【Spring】掌握 Spring Validation 数据校验
271 0
|
前端开发 Java 数据库
Spring Entity数据校验,分组校验,返回校验结果给前端
Spring Entity数据校验,分组校验,返回校验结果给前端
96 0
|
Java 中间件 Maven
Spring 6 源码编译和高效阅读源码技巧分享
Spring 6 源码编译和高效阅读源码技巧分享
|
6月前
|
人工智能 运维 Java
spring数据校验
spring数据校验
26 0