JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。
【1】JSR 303
① 概述
JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint(约束)。
JSR303规范官网文档地址:https://jcp.org/en/jsr/detail?id=303
JSR 303 通过在Bean属性上标注类似于@NotNULL、@Max等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。
maven坐标:
<dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency>
下载之后打开这个包,有个package叫constraints,里面放的就是验证的的注解:
② 注解说明
【2】Hibernate Validator扩展注解
需要注意的是【1】中只是一个规范,想要使用必须注入实现,如Hibernate Validator。否则会抛出异常javax.validation.ValidationException: Unable to create a Configuration, because no Bean Validation provider could be found. Add a provider like Hibernate Validator (RI) to your classpath.
hibernate-validator pom依赖
<dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.18.Final</version> </dependency>
Hibernate Validator
是JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解。
@Email 被注释的元素必须是电子邮箱地址; @Length 被注释的字符串的大小必须在指定的范围内; @NotEmpty 被注释的字符串必须非空; @Range 被注释的元素必须在合适的范围内。
使用实例
public class LoginVo { @NotNull private String mobile; @NotNull @Length(min=32) private String password; ... }
像@NotNull、@Size等比较简单也易于理解,不多说。另外因为bean validation只提供了接口并未实现,使用时需要加上一个provider的包,例如hibernate-validator。需要特别注意的是@Pattern,因为这个是正则,所以能做的事情比较多,比如中文还是数字、邮箱、长度等等都可以做。
【3】SpringMVC 数据校验
默认会装配好一个LocalValidatorFactoryBean,通过在处理方法的入参上标注的@Valid注解,即可让SpringMVC在完成数据绑定后执行数据校验的工作。
即,在已经标注了JSR 303 注解的方法参数前标注一个@Valid,SpringMVC框架在将请求参数绑定到该入参对象后,就会调用校验框架根据注解声明的校验规则实施校验。
值得注意的是SpringMVC是通过对处理方法签名的规约来保存校验结果的。即:前一个参数的校验结果保存到随后的入参中,这个保存校验结果的入参必须是BindingResult或Errors类型。这两个类都位于org.springframework.validation包中。
① 复杂类型参数解析
ModelAttributeMethodProcessor.resolveArgument
方法源码如下:
@Override @Nullable public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer"); Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory"); String name = ModelFactory.getNameForParameter(parameter); ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class); if (ann != null) { mavContainer.setBinding(name, ann.binding()); } Object attribute = null; BindingResult bindingResult = null; if (mavContainer.containsAttribute(name)) { attribute = mavContainer.getModel().get(name); } else { // Create attribute instance try { attribute = createAttribute(name, parameter, binderFactory, webRequest); } catch (BindException ex) { if (isBindExceptionRequired(parameter)) { // No BindingResult parameter -> fail with BindException throw ex; } // Otherwise, expose null/empty value and associated BindingResult if (parameter.getParameterType() == Optional.class) { attribute = Optional.empty(); } bindingResult = ex.getBindingResult(); } } if (bindingResult == null) { // Bean property binding and validation; // 获取数据绑定对象 WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); if (binder.getTarget() != null) { if (!mavContainer.isBindingDisabled(name)) { //进行参数绑定 bindRequestParameters(binder, webRequest); } //进行参数校验 validateIfApplicable(binder, parameter); //如果校验结果有错且参数没有接受错误(参数后面没有Errors类型的参数),就抛出异常 if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new BindException(binder.getBindingResult()); } } // Value type adaptation, also covering java.util.Optional if (!parameter.getParameterType().isInstance(attribute)) { attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter); } bindingResult = binder.getBindingResult(); } // 更新bindingResult到model中 Map<String, Object> bindingResultModel = bindingResult.getModel(); mavContainer.removeAttributes(bindingResultModel); mavContainer.addAllAttributes(bindingResultModel); return attribute; }
② bean对象与绑定结果位置
需校验的Bean对象和其他绑定结果对象或错误对象是成对出现的,它们之间不允许声明其他的入参。当然,你不需要校验结果那么可以不声明BindingResult参数。
为什么?看 isBindExceptionRequired(binder, parameter)
方法源码如下:
protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) { return isBindExceptionRequired(parameter); } protected boolean isBindExceptionRequired(MethodParameter parameter) { int i = parameter.getParameterIndex(); Class<?>[] paramTypes = parameter.getExecutable().getParameterTypes(); //这里表明对象和错误接收对象成对出现 boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1])); return !hasBindingResult; }
【4】获取校验结果代码
① 常用方法
FieldError getFieldError(String field) ;
List getFieldErrors()
;Obejct getFieldValue(String field) ;
Int getErrorCount() 。
② 方法实例
@RequestMapping(value="/emp", method=RequestMethod.POST) public String save(@Valid Employee employee, Errors result, Map<String, Object> map){ System.out.println("save: " + employee); if(result.getErrorCount() > 0){ System.out.println("出错了!"); for(FieldError error:result.getFieldErrors()){ System.out.println(error.getField() + ":" + error.getDefaultMessage()); } //若验证出错, 则转向定制的页面 map.put("departments", departmentDao.getDepartments()); return "input"; } employeeDao.save(employee); return "forward:/emps";
方法参数上面需要用到@Valid(javax.validation.Valid)
注解,当进行数据绑定后会判断是否需要进行校验:
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] var3 = parameter.getParameterAnnotations(); int var4 = var3.length; for(int var5 = 0; var5 < var4; ++var5) { Annotation ann = var3[var5]; //检测是否需要进行校验,如果需要则进行校验 // 是否为Validated或者Valid Object[] validationHints = this.determineValidationHints(ann); if (validationHints != null) { binder.validate(validationHints); break; } } }
【5】在JSP页面上显示错误
SpringMVC除了会将表单对象的校验结果保存到BindingResult或Errors对象之外,还会将所有校验结果保存到“隐含模型”Model中。
即使处理方法的签名中没有对应于表单对象的校验结果入参,校验结果也会保存到“隐含模型”中。
隐含模型中的所有数据最终将通过HttpServletRequest的属性列表暴露给JSP视图对象,因此在JSP页面可以获取错误信息。
获取错误信息格式
<!--1. 获取所有的错误信息--> <form:errors path="*"></form:errors> <!--2.根据表单域的name属性获取单独的错误信息,如:--> <form:errors path="lastName"></form:errors> <!--path属性对应于表单域的name属性-->
【6】错误消息提示的国际化
每个属性在数据绑定和数据校验发生错误时,都会生成一个对应的FieldError对象。
当一个属性校验失败后,校验框架会为该属性生成4个消息代码,这些代码以校验注解类名为前缀,结合ModelAttribute,属性名与属性类型名生成多个对应的消息代码。
例如:User类中的password属性标注了一个@Pattern注解,当该属性值不满足@Pattern所定义的规则时,就会产生如下四个错误代码。
① Pattern.user.password ; ② Pattern.password ; ③ Pattern.java.lang.String ; ④ Pattern
当使用SpringMVC标签显示错误消息时,SpringMVC会查看web上下文是否装配了对应的国际化消息。如果没有,则显示默认的错误消息,否则使用国际化消息。
其他错误消息说明
若数据类型转换或数据格式化发生错误,或该有的参数不存在,或调用目标处理方法时发生错误,都会在隐含模型中创建错误消息。
其错误代码前缀说明如下:
- ① required : 必要的参数不存在。如
@RequiredParam("param1")
标注了一个入参但是该参数不存在; - ② typeMisMatch : 在数据绑定时,发生数据类型不匹配的问题;
- ③ methodInvocation : SpringMVC 在调用处理方法时发生了错误。
【7】错误消息提示国际化步骤
① 注册messageSource
<!-- 配置国际化资源文件 解析i18n.properties--> <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basename" value="i18n"></property> </bean>
② 编辑 i18n.properties
不要忘了两个孩子: i18n_en_US.properties
和i18n_zh_CN.properties
i18n.properties
如下所示:
NotEmpty.employee.lastName=LastName\u4E0D\u80FD\u4E3A\u7A7A. Email.employee.email=Email\u5730\u5740\u4E0D\u5408\u6CD5 Past.employee.birth=Birth\u4E0D\u80FD\u662F\u4E00\u4E2A\u5C06\u6765\u7684\u65F6\u95F4. typeMismatch.employee.birth=Birth\u4E0D\u662F\u4E00\u4E2A\u65E5\u671F. i18n.user=User i18n.password=Password
Java Bean 注解示例
public class Employee { private Integer id; @NotEmpty private String lastName; @Email private String email; //1 male, 0 female private Integer gender; private Department department; @Past @DateTimeFormat(pattern="yyyy-MM-dd") private Date birth; @NumberFormat(pattern="#,###,###.#") private Float salary; ...
错误消息提示如下图所示:
【8】自定义注解校验
有时框架自带的没法满足我们的需求,这时就需要自己动手丰衣足食了。如下所示,自定义校验是否为手机号。
①注解名字为 @IsMobile
import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {IsMobileValidator.class }) public @interface IsMobile { boolean required() default true; String message() default "手机号码格式错误"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; }
② IsMobileValidator处理类
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> { private boolean required = false; public void initialize(IsMobile constraintAnnotation) { required = constraintAnnotation.required(); } public boolean isValid(String value, ConstraintValidatorContext context) { if(required) { return ValidatorUtil.isMobile(value); }else { if(StringUtils.isEmpty(value)) { return true; }else { return ValidatorUtil.isMobile(value); } } } }
③ 使用注解
public class LoginVo { @NotNull @IsMobile private String mobile; @NotNull @Length(min=32) private String password; ... }
【9】数据校验结果全局处理
SpringMVC中异常处理与ControllerAdvice捕捉全局异常一文中说明了如何处理全局异常。那么针对数据校验我们也可以采用这种思路。
① 引入pom文件
<!--参数校验--> <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.18.Final</version> </dependency>
② model与方法校验
model加上校验注解
public class SysOrderLog implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty(value = "编号") @TableId(value = "id", type = IdType.AUTO) private Long id; @NotNull @ApiModelProperty(value = "预约ID") private Long orderId; @NotNull @ApiModelProperty(value = "审批人ID") private Long userId; //... }
方法上加上@Valid注解
@RequestMapping("check") @ResponseBody public ResponseBean checkOrder(@Valid SysOrderLog orderLog){ Long orderId = orderLog.getOrderId(); //... }
③ 全局异常处理
自然不能将校验结果直接抛出去,会非常难看,我们下面做下优化。
@ControllerAdvice @ResponseBody public class ControllerExceptionHandler { private final static Logger log = LoggerFactory.getLogger(ControllerExceptionHandler.class); @ExceptionHandler(value = {Exception.class}) public ResponseBean exceptionHandler(HttpServletRequest request, Exception e) { log.error("系统抛出了异常:{}{}",e.getMessage(),e); return ResultUtil.error(e.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseBean MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { Map<String, String> collect = e.getBindingResult().getFieldErrors().stream() .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage)); return ResultUtil.errorData(collect); } // 这个方法就是对校验结果异常进行优化处理 @ExceptionHandler(BindException.class) public ResponseBean BindException(BindException e) { Map<String, String> collect = e.getBindingResult().getFieldErrors().stream() .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage)); StringBuilder stringBuilder=new StringBuilder(); for(String key :collect.keySet()){ stringBuilder.append(key+":"+collect.get(key)).append(";"); } return ResultUtil.error(stringBuilder.toString()); } }
得到的优化后结果为:
{ "success": false, "data": null, "code": "9999", "msg": "orderId:不能为null;stateDetail:不能为null;userId:不能为null;content:不能为空;" }