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,里面放的就是验证的的注解:
SpringMVC中使用JSR303进行数据校验实践详解_hibernate

② 注解说明

限制

说明

@Null

限制只能为null

@NotNull

限制必须不为null

@AssertFalse

限制必须为false

@AssertTrue

限制必须为true

@DecimalMax(value)

限制必须为一个不大于指定值的数字

@DecimalMin(value)

限制必须为一个不小于指定值的数字

@Digits(integer,fraction)

限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction

@Future

限制必须是一个将来的日期

@Max(value)

限制必须为一个不大于指定值的数字

@Min(value)

限制必须为一个不小于指定值的数字

@Past

限制必须是一个过去的日期

@Pattern(value)

限制必须符合指定的正则表达式

@Size(max,min)

限制字符长度必须在min到max之间

@Past

验证注解的元素值(日期类型)比当前时间早

@NotEmpty

验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)

@NotBlank

验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格

@Email

验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式


【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 数据校验

​<mvc:annotation-driven/>​​​默认会装配好一个​​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参数。
SpringMVC中使用JSR303进行数据校验实践详解_java_02
为什么?看​​ 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<FieldError> 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;
...

错误消息提示如下图所示:

SpringMVC中使用JSR303进行数据校验实践详解_hibernate_03


【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:不能为空;"
}