Spring MVC表单上传场景下的验证

Spring 验证、数据绑定和类型转换》那篇Spring官方Doc文档的翻译并没有涉及具体使用的细节,本篇结合Spring MVC表单数据上传这个通用应用场景写一下笔者的实践。

(转载请注明出处,谢谢)

  1. POST方式新增业务模型类,Spring MVC通过 @ModelAttribute 注解绑定接收参数,使用Spring Validator方式验证。
  2. PUT方式更新业务模型类,Spring MVC通过 @ModelAttribute 注解绑定接收参数,使用Spring Validator方式验证。
  3. PATCH方式更新业务模型类,Spring MVC通过 @ModelAttribute 注解绑定接收参数,使用Spring Validator方式验证。
  4. GET/POST name/value/pk参数更新业务模型类,Spring MVC 接收基本类型参数,使用JSR 303 API手动验证。

写在前面

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中添加即可。


<mvc:annotation-driven validator="validator" />





classpath:messages/messages
classpath:org/hibernate/validator/ValidationMessages
















待验证的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()+"\r\n");
}
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()+"\r\n");
}
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()+"\r\n");
}
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 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 categoryTypeDetailClass =(Class) 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> constraintViolations = validator.validate(categoryTypeDetail, PatchEntity.class);
if(constraintViolations.size()>0) {
Iterator<javax.validation.ConstraintViolation> iterator = constraintViolations.iterator();
StringBuffer errorString = new StringBuffer();
while(iterator.hasNext()){
javax.validation.ConstraintViolation 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方法手工验证加不了分组这两种情况,就只能使用另外一种了。

posted @ 2015-10-09 16:11  唯米天空  阅读(1711)  评论(0)    收藏  举报