Spring:深入分析SpringMVC之请求数据绑定与转换

在上一篇中我们分析了spring会根据请求方法签名的不同,将请求消息中的信息以一定的方式转换并绑定到请求方法的入参中。当请求消息到达真正需要调用的方法时,SpringMVC还有很多工作要做,包括数据转换、数据格式化及数据校验等。

1 数据绑定流程剖析

SpringMVC通过反射机制对目标处理方法的签名进行分析,将请求消息绑定到处理方法的入参中。数据绑定的核心部件是DataBinder,其运行机制描述如下:

SpringMVC主框架将ServletRequest对象及处理方法的入参对象实例传递给DataBinder首先调用装配在SpringWeb上下文中的ConversionService组件进行数据类型转换、数据格式化等工作,将ServletRequest中的消息填充到入参对象中,然后调用Validator组件对已经绑定了请求消息数据的入参对象进行数据合法性校验,最终生成数据绑定结果BindingResult对象。BindingResult包含了已完成数据绑定的入参对象,还包含响应的校验错误对象。SpringMVC抽取BindingResult中的入参对象及校验错误对象,将它们赋给处理方法的相应入参。

2 数据转换

Java标准的PropertyEditor的核心功能是将一个字符串转换为一个Java对象,以便根据界面的输入或配置文件中的配置字符串构造出一个JVM内部的Java对象。但是Java原生的PropertyEditor存在以下不足:

[1] 只能用于字符串到Java对象的转换,不使用与任意两个Java类型之间的转换。

[2] 对源对象及目标对象所在的上下文信息,如注解、所在宿主类的结构等不敏感,在类型转换时不能利用这些上下文信息实施高级的转换逻辑。

鉴于此,Spring在核心模型中添加了一个通用的类型转换模块,类型转换模块位于org.springframework.core.convert包中。Spring希望用这个类型转换体系替换Java标准的PropertyEditor。但由于历史原因,Spring将同时支持两者,在Bean配置、SpringMVC处理方法入参绑定中使用它们。

ConversionService

ConversionService是Spring类型转换体系的核心接口,它位于org.springframework.core.convert包中,也是该包中的唯一一个接口。ConversionService接口定义了一下四个方法:

[1] boolean canConvert(Class<?> var1, Class<?> var2);

判断是否可以将一个Java类转换为另一个Java类

[2] boolean canConvert(TypeDescriptor var1, TypeDescriptor var2);

需转换的类将以成员变量的方式出现在宿主类中。TypeDescriptor不但描述了需转换类的信息,还描述了从宿主类的上下文信息,如成员变量上的注解,成员变量是否以数组、集合或Map的方式呈现等。类型转换逻辑可以利用这些信息作出各种灵活控制。

[3] <T> T convert(Object var1, Class<T> var2);

将原类型对象转换为目标类型对象。

[4] Object convert(Object var1, TypeDescriptor var2, TypeDescriptor var3);

将对象从原类型对象转换为目标类型对象,此时往往会用到所在宿主类的上下文信息。

第一个和第三个接口方法类似于PropertyEditor,他们不关注类型对象所在的上下文信息,只简单的完成两个类型对象的转换,唯一的区别在于这两个方法支持任意两个类型的转换。而第二个和第四个接口方法会参考类型对象所在宿主类的上下文信息,并利用这些信息进行类型转换。

可以利用org.springframework.context.support.ConversionServiceFactoryBean在Spring的上下文中定义一个ConversionService。Spring将自动识别出上下文中的ConversionService,并在Bean属性配置及SpringMVC处理方法入参绑定等场合使用它进行数据转换。

<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"/>

该GactoryBean创建ConversionService内建了很多转换器,可完成大多数Java类型的转换工作。除了包括将String对象转换为各种基础类型的对象外,还包括String、Number、Array、Collection、Map、Properties及Object之间的转换器。

可通过ConversionServiceFactoryBean的converters属性注册自定义的类型转换器:

<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
	<property name="converters">
		<list>
			<bean class="com.smart.MyCustomConverter1"/>
			<bean class="com.smart.MyCustomConverter2"/>
		</list>
	</property>
</bean>

Spring支持哪些转换器

Spring在org.springframework.core.convert.converter包中定义了3种类型的转换器接口,实现任意一个转换器接口都可以作为自定义转换器注册到ConversionServiceFactorBean中。这3中类型的转换器接口分别为:

[1] Converter<S,T>

它是Spring中最简单的一个转换器接口,仅包含一个方法

public interface Converter<S, T> {

	T convert(S source);
}

此方法负责将S类型的对象转换为T类型的对象。如果希望将一种类型的对象转换为另一种类型及其子类的对象,距离来说,将String转换为Number及Number子类对象,就需要一系列的Converter,如StringToInteger、StringToLong及StringToDouble等。

[2] GenericConverter

Converter只负责将一个类型的对象转换为另一个类型的对象,并没有考虑类型对象所在宿主类上下文的信息,因此并不能完成复杂类型转换工作。GenericConverter接口会根据源类对象及目标类对象所在宿主类的上下文信息进行类型转换工作

public interface GenericConverter {

	Set<ConvertiblePair> getConvertibleTypes();

	
	Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

}

ConvertiblePair封装了源类型和目标类型,组成一个对子,而TypeDecriptor包含了需要转换类型对象所在宿主类的信息,因此GenericConverter的convert()方法可以利用这些上下文信息进行类型转换工作。

ConditionalGenericConverter扩展于GenericConverter接口,并添加了一个接口方法:

boolean matches(TypeDescriptor sourceType,TypeDescriptor targetType);

该接口方法根据源类型及目标类型所在宿主类的上下文信息决定是否要进行类型转换,只有该接口方法返回true时,才调用convert()方法完成类型转换。正是因为在转换之前有一个是否要进行类型转换的条件判断动作所以才如此命名该接口。

ConversionServiceFactoryBean的converters属性可接受Converter、ConverterFactory、GenericConverter或ConditionalGenericConverter接口的实现类,并把这些转换器的转换逻辑统一封装到一个ConversionService实例对象中。Spring在Bean属性配置及SpringMVC请求消息绑定时将利用这个ConversionService实例完成类型转换工作。

[3] ConverterFactory

Spring提供了一个将同系列多个同质Converter封装在一起的ConverterFactory接口:

public interface ConverterFactory<S, R> {

	<T extends R> Converter<S, T> getConverter(Class<T> targetType);

}

S为转换的源类型,R为目标类型的基类,而T未扩展于R基类的类型。如Spring的StringToNumberConverterFactory就实现了ConverterFactory接口,封装了String转换到各种数据类型的Converter。

在SpringMVC中使用ConversionService

假设处理方法有一个User类型的入参,我们希望将一个格式化的请求参数字符串直接转换为User对象,该字符串的格式为:userName:password;这就要求我们定义一个负责将格式化的String转换为User对象的自定义转换器:

public class StringToUserConverter implements Converter<String,User>{
	public User convert(String source){
		User user = new User();
		if(source != null){
			String[] items = source.split(":");
			user.setUserName(items[0]);
			user.setPassword(items[1]);
		}
		return user;
	}
}

编写好StingToUserConverter后,接下来将其安装到Spring上下文中:

<?xml version="1.0" encoding="UTF-8"?>
<beans ... />
	<mvc:annotation-driven conversion-service="conversionService"/>
	<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
		<property name="converters">
			<list>
				<bean class="com.xxx.StringToUserConverter"/>
			</list>
		</property>
	</bean>
</beans>

使用<mvc:annotation-driven/>标签,可以简化SpringMVC的相关配置。在默认情况下,该标签会创建并注册一个默认的DefaultAnnotationHandlerMapping和一个RequestMappingHandlerAdapter实例。如果上下文中存在自定义的对应组件Bean,则SpringMVC会自动利用自定义的Bean覆盖默认的Bean。除此之外,该标签还会注册一个默认的ConversionService即FormattingConversionServiceFactoryBean,以满足大多数类型转换的需求。现在由于要注册一个自定义的StringToUserConverter,因此,需要显示定义一个ConversionService覆盖该标签中的默认实现,这是通过该标签的conversion-service属性完成的。

在装配好StringToUserConverter后,就可以在任何控制器的处理方法中使用这个转换器了:

// 请求:http://localhost:8080/login?user=yongqi_wang:123456
@RequestMapping(path="login")
public String login(@RequestParam("user")User user){
	System.out.println(user.getUserName()+":"+user.getPassWord());
	return "success";
}

使用@InitBinder和WebBindingInitializer装配自定义编译器

SpringMVC在支持新的转换器框架的同时,也支持JavaBeans的PropertyEitor。可以在控制器中使用@InitBinder添加自定义的编辑器,也可以通过WebBindingInitializer装配在全局范围内使用的编辑器。

@Controller
@RequestMapping("/user")
public class UserController{
	@IntiBinder//在控制器初始化时调用
	public void initBinder(WebDataBinder binder){
		//注册一个自定义的编译器
		binder.registerCustomEditor(User.class,new UserEditor());
		// 可以指定格式化程序实现,这样就不需要实现一个PropertyEditor的实例了
		binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
	}
}

SpringMVC使用WebDataBinder处理请求消息和处理方法入参的绑定工作。在上面代码中注册的自定义编译器UserEditor是实现PropertyEditor接口的编辑器。如果希望在全局范围内使用UserEditor编辑器,则可实现WebBindingInitializer接口并在该实现类中注册UserEditor:

public class MyBindingInitializer implements WebBindingInitializer{
	public void initBinder(WebDataBinder binder,WebRequest request){
		binder.registerCustomEditor(User.class,new UserEditor());
	}
}

在initBinder接口方法中注册了UserEditor编辑器后,在Spring上下文中通过RequestMappingHandlerAdapter装配MyBindingInitializer:

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
	<property name="webBindingInitializer">
		<bean class="com.xxx.MyBindingInitializer"/>
	</property>
</bean>

对于同一个类型对象来说,如果即在ConversionService中装配了自定义转换器,又通过WebBindingInitializer装配了自定义编辑器,同时还在控制器中通过@InitBinder装配了自定义编辑器,那么SpringMVC将按一下优先顺序查找对应类型的编辑器:

[1] 查询通过@InitBinder装配的自定义编辑器

[2] 查询通过ConversionService装配的自定义转换器。

[3] 查询通过WebBindingInitializer装配的自定义编辑器。

 

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

猜你喜欢

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