@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; } }
测试结果
我们执行用例时会发现,四个用例均断言失败并且控制台打印: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.类上不添加任何注解
2.类上添加@Validated注解
3.类上添加@Valid注解
使用注意要点
通过上面的例子,我们可以发现,只有类上添加了@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了
如下:
可以发现,错误信息已经被封装到了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中具体的数据校验的使用方法及其原理都非常的模糊,但是经过这一篇文章的学习,现在可以说知道自己用了什么了并且知道怎么用,也知道为什么。这也是我写这篇文章的目的。按照惯例,我们还是总结了一张图,如下: