Spring:深入分析SpringMVC之请求数据格式化与校验

1 数据格式化

Spring使用转换器进行源类型对象到目标类型对象的转换,Spring的转换器并不提供输入及输出信息格式化的工作。如果需要转换的源类型数据是从客户端界面传递过来的,为了方便使用者观看,这些数据往往具有一定的格式。举例来说,像日期、时间、数字、货比等数据都是具有一定格式的,在不同的本地化环境中,同一类型的数据还会响应的呈现不同的显示格式。

如何从格式化的数据中获取真正的数据以完成数据绑定,并将处理完成的数据输出为格式化的数据,是Spring格式化框架要解决的位。Spring引入了一个新的格式化框架,这个框架位于org.springframework.format类包中:

Formatter<T>

Formatter<T>接口扩展于Printer<T>和Parser<T>接口。

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

Printer负责对象的格式化输出,而Parser负责对象的格式化输入,在接口中各定义了一个方法。先来看一下Printer接口的方法

public interface Printer<T> {
    //将类型为T的成员对象根据本地化的不同输出为不同的格式化的字符串。
	String print(T object, Locale locale);

}

Parser接口方法:

public interface Parser<T> {
    //参考本地化信息将一个格式化的字符串转换为T类型的对象,即完成格式化对象的输入工作。
	T parse(String text, Locale locale) throws ParseException;

}

Spring的org.springframework.format.datetime包中提供了一个用于时间对象格式化的DateFormatter实现类;而org.springframework.format.number包中提供了3个用于数字对象格式化的实现类。

[1] NumberFormatter:用于数字类型对象的格式化。

[2] CurrencyFormatter:用于货币类型对象的格式化。

[3] PercentFormatter:用于百分数数字类型对象的格式化。

可以手工调用这些Formatter接口实现类进行对象数据输入/输出的格式化工作,但是随着Java技术的发展,这种硬编码的格式化方式显然已经过时。Spring提供了注解驱动的属性对象格式化功能,可在Bean属性设置、SpringMVC处理方法入参数据绑定、模型数据输出时自动通过注解应用格式化功能。

注解驱动格式化的重要接口

为了让注解和格式化的属性类型关联起来,Spring在Formatter<T>所在的包中还提供了一个AnnotatioFormatterFactory<A extends Annotation>接口。来看下该接口的几个方法:

[1] Set<Class<?>> getFieldTypes();注解A的应用范围,即哪些属性类可以标注A注解。

[2] Printer<?> getPrinter(A var1, Class<?> var2);根据注解A获取特定属性类型的Parser。

[3] Parser<?> getParser(A var1, Class<?> var2);根据注解A获取特定属性类型的Printer。

Spring在4.0就提供了两个内建的实现类,分别支持数字及日期来兴的注解驱动格式化:

[1] NumberFormatAnnotationFormatterFactory : 支持对数字类型的属性使用@NumberFormat注解。

[2] JodaDateTimeFormatAnnotationFormatterFactory :支持对日期类型的属性使用@DateTimeFormat注解

其余三个是在后续4.1和4.2中添加的,大家可以自行了解下

启用注解驱动格式化功能

对属性对象的输入/输出进行格式化,从本质上讲依然属于类型转换的范畴。Spring就是基于对象装换框架织入格式化功能的。

Spring在格式化模块中定义了一个实现ConversionService接口的FormattingConversionService实现类,该实现类扩展了GenericConversionService,因此它即具有类型转换功能,又具有格式化功能。

相对于ConversionService的ConversionServiceFactoryBean工厂类,FormattingConver-sionService也拥有一个对应的FormattingConversionServiceFactoryBean工厂类,后者用于在Spring上下文中构造一个FormattingConversionService。通过这个工厂类,即可以注自定义的转换器,还可以注册自定义的注解驱动逻辑。

由于FormattingConversionServiceFactoryBean在内部会自动注册NumberFormatAnnotationFormatterFactory和JodaDateTimeFormatAnnotationFormatterFactory,因此,装配了FormattingConversionServiceFactoryBean后,就可以在SpringMVC入参绑定及模型数据输出时使用注解驱动的格式化功能。

首先在Spring上下文中装配FormattingConversionServiceFactoryBean:

