1、JSR-303校验规范,应该叫约束constraints
改约束主要定义的很多约束注解,如下图:
这些注解就是规范,那么谁来解析这个注解呢?当然是校验器,Java没有帮我们实现校验器,因此只能去找了,比较有名的校验器就是hibernate-validator,别想了这个东西跟orm 框架Hibernate没半毛钱关系,hibernate-validator不仅仅支持JSR-303的约束,自己还扩展了一些约,并实现了校验的业务功能。
2、在springMVC中的校验器
2.1、spring 一贯的作风就是集成,一般不会重复造轮子,所以你只要引入JSR-303约束的依赖 + hibernate-validator的依赖,你就能在springMVC中使用校验器。
2.2、需要引入的maven依赖
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.4.Final</version>
</dependency>
2.3、使用方式
@RequestMapping("test")
public String test(@Validated Huma huma, BindingResult bindingResult) {
使用@Validated标注需要校验的实体类,在添加一个校验结果绑定的入参bindingResult
huma.setAge(18);
return "register";
}
@RequestMapping("user2")
@ResponseBody
private MyResponse <String> user1(@RequestBody @Validated Huma huma, BindingResult bindingResult) {
int i = 1;
if (i == 1) {
throw new BusinessException(101, "BusinessException名称不能为空!");
}
return MyResponse.buildSuccess("register success");
}
public class Huma implements Serializable {
@NotBlank(message = "名称不能为空!")
private String name;
@Min(value = 0, message = "年龄不能小于0")
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
2.4、验证结果展示:在浏览器中输入http://localhost:7070/user/test?name=&age=10 注意我们没传入name属性
3、分组校验
很多时候我们的rest接口在做校验是的时候会发现一个问题就是,比如说多个接口的如参数类型是同一个实体类,但是在不同的接口校验入参的时候,可能校验策略就会不一样,比如在save数据操作的时候我们不需要传入id值,但是在update数据操作的时候id值是必须的,但是我们只定义了一个实体类,这个时候怎么办呢? 分组校验登场了。
先看使用案例:
实体类代码:
public class Huma implements Serializable {
定义只有在指定HumaUpdateGroup分组的时候才会触发校验。
@NotBlank(message = "id不能为空!",groups = HumaUpdateGroup.class)
private String id;
定义只有在指定HumaUpdateGroup、HumaSaveGroup分组的时候才会触发校验。
@NotBlank(message = "名称不能为空!", groups = {HumaUpdateGroup.class, HumaSaveGroup.class})
private String name;
不指定分组就一定会触发校验,指定了分组,如果分组不匹配就不会校验。
@Min(value = 0, message = "年龄不能小于0",groups = {HumaUpdateGroup.class, HumaSaveGroup.class, HumaSelectGroup.class})
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
我们定义三个表示分组的接口,分组接口只有标记作用,除了标记没有任何作用。
save操作分组接口:
public interface HumaSaveGroup {
}
update操作分组接口:
public interface HumaUpdateGroup {
}
select操作分组接口:
public interface HumaSelectGroup {
}
rest接口上指定需要使用那个分组来进行校验:
save操作指定使用HumaSaveGroup分组进行校验:
@RequestMapping("test/save")
public String test(@Validated(value = HumaSaveGroup.class) Huma huma, BindingResult bindingResult) {
huma.setAge(18);
return "register";
}
update操作指定使用HumaUpdateGroup分组进行校验:
@RequestMapping("test/update")
public String testupdate(@Validated(value = HumaUpdateGroup.class) Huma huma, BindingResult bindingResult) {
huma.setAge(18);
return "register";
}
select操作指定使用HumaSelectGroup分组进行校验:
@RequestMapping("test/select")
public String testselect(@Validated(value = HumaSelectGroup.class) Huma huma, BindingResult bindingResult) {
huma.setAge(18);
return "register";
}
测试save操作:http://localhost:7070/user/test/save?name=wenzongyuan&age=10&id= 不传入id,看看会不会有验证错误。
测试update操作:http://localhost:7070/user/test/update?name=wenzongyuan&age=10&id= 入参不变
测试select操作:http://localhost:7070/user/test/select?name=wenzongyuan&age=10&id= 入参还是不变
以上就是分组校验的实践。
4、结合MessageSource进行校验的错误描述信息message进行国际化展示
4.1、实体类修改:
public class Huma implements Serializable {
@NotBlank(message = "{id.notBlank}",groups = HumaUpdateGroup.class)
private String id;
@NotBlank(message = "{id.notBlank}", groups = {HumaUpdateGroup.class, HumaSaveGroup.class})
private String name;
@Min(value = 0, message = "{age.ge.zero}",groups = {HumaUpdateGroup.class, HumaSaveGroup.class, HumaSelectGroup.class})
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
4.2、配置校验器 + 消息源 以及注解驱动中指定校验器
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
<!--不设置则默认为classpath下的 ValidationMessages.properties -->
<property name="validationMessageSource" ref="messageSource"/>
</bean>
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>i18/messages</value>
</list>
</property>
</bean>
也许会有疑问,我们之前没配置校验器,为啥也能正常提供校验功能???这个后面会详细分析。
<mvc:annotation-driven validator="validator"/>
国际化文件目录:
messages_en_US.properties中配置:
messages_zh_CN.properties中配置:中文全部转为ASCLL码
4.3、结合LocaleChangeInterceptor + SessionLocaleResolver 来进行本地化信息切换
<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" p:paramName="locale"/>
</mvc:interceptors>
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
<!--配置在HttpSession中 本地化参数的key,在SessionLocaleResolver解析的是会使用
这个key去HttpSession中获取本地化Locale实例-->
<property name="localeAttributeName" value="locale"/>
</bean>
4.4、开始测试
测试代码:
@RequestMapping("test/save")
public String test(@Validated(value = HumaSaveGroup.class) Huma huma, BindingResult bindingResult) {
huma.setAge(18);
return "register";
}
浏览器输入:http://localhost:7070/user/test/save?name=&age=10&id=&locale=zh_CN 先测试汉语_中国
接着测试英语_美国:http://localhost:7070/user/test/save?name=&age=10&id=&locale=en_US
以上就是结合本地化信息来进行验证错误信息本地化的案例。
5、校验器在SpringMVC中的工作原理
5.1、校验器的配置方式
方式1:不配置,使用SpringMVC默认配置的校验器,依赖于SpringMVC注解驱动<mvc:annotation-driven/>
在SpringMVC的注解驱动的解析期间会帮我们自动注册一个校验器的BeanDefinition到spring容器中,在AnnotationDrivenBeanDefinitionParser的parse方法中有如下处理的代码片段:注意:代码非连续
1、注册一个Validator的BenaDefinition到SpringMVC的容器中,并返回一个引用。
RuntimeBeanReference validator = getValidator(element, source, parserContext);
2、构建一个类型是ConfigurableWebBindingInitializer的BeanDefintion实例,然后设置
ConfigurableWebBindingInitializer实例的属性validator=上面一步构建的RuntimeBeanReference。
ConfigurableWebBindingInitializer的作用就是初始化绑定者WebDataBinder,在WebDataBinder中维护了
验证器的列表,在使用ConfigurableWebBindingInitializer初始化WebDataBinder的时候会将配置的
Validator添加到WebDataBinder中维护的验证器的列表中,然后使用校验器进行参数校验。
RootBeanDefinition bindingDef = new RootBeanDefinition(ConfigurableWebBindingInitializer.class);
bindingDef.getPropertyValues().add("validator", validator);
3、构建一个类型是RequestMappingHandlerAdapter的BeanDefinitions实例并设置其webBindingInitializer属性等于bindingDef
RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class);
handlerAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef);
构建Validator的BenaDefinition到SpringMVC的容器中,并返回一个引用的源码实现:
private RuntimeBeanReference getValidator(Element element, Object source, ParserContext parserContext) {
1、入参的element就是注解驱动元素 <mvc:annotation-driven/>
if (element.hasAttribute("validator")) {
2、如果有validator属性,那就直接使用配置的验证器的beanName构建一个
RuntimeBeanReference实例返回。
return new RuntimeBeanReference(element.getAttribute("validator"));
}
3、如果没有配置validator属性,那就判断是否依赖了javax.validation.Validator这个类。
else if (javaxValidationPresent) {
如果依赖了javax.validation.Validator这个类就构建一个类型是
org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean的
BeanDefinition然后注册到SpringMVC容器中,然后使用其优化后的beanName创建一个
RuntimeBeanReference返回。
RootBeanDefinition validatorDef = new RootBeanDefinition(
"org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean");
validatorDef.setSource(source);
validatorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
String validatorName = parserContext.getReaderContext().registerWithGeneratedName(validatorDef);
parserContext.registerComponent(new BeanComponentDefinition(validatorDef, validatorName));
return new RuntimeBeanReference(validatorName);
}
else {
return null;
}
}
方式2:显示配置一个Validator,配置方式上面演示过,不在累赘。
5.2、Validator验证器的工作时机
我们知道了在WebDataBinder中维护了验证器列表,那么验证器肯定是在参数绑定器里面工作的了,纵观源码我们找到了验证器的工作时机是在参数解析器HandlerMethodArgumentResolver的resolveArgument方法中。
我们使用ServletModelAttributeMethodProcessor参数解析器来进行讲解:代码片段如下:
0、先完成入参绑定。
Object attribute = (mavContainer.containsAttribute(name) ? mavContainer.getModel().get(name) :
createAttribute(name, parameter, binderFactory, webRequest));
1、先使用webs数据绑定工厂创建一个web数据绑定器
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
2、如果需要进行参数校验,就是判断参数是否被注解@Validated标注,如果标注了就进行校验。
validateIfApplicable(binder, parameter);
3、如果验证错误且在绑定验证错误的时候失败,那就抛出验证错误绑定到BindingResult失败异常。
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter )) {
throw new BindException(binder.getBindingResult());
}
}
// Add resolved attribute and BindingResult at the end of the model
Map<String, Object> bindingResultModel = binder.getBindingResult().getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
4、如果需要的话对入参类型进行转换。
return binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
validateIfApplicable(binder, parameter)实现源码:
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
1、获取入参上面标注的注解。
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
2、获取入参上标注的@Validated注解
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
3、如果有@Validated注解 或者有以Valid开头的注解都可以进行验证。
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
4、获取@Validated或者以@Valid名称开头的注解的成员属性value值,也就是分组列表。
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
5、传如分组信息,然后使用web数据绑定者进行参数校验。
binder.validate(validationHints);
break;
}
}
}
来到WebDataBinder的父类的validate(Object... validationHints)方法:
public void validate(Object... validationHints) {
1、循环当前WebDataBinder的验证器列表,我们配置的验证器也会在此列表中。
for (Validator validator : getValidators()) {
2、如果校验的分组信息不为空且校验器实现了SmartValidator接口,那就强转后进行校验
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
3、getTarget()返回当时参数以及绑定好的入参value值,
getBindingResult()就是创建一个BindingResult实例,
validationHints是分组数组。SpringMVC默认配置的是
OptionalValidatorFactoryBean类型,那么就会执行此处。
((SmartValidator) validator).validate(getTarget(), getBindingResult(), validationHints);
}
else if (validator != null) {
基于这里我们可以自定义一些校验器来使用,比如我们可以专门为某个实
体类写一个检验器,然后添加到SpringMVC中,就可以在此处进行调用。
validator.validate(getTarget(), getBindingResult());
}
}
}
我们以SpringMVC默认配置的OptionalValidatorFactoryBean校验来进行跟踪,接着来到器父类SpringValidatorAdapter的validate(Object target, Errors errors, Object... validationHints)方法:
@Override
public void validate(Object target, Errors errors, Object... validationHints) {
if (this.targetValidator != null) {
1、先使用group的数组构建一个Set集合,目的是去重。
Set<Class<?>> groups = new LinkedHashSet<Class<?>>();
if (validationHints != null) {
for (Object hint : validationHints) {
if (hint instanceof Class) {
groups.add((Class<?>) hint);
}
}
}
2、处理约束校验
processConstraintViolations(
this.targetValidator.validate(target, ClassUtils.toClassArray(groups)), errors);
}
}
使用目标校验器进行校验this.targetValidator.validate(target, ClassUtils.toClassArray(groups),SpringValidatorAdapter是一个校验器适配器,里面会委会一个真正干事情的校验器,这里时使用了Hibernate-validation里实现的校验器,ValidatorImpl,这个校验器校验完成后会返回一个校验结果集Set<ConstraintViolation<T>> T类型是我们需要校验的实体类的类型。ConstraintViolation是一个包装单个校验失败的字段实例,里面封装了验证失败的字段、以及验证失败的message,注意此处的message如果有配置需要本地换转换的也会在此步转化好。
问题:Hibernate-validation怎么使用到MessageSource进行转换的,又是怎么获取到本地化信息Locale的?
答:还记得我们在配置LocalValidatorFactoryBean的时候配置了一个属性叫validationMessageSource吗,我们去看看LocalValidatorFactoryBean是如何设置这个属性的,源码如下:
public void setValidationMessageSource(MessageSource messageSource) {
使用我们传入的messageSource构建一个MessageInterpolator类型的消息插补器,
并赋值给我们的校验器,真正的消息插补器的类型是ResourceBundleMessageInterpolator,但是
这个消息插补器是不具备本地化信息功能的插补器。
this.messageInterpolator =
HibernateValidatorDelegate.buildMessageInterpolator(messageSource);
}
消息插补器MessageInterpolator:
LocalValidatorFactoryBean这个我们配置的bean在初始化的时候会对上面创建的ResourceBundleMessageInterpolator进行包装成为LocaleContextMessageInterpolator具有解析当前请求的本地化信息的消息插补器,源码体现在LocalValidatorFactoryBean的初始化方法afterPropertiesSet()中:
先获取到setValidationMessageSource方法执行的时候构建的ResourceBundleMessageInterpolator实例
MessageInterpolator targetInterpolator = this.messageInterpolator;
if (targetInterpolator == null) {
targetInterpolator = configuration.getDefaultMessageInterpolator();
}
然后将整个校验环境的配置中的消息插补器包装成为LocaleContextMessageInterpolator带请求本地化解析的消息插补器。
configuration.messageInterpolator(new
LocaleContextMessageInterpolator(targetInterpolator));
使用上面构建好的校验环境的配置实例来创建验证器工厂validatorFactory,configuration的类型
是Hibernate-validation中的ConfigurationImpl 这个就是Hibernate-validation对jsr303的
Configuration接口的实现,在配置LocalValidatorFactoryBean的时候指定了一个
providerClass=org.hibernate.validator.HibernateValidator所以这个configuration实例就是使用其构
建的。所以我们最终得到的验证器工厂就是Hibernate-validation中的ValidatorFactoryImpl类型。
this.validatorFactory = configuration.buildValidatorFactory();
构建好验证器工厂后那就使用此validatorFactory 来构建一个验证器并设置到
LocalValidatorFactoryBean实例的父类SpringValidatorAdapter的targetValidator属性,然后在校验的时
候就是用这个目标验证器来进行校验。这个目标校验器的类型是Hibernate-validation中的ValidatorImpl类型
setTargetValidator(this.validatorFactory.getValidator());
上面我们分析了LocalValidatorFactoryBean在初始化的时候会构建一个目标校验器targetValidator,而在使用Hibernate-validation的时候这个目标验证器的类型是Hibernate-validation中的ValidatorImpl类型,我们发现在这个ValidatorImpl中有一个是属性private final MessageInterpolator messageInterpolator; 没错就是消息插补器,而这个插补器正是我们包装好的LocaleContextMessageInterpolator实例,整条链路就是这样,我们来看看LocaleContextMessageInterpolator的实现方式:我们跟踪到ValidatorImpl最终会调用LocaleContextMessageInterpolator的interpolate(messageTemplate, context);方法来获取验证错误的提示信息message,我们就查看LocaleContextMessageInterpolator的String interpolate(String messageTemplate, Context context)实现原理:源码如下:
@Override
public String interpolate(String message, Context context) {
return this.targetInterpolator.interpolate(message, context, LocaleContextHolder.getLocale());
}
实现里面获取本地化信息的方式是使用LocaleContextHolder来获取当前的本地化信息,LocaleContextHolder时使用ThreadLocal来实现的,里面存的value值类型是LocaleContext也就是本地换信息上下文,那么设置本地化信息是什么时候设置进去的呢?跟我们的本地化信息解析器LocaleResolver又是如何搭配着干活的呢????? TMD 大神不好当啊。。。。
LocaleContext既然是本地化信息上下文,那么里面必然能够获取到当前的本地化信息实例,这是必然的,LocaleContext的类图如下:
接下来我们再了解一个东西LocaleContextResolver接口:
public interface LocaleContextResolver extends LocaleResolver {
使用HttpServletRequest实例来获取一个LocaleContext实例
LocaleContext resolveLocaleContext(HttpServletRequest request);
将解析好的LocaleContext实例设置到请求的属性集中。
void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext);
}
这个接口是本地化解析LocaleResolver的派生接口其类图如下:
LocaleContextResolver这个东西也讲清楚后我们来剖析什么时候会将解析好的LocaleContext实例绑定到LocaleContextHolder的ThreadLocal中???
纵观源码实现我们找到了有2个地方都进行了LocaleContext的构建与线程绑定,分别如下:
1、RequestContextListener的requestInitialized(ServletRequestEvent requestEvent):源码片段如下:
LocaleContextHolder.setLocale(request.getLocale());
RequestContextListener是web容器(如Tomcat)的请求事件,也就是说一旦有请求第1就会执行其requestInitialized(ServletRequestEvent requestEvent)方法,因此在这一步会获取request实例的一个本地化信息,注意此时本地化解析器LocaleResolver还没有工作。
2、FrameworkServlet的processRequest(HttpServletRequest request, HttpServletResponse response)方法,这个方法是在DispatcherServlet的doService方法之前执行,此方法源码片段如下:
1、先获取当前线程绑定好的LocaleContext实例,这里能够获取到在RequestContextListener里面设置的LocaleContext事实例。
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
2、使用请求实例解析一个LocaleContext实例,这里就会使用到LocaleResolver了,这里的实现在DispatcherServlet中。
LocaleContext localeContext = buildLocaleContext(request);
...
3、然后将解析到的LocaleContext实例再次进行线程绑定,会覆盖RequestContextListener里面设置的值。
initContextHolders(request, localeContext, requestAttributes);
DispatcherServlet中buildLocaleContext(final HttpServletRequest request)的实现:
@Override
protected LocaleContext buildLocaleContext(final HttpServletRequest request) {
1、判断配置的本地化解析器LocaleResolver是否实现了LocaleContextResolver接口,上面的类图里面有展示。
if (this.localeResolver instanceof LocaleContextResolver) {
2、如果是那就强转然后调用其resolveLocaleContext方法解析到一个LocaleContext实例
return ((LocaleContextResolver)
this.localeResolver).resolveLocaleContext(request);
}
else {
3、如果不是构建一个LocaleContext的匿名对象然后实现的getLocale就使用localeResolver解析到的Locale实例。
return new LocaleContext() {
@Override
public Locale getLocale() {
return localeResolver.resolveLocale(request);
}
};
}
}
我们以SessionLocaleResolver的resolveLocaleContext(final HttpServletRequest request)来举例:
@Override
public LocaleContext resolveLocaleContext(final HttpServletRequest request) {
构建一个匿名的TimeZoneAwareLocaleContext实例,获取Locale实例的方式是从
request实例的属性列表中取,那么问题来了什么时候LocaleResolver解析好的Locale实例什么时候放进去呢?
就是在setLocaleContext方法中放进去的,如果配置了LocaleChangeInterceptor的话,拦截器会构建好
Locale实例后调用setLocaleContext方法进行设置,如果没有的话就是直接从HttpSession中获取,前提是就需
要做我们之前的操作实现设置好,然后才能获取到。
return new TimeZoneAwareLocaleContext() {
@Override
public Locale getLocale() {
Locale locale = (Locale) WebUtils.getSessionAttribute(request, localeAttributeName);
if (locale == null) {
locale = determineDefaultLocale(request);
}
return locale;
}
@Override
public TimeZone getTimeZone() {
TimeZone timeZone = (TimeZone) WebUtils.getSessionAttribute(request, timeZoneAttributeName);
if (timeZone == null) {
timeZone = determineDefaultTimeZone(request);
}
return timeZone;
}
};
}
以上就是整个校验器的原理。