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

简介: SpringMVC中使用JSR303进行数据校验实践详解

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,里面放的就是验证的的注解:

② 注解说明

image.png

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


目录
相关文章
|
缓存 监控 Java
深入了解Spring中的JSR 303验证和拦截器
深入了解Spring中的JSR 303验证和拦截器
109 0
|
监控 前端开发 安全
Spring-MVC使用JSR303及拦截器,增强网络隐私安全
Spring-MVC使用JSR303及拦截器,增强网络隐私安全
56 0
|
XML Java API
SpringMVC进阶-校验框架
SpringMVC进阶-校验框架
68 0
|
Java 数据库连接 API
Spring MVC-07循序渐进之验证器 下 (JSR 303验证)
Spring MVC-07循序渐进之验证器 下 (JSR 303验证)
81 0
|
前端开发 JavaScript Java
SpringBoot 项目中整合数据校验框架 hibernate-validator
SpringBoot 项目中整合数据校验框架 hibernate-validator
415 0
SpringBoot 项目中整合数据校验框架 hibernate-validator
|
Java Maven Spring
|
Java 数据库连接 数据安全/隐私保护
SpringMVC的文件上传与JSR303数据校验
JSR303是Java为Bean数据合法性校验提供给的标准框架,已经包含在 JavaEE6.0中、 JSR303通过在Bean 属性中标注类似 @NotNull @Max 等标准的注解指定校验规则,并通过标准的验证接口对 Bean进行验证
|
Java Spring
Spring - JSR-330 标准注解(二)
Spring - JSR-330 标准注解(二)
243 0
Spring - JSR-330 标准注解(二)
|
Java 数据库连接 API
深入了解数据校验(Bean Validation):ValidatorFactory和Validator等核心API【享学Java】(上)
深入了解数据校验(Bean Validation):ValidatorFactory和Validator等核心API【享学Java】(上)
深入了解数据校验(Bean Validation):ValidatorFactory和Validator等核心API【享学Java】(上)