<mvc:annotation-driven conversion-service="conversionService"/>
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
	<property name="converters">
		<list>...</list>
	</property>
</bean>

值得注意的是,<mvc:annotation-driven/>标签内部默认创建的ConversionService实例就是一个FormattingConversionServiceFactoryBean。装配好FormattingConversionServiceFactoryBean后,SpringMVC对处理方法的入参绑定就支持注解驱动功能了。

注解驱动格式化实例

在User中添加两个新属性,并标注格式化注解:

public class User{
	//可将如 2019-02-21形式的字符串转成Date类型的birthady属性中
	@DateTimeFormat(pattern="yyyy-MM-dd")
	private Date birthady;
	
	//可将如 1,234.56形式的字符串转换成long类型的salary中
	@NumberFormat(pattern="#,###.##")
	private long salary;
}

@DateTimeFormat注解可对java.util.Date、java.util.Calendar、java.long.Long及Joda时间类型的属性进行标注。因为Spring4.0就已支持java 8 所以也可以使用JDK 8 中的java.time包。它支持以下几个互斥的属性:

[1] iso:类型为DateTimeFormat.ISO 常用可选值有:
    DateTimeFormat.ISO.DATE        :格式为yyyy-MM-dd
    DateTimeFormat.ISO.DATE_TIME:格式为yyyy-MM-dd hh:mm:ss.SSSZ
    DateTimeFormat.ISO.TIME        :格式为hh:mm:ss.SSSZ
    DateTimeFormat.ISO.NONE        :表示不应该使用ISO格式的时间

[2] pattern:类型为String,使用自定义的时间格式化串,如:yyyy/MM/dd hh:mm:ss

[3] style:类型为String,通过样式指定日期/时间的格式,由两位字符组成,第一位表示日期的样式,第二位表示时间的样式:
    S:短日期/时间的样式。
    M:中日期/时间的样式。
    L:长日期/时间的样式。
    F:完整日期/时间的样式。
    -:忽略日期/时间的样式。

而@NumberFormat可对类似于数字类型的属性进行标注,它拥有两个互斥的属性:

[1] pattern:类型为String,使用自定义的数字格式化串,如 ##,###.##

[2] style:类型为NumberFormat.Style。常用可选值有:
    NumberFormat.Style.CURRENCY :货币类型。
    NumberFormat.Style.NUMBER    :正常数字类型。
    NumberFormat.Style.PERCENT    :百分数类型。

如果希望在视图页面中将模型属性数据也以格式化的方式进行渲染,则可以通过Spring的页面标签显示模型数据来达到目的。

2 数据校验

应用程序在执行业务逻辑前,必须通过数据校验保证接收到的输入数据是正确合法的,如代表生日的日期应该是一个过去的时间、工资的数值必须是一个整数等。一般情况下,应用程序的开发时分层的,不同层的代码由不同的开发人员负责。很多时候,同样的数据验证会出现在不同的层中,这样就会导致代码冗余,违反了DRY原则。为了避免这样的情况,最好将验证逻辑和响应的域模型进行绑定,将代码验证的逻辑集中起来管理。

JSR-303

JSR-303是Java为Bean数据合法校验锁提供的标准框架,它已经包含在JavaEE 6.0中。JSR-303通过在Bean属性上标注类似于@NotNull、@Max等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。可以通过http://jcp.org/en/jsr/detail?id=303了解更多详细内容。

JSR-303定义了一套可标注在成员变量、属性方法上的校验注解:

Spring校验框架

Spring拥有自己独立的数据校验框架,同时支持JSR-303标准的校验框架。Spring的DataBinder在进行数据绑定时,可同时调用校验框架完成数据校验工作。在SpringMVC中,则可直接通过注解驱动的方式进行数据校验。

Spring的org.springframework.validation是校验框架所在的包,下面来了解一下校验框架的几个重要的接口和类。

[1] Validator:

boolean supports(Class<?> clazz);该校验器能够对clazz类型的对象进行校验。

void validate(Object target, Errors errors);对目标类target进行校验,并将校验错误记录在errors中

LocalValidatorFactoryBean即实现了Spring的Validator接口,又实现了JSR-303的Validator接口。只要在Spring容器中定义了一个LocalValidatorFactoryBean,即可将其注入需要数据校验的Bean中。定义LocalValidatorFactoryBean的Bean非常简单:

