zoukankan      html  css  js  c++  java
  • 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中添加即可。

    <!-- 打开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 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方法手工验证加不了分组这两种情况,就只能使用另外一种了。

  • 相关阅读:
    安装VMware Tools和设置屏幕
    线程
    制作数据集-解析篇
    制作数据集-应用篇
    tf.train.examle函数
    输入手写数字输出识别结果——分析篇
    输入手写数字输出识别结果
    断点续训
    UC972开发板,参考实验8,完成定时器触发信号输出实验
    hz和s和脉冲
  • 原文地址:https://www.cnblogs.com/vimisky/p/4864219.html
Copyright © 2011-2022 走看看