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

本文介绍JSR303规范及其在Java EE 6中的应用,包括Bean Validation的基本概念、Hibernate Validator的使用方法,以及如何在Spring MVC中进行数据校验。此外,还介绍了自定义注解校验及数据校验结果的全局处理。

JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。

【1】JSR 303

① 概述

JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean ValidationHibernate ValidatorBean 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,里面放的就是验证的的注解:
在这里插入图片描述

② 注解说明

限制说明
@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参数。
在这里插入图片描述
为什么?看 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除了会将表单对象的校验结果保存到BindingResultErrors对象之外,还会将所有校验结果保存到“隐含模型”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:不能为空;"
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流烟默

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值