页面传递的参数都是string,而在控制器中接收参数类型是不确定的,对于基础数据类型,springmvc已经提供了类型转换器,对于不支持的目标类型,例如日期类型,自定义的对象类型,则可以通过实现接口org.springframework.core.convert.converter.Converter
接口的方法来实现,该接口定义如下:
@FunctionalInterface
public interface Converter<S, T> {
@Nullable
T convert(S source);
}
其中的泛型
中的S代表的是页面参数的类型,一般是String,T代表控制器中接收参数的类型,方法convert(source)
就是我们要实现的从S转换为T的方法。接下来我们实现一个通过字符串向自定义对象转换的小栗子。
public class GoodsModel {
private String goodsname;
private double goodsprice;
private int goodsnumber;
...getter and setter...
}
我们假定传递参数为英文逗号分割的字符串,通过字符串分割后可以获取构建GoodModel对象需要的值,则源码如下:
public class MyGoodsModelConverter implements Converter<String, GoodsModel> {
public MyGoodsModelConverter() {
System.out.println("MyGoodsModelConverter no args constructor invoked!!!");
}
@Override
public GoodsModel convert(String source) {
String logHead = "MyGoodsModelConverter_convert";
System.out.println(logHead + " begin, source is: " + source);
// 创建一个Goods实例
GoodsModel goods = new GoodsModel();
// 以英文逗号,分隔
String stringvalues[] = source.split(",");
if (stringvalues != null && stringvalues.length == 3) {
// 为Goods实例赋值
goods.setGoodsname(stringvalues[0]);
goods.setGoodsprice(Double.parseDouble(stringvalues[1]));
goods.setGoodsnumber(Integer.parseInt(stringvalues[2]));
System.out.println(logHead + " end, goods is: " + goods);
return goods;
} else {
throw new IllegalArgumentException(String.format(
"类型转换失败, 需要格式'apple, 10.58,200 ',但格式是[% s ] ", source));
}
}
}
从String自动转换为dongshi.converter.GoodsModel的转换器需要注意这里的转换器是属于MVC范畴的,所以一定要配置在springMVC的配置文件,即{dispatcher-servlet-name}-spring.xml文件中,如
<servlet>
<servlet-name>letsGOservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
...
<load-on-startup>1load-on-startup>
servlet>
则需要配置的配置文件是letsGO-servlet.xml
,配置方式如下:
<bean id="myConversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="dongshi.converter.MyGoodsModelConverter"/>
set>
property>
bean>
<mvc:annotation-driven conversion-service="myConversionService">
...消息转换等配置,和自定义转换器配置没有关系...
mvc:annotation-driven>
@RequestMapping("/mytest")
@Controller
public class MyTestController {
@RequestMapping("/testConverter")
@ResponseBody
public GoodsModel testConverter(@RequestParam("goods") GoodsModel testConverter) {
System.out.println(testConverter);
return testConverter;
}
}
分别在转换器的return goods;
和controler的return testConverter;
打断点测试,通过curlcurl http://localhost:8080/Gradle___org_springframework___dongsir_testspringmvc_5_1_18_BUILD_SNAPSHOT_war__exploded_/mytest/testConverter?goods=3,4,5
,转换器如下图:
继续走代码到controller断点,如下图:
执行后返回:
{"goodsname":"3","goodsprice":4.0,"goodsnumber":5}
。
如果是在springboot环境中,则只需要定义转换器为spring的bean就可以了,springboot会自动从容器中扫描并通过DefaultFormattingConversionService
类完成注册,其中,自动注册部分源码如下:
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#addFormatters
@Override
public void addFormatters(FormatterRegistry registry) {
ApplicationConversionService.addBeans(registry, this.beanFactory);
}
org.springframework.boot.convert.ApplicationConversionService#addBeans
public static void addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory) {
Set<Object> beans =new LinkedHashSet();
// 注册自定义的GenericConverter
beans.addAll(beanFactory.getBeansOfType(GenericConverter.class).values());
// 注册自定义Converter(本例重点)
beans.addAll(beanFactory.getBeansOfType(Converter.class).values());
// 忽略
beans.addAll(beanFactory.getBeansOfType(Printer.class).values());
// 忽略
beans.addAll(beanFactory.getBeansOfType(Parser.class).values());
...
}
下面我们开始实例。
@RestController
public class HelloWorldController {
@RequestMapping("/testSpringBootConverter")
@ResponseBody
public GoodsModel testSpringBootConverter(@RequestParam("springboot-goods") GoodsModel testConverter) {
System.out.println(testConverter);
return testConverter;
}
}
注意添加@Component
注册到spring的容器中,成为spring的bean:
public class MySpringBootGoodsModelConverter implements Converter<String, GoodsModel> {
public MySpringBootGoodsModelConverter() {
System.out.println("MyGoodsModelConverter no args constructor invoked!!!");
}
@Override
public GoodsModel convert(String source) {
String logHead = "MyGoodsModelConverter_convert";
System.out.println(logHead + " begin, source is: " + source);
// 创建一个Goods实例
GoodsModel goods = new GoodsModel();
// 以英文逗号,分隔
String stringvalues[] = source.split(",");
if (stringvalues != null && stringvalues.length == 3) {
// 为Goods实例赋值
goods.setGoodsname(stringvalues[0]);
goods.setGoodsprice(Double.parseDouble(stringvalues[1]));
goods.setGoodsnumber(Integer.parseInt(stringvalues[2]));
System.out.println(logHead + " end, goods is: " + goods);
return goods;
} else {
throw new IllegalArgumentException(String.format(
"类型转换失败, 需要格式'apple, 10.58,200 ',但格式是[% s ] ", source));
}
}
}
这样就可以了,springboot就会自动完成注册了,启动应用时debug如下:
debug位置org.springframework.boot.convert.ApplicationConversionService#addBeans
。
前面的Converter是一对一转化器接口,即转换的源和目标都是单一的对象,GenericConverter可以认为是对Converter的增强,支持一对多,源码如下:
public interface GenericConverter {
@Nullable
Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
即控制器的参数可以是List类型的,在spring中已经提供了StringToCollectionConverter
来默认提供String向集合转换,源码如下:
org.springframework.core.convert.support.StringToCollectionConverter#convert
@Override
@Nullable
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
// 强转http中的字符串
String string = (String) source;
// 按照英文逗号分割为字符串数组(重点!!!)
String[] fields = StringUtils.commaDelimitedListToStringArray(string);
// 获取http参数的类型描述器,这里一般都是字符串
TypeDescriptor elementDesc = targetType.getElementTypeDescriptor();
Collection<Object> target = CollectionFactory.createCollection(targetType.getType(),
(elementDesc != null ? elementDesc.getType() : null), fields.length);
// 一般不会进if,所以只看else逻辑
if (elementDesc == null) {
for (String field : fields) {
target.add(field.trim());
}
}
else {
// 遍历每个字段值,调用convertionService,实际上就是GenericConversionService方法,该方法内部会通过
// sourceType+targetType(elementDesc)组合的方式来寻找可进行转换的转换器进行转换,转换后添加到最终的结果集中
for (String field : fields) {
Object targetElement = this.conversionService.convert(field.trim(), sourceType, elementDesc);
target.add(targetElement);
}
}
// 返回结果集
return target;
}
该转换器是先通过切割字符串的方式获取字符串数组,然后对每个元素,在通过http原始类型+控制器中的元素类型
匹配的方式获取对应的转换器再进行转换,因此,前面的例子,我们就可以定义一个List控制器参数,然后http参数通过逗号分割的方式,就可以进行多值传递了。
@RequestMapping("/testSpringBootConverterWithList")
@ResponseBody
public List<GoodsModel> testSpringBootConverter(@RequestParam("springboot-goods-list") List<GoodsModel> testConverterList) {
System.out.println(testConverterList);
return testConverterList;
}
因为我们前面的转换器也是通过英文逗号分割的,这里和StringToCollectionConverter冲突,所以我们改为使用_
分割:
将:
String stringvalues[] = source.split(",");
改为:
String stringvalues[] = source.split("_");