上篇:Spring MVC 数据绑定(一)
Spring MVC通过反射机制对目标处理方法的签名进行分析,将请求信息绑定到处理方法的入参中。在参数解析之后,紧跟着进行参数的类型转换、格式化、校验等。这里主要讲解参数解析之后的过程。
数据绑定的核心部件是DataBinder,其运行机制如图
流程大概分为几个步骤:
Spring MVC主框架将ServeltRequest对象及处理方法的入参对象实例传递给 DataBinder
DataBinder首先调用装配在Spring Web上下文中的ConversionService组件进行数据类型转换、数据格式化工作,将ServletRequest对象填充到入参对象中,
然后调用Validator组件对已经绑定了请求消息数据的入参对象进行数据合法性校验
最终生成数据绑定结果BindingResult对象。BindingResult对象包含了已完成数据绑定的入参对象,还包含相应的校验错误对象。
SpringMVC抽取BindingResult中的入参对象及校验错误对象,将他们赋给处理方法的相应入参。
传递参数到DataBinder
public class AnnotationMethodHandlerAdapter extends WebContentGenerator
implements HandlerAdapter, Ordered, BeanFactoryAware {
...
@Override
protected void doBind(WebDataBinder binder, NativeWebRequest webRequest) throws Exception {
ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
servletBinder.bind(webRequest.getNativeRequest(ServletRequest.class));
}
...
}
数据类型转换
Spring在核心模块中添加了一个通用的类型转换模块org.springframework.core.convert
,希望通过整个模块体系替换Java标准的PropertyEditor
。由于历史原因,Spring同时支持两者,在Bean配置、SpringMVC处理方法入参的过程中都会使用。
ConversionService
ConversionService 是 Spring类型转换体系的核心接口,在此接口中,定义了4个方法
public interface ConversionService {
//判断是否可以将一个Java类转换为另外一个Java类,类似于PropertyEditor中的方法
boolean canConvert(Class> sourceType, Class> targetType);
/*
需转换的类将以成员变量的方式出现在宿主类中。
TypeDescriptor描述了需转换类的信息,还描述了宿主类的上下文信息,如成员变量上的注解,
成员变量是否以数组、集合或Map的方式呈现。
类型转换逻辑可以利用这些信息做出灵活的控制,这是PropertyEditor所做不到的。
*/
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
//将原类型对象转换为目标类型对象,类似于PropertyEditor中的方法
T convert(Object source, Class targetType);
//将原类型对象转换为目标类型对象,此方法会用到宿主类上下文信息
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
在使用时,可以使用ConversionServiceFactoryBean
在Spring上下文中定义一个ConversionService
,Spring会自动识别上下文中的ConversionService
,并在Bean属性配置及SpringMVC处理方法入参绑定时使用它进行数据转换。
该FactoryBean 创建的 conversionService
内置了很多转换器,可以完成大多数Java类型的转换工作,包括String、Number、Array、Collection、Map、Properties及Object。
也可以注册自定义的类型转换器,自定义的转换器需要实现特定的接口。具体见下面转换器。
MyConverter
转换器
Spring 在org.springframework.core.convert.converter
中定义了多个Converter接口
Converter
public interface Converter {
@Nullable
T convert(S var1);
}
它是Spring中最简单的一个转换器接口,负责将S类型的对象转换为T类型的对象。如果需要将String转换为Number及Number的子类Integer、Long、Double等对象,就需要一系列的Converter,如StringToInteger、StringToLong、StringToDouble。
基于以上原因,Spring提供了一个将相同系列多个同质Convert封装在一起的ConvertFactory接口
ConverterFactory
S为转换的源类型,R为目标类型的基类,而T为扩展于R基类的类型。
public interface ConverterFactory {
Converter getConverter(Class var1);
}
如StringToNumberConverterFactory就实现了ConverterFactory接口,封装了String转换到各个数据类型的Converter。
final class StringToNumberConverterFactory implements ConverterFactory {
StringToNumberConverterFactory() {
}
public Converter getConverter(Class targetType) {
return new StringToNumberConverterFactory.StringToNumber(targetType);
}
private static final class StringToNumber implements Converter {
private final Class targetType;
public StringToNumber(Class targetType) {
this.targetType = targetType;
}
public T convert(String source) {
return source.isEmpty() ? null : NumberUtils.parseNumber(source, this.targetType);
}
}
}
Converter只负责将对象与对象之间的转换,并没有考虑类型对象所在宿主类上下文的信息。GenericConverter接口会根据源类对象及目标类对象所在宿主类的上下文信息进行类型转换工作。
GenericConverter
GenericConverter.ConvertiblePair封装了源类型和目标类型,TypeDescriptor包含了需转换类型对象所在宿主类的信息,因此GenericConverter 的 convert接口方法可以利用上下文信息进行类型转换工作。
public interface GenericConverter {
@Nullable
Set getConvertibleTypes();
@Nullable
Object convert(@Nullable Object var1, TypeDescriptor var2, TypeDescriptor var3);
public static final class ConvertiblePair {
private final Class> sourceType;
private final Class> targetType;
public ConvertiblePair(Class> sourceType, Class> targetType) {
Assert.notNull(sourceType, "Source type must not be null");
Assert.notNull(targetType, "Target type must not be null");
this.sourceType = sourceType;
this.targetType = targetType;
}
...
}
}
ConditionalConverter
ConditionalGenericConverter继承了GenericConverter 和 ConditionalConverter,自身并没有接口方法
public interface ConditionalGenericConverter
extends GenericConverter, ConditionalConverter {
}
在ConditionalConverter 中定义了一个接口方法,该方法只有只有返回true后,才能调用convert()方法进行类型转换。
public interface ConditionalConverter {
boolean matches(TypeDescriptor var1, TypeDescriptor var2);
}
ConversionServiceFactoryBean 的converts属性可接受Converter、ConverterFactory、GenericConverter、
ConditionalConverter接口的实现类,并把这些转换器的转换逻辑统一封装到一个ConversionService实例对象中。Spring在属性配置及Spring MVC请求消息绑定时将使用这个ConversionService完成数据转换。
使用ConversionService
我们可以自定义一个Converter
@Component
public class StringtoUserConvert implements Converter {
@Override
public User convert(String s) {
User user = new User();
user.setName(s);
user.setAge(0);
return user;
}
}
配置到ConversionService
到上下文。RequestMappingHandlerMapping
和 RequestMappingHandlerAdapter
实例,还会注册一个默认的ConversionService
(FormattingConversionServiceFactoryBean)以满足大部分类型转换的需求。
我们这里显示的定义一个conversionService代替默认实现,实际开发中不建议这么做。
spring 3.1 开始我们应该用
RequestMappingHandlerMapping
来替换DefaultAnnotationHandlerMapping
,用RequestMappingHandlerAdapter
来替换AnnotationMethodHandlerAdapter
,提供更多的扩展点。
数据格式化
Spring使用转换器完成对象目标类型的转换,转换过程不包含输入、输出信息的格式化。如日期,数字,时间,货币等数据都是具有一定格式的,在不同的本地化环境中,同一类型的数据显示不同的数据格式。
从格式化数据中获取真正数据并完成绑定,并将处理完成的数据输出为格式化的数据,Spring提供了格式化框架org.springframework.format
。其中最重要的接口是Formatter
Formatter
Printer
public interface Formatter extends Printer, Parser {}
在两个接口中,各有一个接口方法
//将类型为T的成员根据本地化的不同输出为不同格式的字符串
@FunctionalInterface
public interface Printer {
String print(T var1, Locale var2);
}
//参考本地化信息将一个格式化的字符串转换为T类型的对象
@FunctionalInterface
public interface Parser {
T parse(String var1, Locale var2) throws ParseException;
}
格式化注解驱动
spring提供了注解驱动接口AnnotationFormatterFactory
public interface AnnotationFormatterFactory {
//注解A的应用范围,哪些属性类可以标注A注解
Set> getFieldTypes();
//获取属性A特定Printer
Printer> getPrinter(A var1, Class> var2);
//获取属性A特定Parser
Parser> getParser(A var1, Class> var2);
}
spring提供了多个内建的实现类,通过名字很容易理解每个实现类的作用
启用格式化注解驱动
spring是基于对象转换框架植入格式化功能的,Spring在格式化模块定义了一个实现ConversionService
接口的FormattingConversionService
,它既继承了GenericConversionService
,又实现了FormatterRegistry
(实现FormatterRegistry
是实现Formatter的一种方式)。
public class FormattingConversionService extends GenericConversionService
implements FormatterRegistry, EmbeddedValueResolverAware {}
和ConversionServiceFactoryBean
一样,FormattingConversionService
也有自己的工厂类FormattingConversionServiceFactoryBean
,在上下文中构造这个工厂类,可以注册自定义转换器,还可以注册自定义注解驱动逻辑。
FormattingConversionService
在内部会自动注册(这块源码没找到,应该读取配置文件是通过set方法注入)NumberFormatAnnotationFormatterFactory
,JodaDateTimeFormatAnnotationFormatterFactory
,因此,在装配了FormattingConversionService
后,就可以在Spring MVC入参绑定及模型输出时使用注解驱动进行格式化。
public class User {
private String name;
private String age;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthday;
@NumberFormat(pattern = "#,###.##")
private long salary;
...
}
数据校验
JRS-303
JRS-303是Java为Bean数据合法性校验提供的标准框架,包含在Java EE6.0中,通过在Bean属性上标注类似@NotNull、@Max等注解指定校验规则,并通过标准的验证接口对Bean进行验证。
参考:JSR 303 - Bean Validation 介绍及最佳实践
Constraint | 详细信息 |
---|---|
@Null |
被注释的元素必须为 null |
@NotNull |
被注释的元素必须不为 null |
@AssertTrue |
被注释的元素必须为 true |
@AssertFalse |
被注释的元素必须为 false |
@Min(value) |
被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) |
被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) |
被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) |
被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) |
被注释的元素的大小必须在指定的范围内 |
@Digits (integer, fraction) |
被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past |
被注释的元素必须是一个过去的日期 |
@Future |
被注释的元素必须是一个将来的日期 |
@Pattern(value) |
被注释的元素必须符合指定的正则表达式 |
Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
Constraint | 详细信息 |
---|---|
@Email |
被注释的元素必须是电子邮箱地址 |
@Length |
被注释的字符串的大小必须在指定的范围内 |
@NotEmpty |
被注释的字符串的必须非空 |
@Range |
被注释的元素必须在合适的范围内 |
javax.validation
validation-api
1.1.0.Final
org.hibernate
hibernate-validator
5.4.1.Final
Spring数据校验
Spring拥有自己独立的数据验证框架,同时支持JRS-303标准框架。Spring的DataBinder在进行数据绑定时,可同时调用校验框架完成数据校验,在Spring MVC中,可直接通过注解驱动的方式进行数据校验。
Validator
最基本的Spring校验接口
package org.springframework.validation;
public interface Validator {
//对clazz类型的对象进行校验
boolean supports(Class> clazz);
//对目标类target进行校验,并将校验错误记录在errors中
void validate(Object target, Errors errors);
}
LocalValidatorFactoryBean
LocalValidatorFactoryBean既实现了spring的Validator接口,又实现了JRS-303的Validator接口。
public class LocalValidatorFactoryBean extends SpringValidatorAdapter implements ValidatorFactory, ApplicationContextAware, InitializingBean, DisposableBean {}
public class SpringValidatorAdapter
implements SmartValidator, javax.validation.Validator {}
在Spring中定义一个bean,即可将其加入需要校验的bean中。另外一点,Spring并没有提供JRS-303的实现,必须引入相关实现者的jar包。
Spring MVC数据校验
public class User {
@Pattern(regexp = "w{4,30}")
private String name;
@Min(18)
private Integer age;
...
}
@RequestMapping("/user")
/*
Spring MVC通过处理方法签名的规约来保存校验结果,前一个校验结果保存在其后的入参中,必须要是
BindingResult或Errors类型,之间不允许其他入参
*/
public String UserController(@Valid User user, BindingResult bindingResult){
if(bindingResult.hasErrors()){
System.out.println(bindingResult.getFieldErrors("age"));
}else{
System.out.println("no errors");
}
return null;
}
控制台信息:
[Field error in object 'user' on field 'age': rejected value [1]; codes [Min.user.age,Min.age,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age],18]; default message [最小不能小于18]]
自定义校验规则
可以使用注解@InitBinder设置自定义的Validator
@RestController
public class UserController {
//这种方式会放弃Spring框架装载的validator
@InitBinder
public void initBinder(WebDataBinder binder){
binder.setValidator(new UserValidator());
}
...
}
也可以在方法中直接校验或者手动添加错误信息
@RequestMapping("/user2")
public String getUser2(User user, BindingResult bindingResult){
//校验是否为空或有空格
ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult,"name","required");
if("aaa".equalsIgnoreCase(user.getName())){
bindingResult.rejectValue("name","reserved");
}
return null;
}
/*
产生的对应错误信息:
required.user.name
required.name
required.java.lang.String
required
reserved.user.name
reserved.name
reserved.java.lang.String
reserved