<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

值得注意的是:Spring本身并没有提供JSR-303的实现,所以必须将JSR-303的是闲着的JAR文件放到类路径下,Spring将自动加载并装配好JSR-303的实现者。

SpringMVC数据校验

<mvc:annotation-driven/>会默认装配一个LocalValidatorFactoryBean,通过在处理方法的入参上标注@Valid注解即可让SpringMVC在完成数据绑定后执行数据校验工作。

public class User{
	
	//通过正则表达式进行校验,匹配4~30个包含数字、字母及下划线的字符
	@Pattern(regexp="w{4,30}")
	private String userName;
	
	//通过正则表达式进行校验,匹配6~30个非空白字符
	@Pattern(regexp="S{6,30}")
	private String password;
	
	//Hibernate Validator的扩展度,限制属性值长度为2~100之间
	@Length(min=2,max=100)
	private String realName;
	
	//限制时间必须是一个过去的时间
	@Past
	@DateTimeFormat(pattern="yyyy-MM-dd")
	private Date birthady;
	
	//属性值必须在100.00~1000.00之间
	@DecimalMin(value="100.00")
	@DecimalMax(value="1000.00")
	@NumberFormat(pattern="#,###.##")
	private long salary;
}

在属性上标注校验注解后,接下来的问题就是如何在SpringMVC中使用注解锁声明的限制规则进行数据校验。

//在要校验的对象前添加@Valid注解,然后在参数中添加一个BindingResult参数对象
@RequestMapping(path="addUser")
public String addUser (@Valid @ModelAttribute("user")User user,BindingResult br){
	//存在参数校验错误则返回error
	if(br.hasErrors()){
		return "error";
	}
	return "success";
}

SpringMVC是通过对处理方法签名的规约来保存校验结果的:前一个表单对象的校验结果保存在其后的入参中,这个保存校验结果的入参必须为BindingResult或Errors类型。也就是说 如果有一个!@Valid注解那么这个注解对应的对象后面就要添加一个BindingResult或Errors类型的参数用来接收验证结果。

需要验证的表单/命令对象和其绑定结果对象或错误对象时成对出现的,它们之间不允许声明其他入参。

Errors接口提供了获取错误信息的方法,如getErrorCount()方法获取错误的数量,getFieldErrors(sTRING FIELD)方法得到成员属性的校验错误列表。而BindingResult接口扩展了Errors接口,以便可以使用Spring的org.springframework.validation.Validator对数据进行校验,同时获取数据绑定结果对象的信息。BindingResult接口通过DataBinder.getBindingResult()方法获取。

如何获取校验结果

在表单/命令对象类的属性中标注校验注解,在处理方法对应 入参前添加@Valid,SpringMVC就会实施校验并将校验结果保存在被校验入参对象之后的BindingResult或Errors入参中。

在处理方法内部可以通过BindingResult或Errors入参对象获取错误信息,这样就可以通过br.hasErrors()方法判断User入参对象时否存在校验错误。还可以通过接口方法获取更多的信息,以下是几个常用方法:

[1] FieldError getFieldError(String field);根据属性名获取对应的校验错误。

[2] List<FieldError> getFieldErrors();获取所有的属性校验错误。

[3] Object getFieldValue(String field);获取属性值。

[4] int getErrorCount();获取错误数量。

如何在页面中显示错误

由于表单/命令对象锁对应的请求一般是从客户端的网页中传过来的,因此,如果发生了错误,则必须要有返回的响应信息。

SpringMVC除了将表单/命令对象的校验结果保存到对应的BindingResult或Errors对象中外,还将所有的校验结果保存到隐含模型中。也就是说即使处理方法的签名中没有对应表单/命令对象的校验结果入参,校验结果也不会丢失,他们始终可以从隐含模型对象中获取。校验结果对象和被校验的表单/命令对象是一对一的关系,但隐含模型除了存储模型数据外,还保存了所有被校验的表单/命令对象的校验结果。隐含模型中的所有数据最终将通过HttpServletRequest的属性列表暴露给JSP视图对象,因此我们可以很容易的在JSP中获取校验错误信息。

[1] HTTP请求报文叨叨Web服务器,Web服务器将其封装成一个HttpServletRequest对象。

