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验证和拦截器
186 0
|
前端开发 数据安全/隐私保护
若依框架---权限控制角色设计
若依框架---权限控制角色设计
2705 0
|
程序员 人工智能 Serverless
通义灵码保姆级教程:官网、安装、使用指南、常见问题、线上活动、官方答疑
通义灵码保姆级教程:官网、安装、使用指南、常见问题、线上活动、官方答疑
21447 1
|
SQL 监控 NoSQL
架构师第一课,一文带你玩转 ruoyi 架构
我理解的架构/框架应该有以下功能: 1.满足日常开发功能,如单点登陆、消息队列、监控等; 2.规范开发者的开发,指定代码格式、注释等; 3.提高开发效率,提供一系列的封装方法,并减少bug的产生率。 下文将详细介绍ruoyi框架。
8072 1
架构师第一课,一文带你玩转 ruoyi 架构
|
XML Java Maven
jar包导入到项目中、本地maven仓库、私库
jar包导入到项目中、本地maven仓库、私库
2849 0
jar包导入到项目中、本地maven仓库、私库
|
10月前
|
存储 easyexcel Java
SpringBoot+EasyExcel轻松实现300万数据快速导出!
本文介绍了在项目开发中使用Apache POI进行数据导入导出的常见问题及解决方案。首先比较了HSSFWorkbook、XSSFWorkbook和SXSSFWorkbook三种传统POI版本的优缺点,然后根据数据量大小推荐了合适的使用场景。接着重点介绍了如何使用EasyExcel处理超百万数据的导入导出,包括分批查询、分批写入Excel、分批插入数据库等技术细节。通过测试,300万数据的导出用时约2分15秒,导入用时约91秒,展示了高效的数据处理能力。最后总结了公司现有做法的不足,并提出了改进方向。
|
Java API 对象存储
Spring揭秘:AnnotationMetadata接口应用场景及实现原理!
AnnotationMetadata接口可以轻松获取类、方法或字段上的注解信息,简化注解处理,提供一致且灵活的访问方式,支持运行时处理,让开发者能更专注于业务逻辑而非底层细节,从而加速开发进程。
542 0
Spring揭秘:AnnotationMetadata接口应用场景及实现原理!
|
Java 开发者 微服务
深入解析@SpringBootApplication注解:简化Spring Boot应用的配置
在现代的Java开发中,Spring Boot框架成为了构建微服务和快速开发应用的首选。Spring Boot的成功部分归功于其简化的配置和约定大于配置的理念。而`@SpringBootApplication`注解则是Spring Boot应用的入口,负责自动配置和启动Spring Boot应用。本文将深入探讨`@SpringBootApplication`注解的作用、用法,以及在Spring Boot应用中的应用场景。
1734 1
|
JavaScript
cnpm 的安装与使用
本文介绍了npm和cnpm的概念、安装nodejs的步骤,以及cnpm的安装和使用方法,提供了通过配置npm使用中国镜像源来加速包下载的替代方案,并说明了如何恢复npm默认仓库地址。
cnpm 的安装与使用
|
缓存 监控 安全
Spring AOP 详细深入讲解+代码示例
Spring AOP(Aspect-Oriented Programming)是Spring框架提供的一种面向切面编程的技术。它通过将横切关注点(例如日志记录、事务管理、安全性检查等)从主业务逻辑代码中分离出来,以模块化的方式实现对这些关注点的管理和重用。 在Spring AOP中,切面(Aspect)是一个模块化的关注点,它可以跨越多个对象,例如日志记录、事务管理等。切面通过定义切点(Pointcut)和增强(Advice)来介入目标对象的方法执行过程。 切点是一个表达式,用于匹配目标对象的一组方法,在这些方法执行时切面会被触发。增强则定义了切面在目标对象方法执行前、执行后或抛出异常时所
15990 4