《Spring 验证、数据绑定和类型转换》那篇Spring官方Doc文档的翻译并没有涉及具体使用的细节,本篇结合Spring MVC表单数据上传这个通用应用场景写一下笔者的实践。
(转载请注明出处,谢谢)
- POST方式新增业务模型类,Spring MVC通过 @ModelAttribute 注解绑定接收参数,使用Spring Validator方式验证。
- PUT方式更新业务模型类,Spring MVC通过 @ModelAttribute 注解绑定接收参数,使用Spring Validator方式验证。
- PATCH方式更新业务模型类,Spring MVC通过 @ModelAttribute 注解绑定接收参数,使用Spring Validator方式验证。
- GET/POST name/value/pk参数更新业务模型类,Spring MVC 接收基本类型参数,使用JSR 303 API手动验证。
写在前面
Spring中使用Validation的两种方式:
- Bean Validation1.0/1.1 (JSR-303/JSR-349),官方文档请查阅JSR303-spec,JSR303-Javadoc;JSR349-spec,JSR349-Javadoc
- Spring Validation
以上两个均不提供验证实现,Bean Validation默认实现由Hibernate Validation(官方文档请查阅Hibernate Validation - Reference)提供。
Bean Validation内嵌约束
注解 | 支持的数据类型 | 说明 | 版本 |
---|---|---|---|
@AssertFalse | Boolean, boolean | 检查被注解的元素是否为false | 1.0 |
@AssertTrue | Boolean, boolean | 检查被注解的元素是否为true | 1.0 |
@DecimalMax | BigDecimal, BigInteger, String, byte, short, int, long及封装对象,Number and CharSequence的子类型. | 检查被注解的元素是否为数字,且小于等于注解内的值 | 1.0 |
@DecimalMin | BigDecimal, BigInteger, String, byte, short, int, long及封装对象,Number and CharSequence的子类型. | 检查被注解的元素是否为数字,且大于等于注解内的值 | 1.0 |
@Digits(integer=, fraction=) | BigDecimal, BigInteger, String, byte, short, int, long及封装对象,Number and CharSequence的子类型. | 检查被注解的元素是否为数字,且符合要求范围,integer参数表示整数位数,fraction表示小数位数. | 1.0 |
@Future | java.util.Date, java.util.Calendar, ReadablePartial和ReadableInstant的实现类;如果classpath中有joda time库,支持; | 检查被注解的时间是否是将来时间 | 1.0 |
@Max | BigDecimal, BigInteger, String, byte, short, int, long及封装对象,Number and CharSequence的子类型. | 检查被注解的元素是否小于等于注解内的值 | 1.0 |
@Min | BigDecimal, BigInteger, String, byte, short, int, long及封装对象,Number and CharSequence的子类型. | 检查被注解的元素是否大于等于注解内的值 | 1.0 |
@NotNull | 任何类型 | 检查被注解的元素是否不为null | 1.0 |
@Null | 任何类型 | 检查被注解的元素是否为null | 1.0 |
@Past | java.util.Date, java.util.Calendar, ReadablePartial和ReadableInstant的实现类;如果classpath中有joda time库,支持; | 检查被注解的时间是否是过去时间 | 1.0 |
@Pattern(regex=, flag=) | String, CharSequence的子类型. | 检查被注解的元素是否符合正则表达式 | 1.0 |
@Size(min=, max=) | String, Collection, Map, arrays以及CharSequence的子类型. | 检查被注解元素的Size是否在min和max范围内 | 1.0 |
@Valid | 任意非基本类型 | 对被注解元素对象执行验证。如果对象是集合类collection或者array,对集合类中的元素进行逐一验证。如果对象是map,则对value进行验证 | 1.0 |
Bean Validation约束保留属性
- groups,用于分组验证,支持同一个待验证对象在不同验证场景下的不同验证规则
- message,验证错误后的提示信息
- payload,验证负载
版本兼容性
- JDK1.6、Spring 3、JSR-303、Hibernate Validaiton 4
尽量匹配,笔者使用Spring 3.2.4 + Hibernate Validation 5.1.3.Final,报错,回退为Hibernate Validation 4.2.0.Final,正常。
本篇主要使用JSR-303。
Spring环境配置
在Spring MVC配置文件中注入validator,在validator中注入全局验证器和验证消息类。
局部validator在具体Controller类的DataBinder中添加即可。
<!-- 打开Spring MVC注解驱动 -->
<mvc:annotation-driven validator="validator" />
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<!--对应classpath下的messages文件夹下的messages.properties和messages_zh_CN.properties以及其它国际化文件-->
<!--如果有多个消息国际化文件,在下面添加即可,value不需要添加文件扩展名-->
<value>classpath:messages/messages</value>
<value>classpath:org/hibernate/validator/ValidationMessages</value>
</list>
</property>
<!-- 默认值为none, 使用java.utils.properties -->
<property name="defaultEncoding" value="UTF-8" />
<!-- 默认值为-1, 缓存永远不刷新 -->
<property name="cacheSeconds" value="60" />
<!-- 父类AbstractMessageSource该属性默认值为false -->
<!--
<property name="useCodeAsDefaultMessage" value="false" />
-->
</bean>
<!-- mvc:annotation-driven默认会注入该类,但是如果需要定制,显式注入-->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<!-- 指定自定义的Spring MessageSource对象,用来解析验证消息,替换JSR-303默认的classpath根路径下的ValidationMessages.properties文件-->
<property name="validationMessageSource" ref="messageSource" />
<!-- 指定Validation实现类,若不指定,JSR-303按照默认机制查找,即在classpath中查找 -->
<!-- 注意,这里并没有使用ref, 因为参数是类名称,而不是对象-->
<property name="providerClass" value="org.hibernate.validator.HibernateValidator" />
</bean>
待验证的JavaBean
Java Bean Validation规范中,有3类验证注解。
- 对象注解,注解在类定义上,值为对象验证器。
- 字段/属性注解,可以注解在字段上,也可以注解在getter方法上。
- 图注解,笔者认为可以理解为级联注解,待验证类中不止包含基本类型(String、Integer等)属性,还包含类类型。使用级联注解来对验证进行解耦,验证只在本级执行,对象属性的验证交给对象自己的验证规则执行。
本篇使用Field注解。
分组验证
使用约束的groups参数,类型为数组,数组中的元素类型为Class
验证消息
本篇使用的是Spring 的 LocalValidationFactoryBean对象,在该对象中注入了messageSource对象,使用国际化文件来根据用户Locale显示不同语种的消息。
注入的实现MessageSource接口的具体类为org.springframework.context.support.ReloadableResourceBundleMessageSource,相比于org.springframework.context.support.ResourceBundleMessageSource,可以设置刷新时间,在指定时间过期后,重新载入message配置文件,及时更新message字符串所代表的实际文本。
为message属性赋值国际化文件中的字符串,加入了EL表达式。在J2EE classpath中要求javax.el包。
类定义
public class CategoryTypeDetail {
@NotNull(groups={UpdateEntity.class},message="{com.vimisky.dms.backend.restful.id.notnullerror}")
@Min(groups={UpdateEntity.class,PatchEntity.class},value=1,message="{com.vimisky.dms.backend.restful.id.minerror}")
@Max(groups={UpdateEntity.class,PatchEntity.class},value=10000000,message="{com.vimisky.dms.backend.restful.id.maxerror}")//本处验证不严谨
private int id;
@NotNull(groups={NewEntity.class}, message="{com.vimisky.dms.backend.restful.name.notnullerror}")
@Size(groups={NewEntity.class,UpdateEntity.class,PatchEntity.class},min=1,max=255,message="{com.vimisky.dms.backend.restful.name.lengtherror}")
private String name;
@Size(groups={NewEntity.class,UpdateEntity.class,PatchEntity.class},min=0,max=255,message="{com.vimisky.dms.backend.restful.secondaryname.lengtherror}")
private String secondaryName;
//如果没有定义分组,而在应用validated时,指定了分组,则不会使用该条验证约束
@Size(groups={NewEntity.class,UpdateEntity.class,PatchEntity.class},min=0,max=1024,message="{com.vimisky.dms.backend.restful.description.lengtherror}")
private String description;
@Size(groups={NewEntity.class,UpdateEntity.class,PatchEntity.class},min=0,max=255,message="{com.vimisky.dms.backend.restful.language.lengtherror}")
private String language;
@Size(groups={NewEntity.class,UpdateEntity.class,PatchEntity.class},min=0,max=255,message="{com.vimisky.dms.backend.restful.thumbnailurl.lengtherror}")
private String thumbnailUrl;
//如果没有注释不能为空,则为空时,不检验Size等
@Size(groups={NewEntity.class,UpdateEntity.class,PatchEntity.class},min=1,max=255,message="{com.vimisky.dms.backend.restful.thumbnailuri.lengtherror}")
private String thumbnailUri;
@Size(groups={NewEntity.class,UpdateEntity.class,PatchEntity.class},min=0,max=255,message="{com.vimisky.dms.backend.restful.thumbnailicon.lengtherror}")
private String thumbnailIcon;
@Size(groups={NewEntity.class,UpdateEntity.class,PatchEntity.class},min=0,max=255,message="{com.vimisky.dms.backend.restful.code.lengtherror}")
private String code;
@Null(groups={NewEntity.class,UpdateEntity.class,PatchEntity.class}, message="{com.vimisky.dms.backend.restful.createtime.nullerror}")
private Date createTime;
@Null(groups={NewEntity.class,UpdateEntity.class,PatchEntity.class},message="{com.vimisky.dms.backend.restful.lastmodifytime.nullerror}")
private Date lastModifyTime;
public CategoryTypeDetail(){
super();
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSecondaryName() {
return secondaryName;
}
public void setSecondaryName(String secondaryName) {
this.secondaryName = secondaryName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public String getThumbnailUrl() {
return thumbnailUrl;
}
public void setThumbnailUrl(String thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl;
}
public String getThumbnailUri() {
return thumbnailUri;
}
public void setThumbnailUri(String thumbnailUri) {
this.thumbnailUri = thumbnailUri;
}
public String getThumbnailIcon() {
return thumbnailIcon;
}
public void setThumbnailIcon(String thumbnailIcon) {
this.thumbnailIcon = thumbnailIcon;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
public Date getLastModifyTime() {
return lastModifyTime;
}
public void setLastModifyTime(Date lastModifyTime) {
this.lastModifyTime = lastModifyTime;
}
@Override
public String toString() {
return "CategoryTypeDetail [id=" + id + ", name=" + name
+ ", secondaryName=" + secondaryName + ", description="
+ description + ", language=" + language + ", thumbnailUrl="
+ thumbnailUrl + ", thumbnailUri=" + thumbnailUri
+ ", thumbnailIcon=" + thumbnailIcon + ", code=" + code
+ ", createTime=" + createTime + ", lastModifyTime="
+ lastModifyTime + "]";
}
}
为支持分组验证,新建三个接口
新建
public interface NewEntity {
}
部分更新
public interface PatchEntity {
}
完全更新
public interface UpdateEntity {
}
POST+ModelAttribute
Spring MVC使用WebDataBinder类来管理属性编辑器PropertyEditor和验证器validator等,用来进行数据绑定、数据类型转换和数据验证。如果需要在Spring MVC中增加局部验证器,可以通过webDataBinder.addValidator(...)方法加入。本章中由于只使用Field/Property验证,Java Bean Validation即可满足,不需要另行新增类验证器。
使用Spring MVC的 @ModelAttribute 注解绑定表单数据到业务对象时,在前面增加验证注解 @Valid (Java Bean Validation 标准) 或者 @Validated (Spring 特有)。
本篇使用了分组验证,而 @Valid 没有分组验证的参数,只能使用 @Validated 。在业务对象参数后面,需要紧跟一个实现了Errors接口的对象。在Spring MVC中,BindingResult接口为Errors子接口,由Spring MVC注入具体实现对象。
@RequestMapping(value="/categorytype", method=RequestMethod.POST)
@ResponseBody
public RestfulResult insertCategoryType(
@Validated(value={NewEntity.class}) @ModelAttribute CategoryTypeDetail categoryTypeDetail,
BindingResult bResult,
HttpServletRequest webRequest){
logger.debug("接收到的分类类型数据为:"+categoryTypeDetail.toString());
//在客户端的Request头中,增加Content-Type:x-www-form-urlencoded;charset=utf8;这里就能获取到,否则获取不到
logger.debug(webRequest.getCharacterEncoding());
try {
webRequest.setCharacterEncoding("");
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//补偿验证,避免用户上传ID
categoryTypeDetail.setId(0);
if(bResult.hasErrors()){
StringBuffer errorString = new StringBuffer();
logger.error("分类类型数据新增接口接收表单数据出现错误:");
//对前端而言,不需要显示全部错误
for (ObjectError objectError : bResult.getAllErrors()) {
logger.error(objectError.getObjectName()+"赋值验证出错:"+objectError.getDefaultMessage()+",错误代码为:"+objectError.getCode());
}
for (FieldError fieldError : bResult.getFieldErrors()){
logger.error(fieldError.getObjectName()+"的"+fieldError.getField()+"字段赋值验证出错:"+fieldError.getDefaultMessage()+",错误代码为:"+fieldError.getCode());
errorString.append(fieldError.getDefaultMessage()+"
");
}
return new RestfulResult(false, "表单提交数据错误:"+errorString);
}
OperationServiceResult osr = this.backendCategoryService.insertCategoryType(categoryTypeDetail);
return new RestfulResult(osr);
}
PUT+ModelAttribute
为支持Method 为PUT的HTTP Request,需要增加一个响应Method 为OPTIONS的接口。
@RequestMapping(value="/categorytype/{id}", method=RequestMethod.OPTIONS)
@ResponseBody
public RestfulResult updateCategoryTypeOPTIONS(@PathVariable int id){
logger.info("响应HTTP OPTIONS Method");
return new RestfulResult(true);
}
另外,需要增加一个拦截器,修改HTTP Response报头,Accept-Method中增加PUT。(超出本篇范围,不再粘贴相关代码)
PUT更新的验证规则见上面“待验证的JavaBean”,groups里含有UpdateEntity.class的注解。
@RequestMapping(value="/categorytype/{ctid}", method=RequestMethod.PUT)
@ResponseBody
public RestfulResult updateCategoryType(
@PathVariable int ctid,
@Validated(value={UpdateEntity.class}) @ModelAttribute CategoryTypeDetail categoryTypeDetail,
BindingResult bResult){
logger.info("通过HTTP PUT方法对分类"+categoryTypeDetail.getName()+"进行全部升级");
if(bResult.hasErrors()){
StringBuffer errorString = new StringBuffer();
logger.error("分类类型数据更新接口接收表单数据出现错误:");
//对前端而言,不需要显示全部错误
for (ObjectError objectError : bResult.getAllErrors()) {
logger.error(objectError.getObjectName()+"赋值验证出错:"+objectError.getDefaultMessage()+",错误代码为:"+objectError.getCode());
}
for (FieldError fieldError : bResult.getFieldErrors()){
logger.error(fieldError.getObjectName()+"的"+fieldError.getField()+"字段赋值验证出错:"+fieldError.getDefaultMessage()+",错误代码为:"+fieldError.getCode());
errorString.append(fieldError.getDefaultMessage()+"
");
}
return new RestfulResult(false, "表单提交数据错误:"+errorString);
}
//该判断补充验证框架中的值验证
if(categoryTypeDetail.getId() != ctid){
logger.warn("URL资源ID与表单ID数据不一致");
return new RestfulResult(false, "URL资源ID与表单ID数据不一致");
}
OperationServiceResult osr = this.backendCategoryService.updateCategoryType(categoryTypeDetail);
return new RestfulResult(osr);
}
PATCH+ModelAttribute
为支持Method 为PATCH的HTTP Request,需要增加一个响应Method 为OPTIONS的接口。
@RequestMapping(value="/categorytype/{id}", method=RequestMethod.OPTIONS)
@ResponseBody
public RestfulResult updateCategoryTypeOPTIONS(@PathVariable int id){
logger.info("响应HTTP OPTIONS Method");
return new RestfulResult(true);
}
另外,需要增加一个拦截器,修改HTTP Response报头,Accept-Method中增加PUT。(超出本篇范围,不再粘贴相关代码)
PATCH更新的验证规则见上面“待验证的JavaBean”,groups里含有PatchEntity.class的注解。
@RequestMapping(value="/categorytype/{id}", method=RequestMethod.PATCH)
@ResponseBody
public RestfulResult updateCategoryTypePart(
@PathVariable int id,
@Validated(value={PatchEntity.class}) @ModelAttribute CategoryTypeDetail categoryTypeDetail,
BindingResult bResult){
logger.info("通过HTTP PATCH方法对分类类型进行部分升级");
logger.debug("原始CategoryType为:"+categoryTypeDetail.toString());
if(bResult.hasErrors()){
StringBuffer errorString = new StringBuffer();
logger.error("分类类型部分数据更新接口接收表单数据出现错误:");
//对前端而言,不需要显示全部错误
for (ObjectError objectError : bResult.getAllErrors()) {
logger.error(objectError.getObjectName()+"赋值验证出错:"+objectError.getDefaultMessage()+",错误代码为:"+objectError.getCode());
}
for (FieldError fieldError : bResult.getFieldErrors()){
logger.error(fieldError.getObjectName()+"的"+fieldError.getField()+"字段赋值验证出错:"+fieldError.getDefaultMessage()+",错误代码为:"+fieldError.getCode());
errorString.append(fieldError.getDefaultMessage()+"
");
}
return new RestfulResult(false, "表单提交数据错误:"+errorString);
}
//与完全更新不同,表单中无需提供ID,使用URL中的ID即可
if(categoryTypeDetail.getId() == 0)
categoryTypeDetail.setId(id);
OperationServiceResult osr = backendCategoryService.updateCategoryTypePart(categoryTypeDetail);
return new RestfulResult(osr);
}
GET/POST + name/value/pk
这种方式是为了适应更快速的更改单个属性,上传表单时,参数为属性名(name),属性值(value),主键ID(pk)。
首先通过Java反射机制,为实体类赋值,然后手动调用javax.validation.validator进行验证。这里选择java标准validator的原因是,Spring validator并未提供分组验证的参数。
Spring validator的API为 validator.validate(Object obj, Errors errors),而java标准validator API为validator.validate(Object obj, Class
但是在注入validator时,使用Spring配置文件中的LocalValidatorFactoryBean即可。可以通过 @Autowired 注入,javax.validation.Validator validator;
@RequestMapping(value="/categorytype/updatefield", method=RequestMethod.POST, params={"name","value","pk"})
@ResponseBody
public RestfulResult updateCategoryTypeField(
@RequestParam(value="name", required = true) String name,
@RequestParam(value = "value", required = true) String value,
@RequestParam(value = "pk", required = true) int pk,
WebRequest webRequest){
//验证
try {
Class<CategoryTypeDetail> categoryTypeDetailClass =(Class<CategoryTypeDetail>) Class.forName("com.vimisky.dms.entity.backend.CategoryTypeDetail");
CategoryTypeDetail categoryTypeDetail = categoryTypeDetailClass.newInstance();
Field field = categoryTypeDetailClass.getDeclaredField(name);
field.setAccessible(true);
field.set(categoryTypeDetail, value);
categoryTypeDetail.setId(pk);
Set<javax.validation.ConstraintViolation<CategoryTypeDetail>> constraintViolations = validator.validate(categoryTypeDetail, PatchEntity.class);
if(constraintViolations.size()>0) {
Iterator<javax.validation.ConstraintViolation<CategoryTypeDetail>> iterator = constraintViolations.iterator();
StringBuffer errorString = new StringBuffer();
while(iterator.hasNext()){
javax.validation.ConstraintViolation<CategoryTypeDetail> constraintViolation = iterator.next();
logger.warn(constraintViolation.getMessage());
errorString.append(constraintViolation.getMessage());
}
return new RestfulResult(false, "表单提交数据错误:"+errorString);
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return new RestfulResult(false,"服务器后台出错");
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return new RestfulResult(false,"服务器后台出错");
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return new RestfulResult(false,"服务器后台出错");
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return new RestfulResult(false,"服务器后台出错");
} catch (NoSuchFieldException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return new RestfulResult(false,"服务器后台出错");
}
OperationServiceResult osr =
backendCategoryService.updateCategoryTypePart(pk, name, value);
return new RestfulResult(osr);
}
写在后面
源码中的RestfulResult类和OperationServiceResult类是为前端服务而自定义的类,比较简单,与具体实现的业务相关,与本文无关,所以不贴了。
关于Bean Validation自定义约束,在以后的博文中再单写。
关于Bean ValidationValidator接口自定义实现类,在以后的博文中再单写。可以在Spring MVC中通过给databinder添加validator的方式加入。
关于Bean Validation和Spring Validation的框架选择上,使用自己熟悉的就好,但是遇到的 @Valid 注解加不了分组,Spring validator validate方法手工验证加不了分组这两种情况,就只能使用另外一种了。