[2] SpringMVC框架截获这个HttpServletRequest对象。

[3] SpringMVC创建一个隐含模型对象,作为处理本次请求的上下文数据存放处。

[4] SpringMVC将HttpServletRequest对象数据绑定到处理方法的入参对象中。

[5] 将绑定错误信息、检验错误信息都保存到隐含模型中。

[6] 将本次请求的对应隐含模型数据存放到HttpServletRequest属性列表中,暴露给视图对象。

[7] 视图对象对已经存放在HttpServletRequest属性列表中的模型数据进行渲染。

[8] 将渲染后的HTTP响应报文发送给客户端。

理解了SpringMVC的整体处理过程后,可以肯定的是,校验错误信息一定包含在隐含模型中,并且会通过HttpServletRequest暴露给视图对象。

可通过Spring的<form:errors path="propName"/>标签在JSP页面中显示错误信息

需要在表头添加taglib标签引入Spring表单标签:

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

通过国际化资源显示错误信息

每个属性在数据绑定和数据校验发生错误时,都会生成一个对应的FieldError对象。FieldError对象实现了org.springframework.context.MessageSourceResolvable接口,MessageSourceResolvable顾明思义就是国际化资源进行解析的对象。MessageSourceResolvable拥有三个接口方法:

[1] String[] getCodes();返回一组消息代码,每个代码对应一个资源属性。可以使用getArguments()方法返回的参数对资源属性值进行参数替换。

[2] Object[] getArguments();返回一组参数对象

[3] String getDefaultMessage();默认的错误消息。

当一个属性校验失败后,校验框架会为该属性生成4个消息代码,这些消息代码已校验注解类名为前缀,结合类名、属性名及属性类型名生成多个对应的消息代码。例如,当当password属性不满足@Pattern注解的规则时会产生:
    Pattern.user.password    :根据类项目、属性名产生的错误代码。
    Pattern.password        :根据属性名产生的错误代码
    Pattern.java.lang.String:根据属性类型产生的错误代码
    Pattern                    :根据验证注解名产生的错误代码

当使用SpringMVC标签显示错误信息时,SpringMVC会查看Web上下文是否装配了对应的国际化消息。如果没有,则显示默认的错误信息,否则使用国际化配置进行翻译。

需要注意的是,如果在数据类型转换或数据格式转换时发生错误,或者该有的参数不存在,或调用处理方法时发生错误,则都会在隐含模型中创建错误信息。其错误代码前缀说明如下:
[1] required:必要参数不存在,如@RequestParam("param")标注了一个参数但是请求时没有传递此参数
[2] typeMismatch:在数据绑定时,发生数据类型不匹配问题
[3] methodInvocation:SprngMVC在地用用处理方法时发生了错误

可以通过ResourceBundleMessageSource的basename属性指定一个国际化资源,还可以通过basenames属性指定多个国际化资源。

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"
 p:basename="资源路径及名称"/>

自定义校验规则

JSR-303允许定义自定义校验注解及对应的校验器类,Spring的校验框架完全支持它的自定义扩展注解。关于它自定义校验注解的内从可自行去之前给出的官网查看。

下面要介绍的是如何给请求处理类装配一个自定义的Validator,或者直接在处理方法中使用自定义的Validator对入参进行校验。可以在UserController中标注@InitBinder注解的initBinder()方法中装配自定义的MyValidator。

@InitBinder
public void initBinder(WebDataBinder binder){
	//在进行数据绑定时使用的校验器
	binder.setValidator(new UserValidator());
}

也可以借助请求处理方法的签名传递一个Errors或BindingResult对象进来,然后在处理方法中直接校验。

@RequestMapping(value="/addUser")
public String addUser(User user,BindingResult br){
	if("a".equalsIgnoreCase(user.getUserName)) bindingResult.rejectValue("userName","用户名不能为a");
	if(br.hasErrors()){
		return "错误地址";
	}else{
		return "正确地址";
	}
}

通过binder.setValidator()方法设置自定义的Validator后,SpringMVC将使用它对入参对象进行校验,而不再使用SpringMVC框架装配的Validator对入参对象进行校验。

发布了110 篇原创文章 · 获赞 475 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/yongqi_wang/article/details/87883764