开发Web应用时,Controller层收到的HTTP Request请求的参数往往是Text字符串形式,需要转换成Java中Integer、Date、Boolean等类型。Spring已经帮我们把常用的数据类型转换做了实现,这些实现在org.springframework.core.convert.support包中。例如StringToBooleanConverter:
/**
* Converts String to a Boolean.
* @author Keith Donald
* @author Juergen Hoeller
* @since 3.0
*/
final class StringToBooleanConverter implements Converter<String, Boolean> {
private static final Set<String> trueValues = new HashSet<String>(4);
private static final Set<String> falseValues = new HashSet<String>(4);
static {
trueValues.add("true");
trueValues.add("on");
trueValues.add("yes");
trueValues.add("1");
falseValues.add("false");
falseValues.add("off");
falseValues.add("no");
falseValues.add("0");
}
@Override
public Boolean convert(String source) {
String value = source.trim();
if ("".equals(value)) {
return null;
}
value = value.toLowerCase();
if (trueValues.contains(value)) {
return Boolean.TRUE;
}
else if (falseValues.contains(value)) {
return Boolean.FALSE;
}
else {
throw new IllegalArgumentException("Invalid boolean value '" + source + "'");
}
}
}
我们经常需要定制类型转换,例如,收到字符串“null”时,我们希望将其转换为null而不是报出异常。Spring提供了两种方法配置:一种是通过@InitBinder在Controller实现自己的PropertyEditor,另外一种是通过ConversionService实现全局配置。
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
public void setAsText(String value) {
if (“null”.qualIgnoreCase(value)) {
setValue(null);
}
else {
setValue(new Date(Long.valueOf(value)));
}
}
});
}
如此便实现了这个Controller中目标类是Date的参数转换——将字符串“null”转换成null。但如果对基本类型都进行转换,需要在每一个Controller写这样的代码,十分繁琐。一种方法是写一个BaseController,在BaseController中处理。这样貌似也不是特别稳妥。所以,Spring提供了另外一种可以全局配置的方法。
测试环境:
spring-boot |
1.1.9 |
spring-mvc |
4.3.8 |
首先实现自己的转换器类。
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
例如StringToInteger:
public static class MyStringToIntegerConverter implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
try {
return (source == null || source.trim().equalsIgnoreCase("null") ? null : Integer.decode(source));
} catch (NumberFormatException e) {
log.error("String to Integer 转换异常. source string: " + source + " message:" + e.getMessage());
return null;
}
}
}
但如果我需要把Long、Float、Double等类型均进行类似转换,岂不是要写许多重复代码?对此,Spring提供了转换器工厂方法。
package org.springframework.core.convert.converter;
public interface ConverterFactory<S, R> {
<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
考虑这些类型都是Number的子类型,我们基于泛型继承写StringToNumberFactory:
/**
* String to Number (Byte/Integer/Double/Float/Short/Long)
*/
public class MyStringToNumberConverterFactory implements ConverterFactory<String, Number> {
@Override
public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToNumberConverter<>(targetType);
}
public static class StringToNumberConverter<T extends Number> implements Converter<String, T> {
private final Class<T> targetType;
public StringToNumberConverter(Class<T> targetType) {
this.targetType = targetType;
}
@Override
public T convert(String source) {
//"null" -> null
if (source.isEmpty() || source.trim().equalsIgnoreCase("null"))
return null;
try {
return NumberUtils.parseNumber(source, this.targetType);
} catch (NumberFormatException e) {
log.error("String to Number 转换异常. source string: " + source
+ " destination type " + this.targetType.getName() + " message:" + e.getMessage());
throw new IllegalArgumentException("Invalid Number value '" + source + "'");
}
}
}
}
工厂类中,转换的目标类型为<T extends Number>,Class<T> targetType为实际参数类型,如此实现了Number所有子类的统一转换逻辑。执行转换时,使用了org.springframework.util包中的NumberUtils进行转换。
好了,重点来了——如何将我们自定义的Converter和ConverterFactory配置到框架中?Spring官方文档提供了基于@Bean的配置方法,但这种方法有问题!
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="example.MyCustomConverter"/>
</set>
</property>
</bean>
Spring提供基于xml的配置方法,转化成基于@Bean的配置为:
@Configuration
public class ConverterConfig {
@Bean(name = "conversionService")
public ConversionServiceFactoryBean getConversionService() {
ConversionServiceFactoryBean bean = new ConversionServiceFactoryBean();
Set converters = new HashSet<>();
converters.add(new MyStringToNumberConverterFactory());
bean.setConverters(converters);
return bean;
}
}
这样配置没有效果!
有网友指出,应将此Bean注册到webBindingInitializer以及RequestMappingHandlerAdapter:
@Bean(name = "webBindingInitializer")
public ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() {
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(getConversionService().getObject());
return initializer;
}
@Bean
public RequestMappingHandlerAdapter RequestMappingHandlerAdapter() {
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer());
return adapter;
}
这样虽然起作用,但在执行一般的查询时,没有执行任何我们自己定制的代码,却在解析request时报出IllegalArgumentException异常。这也不是一个正确的配置方法。
Spring官方文档给出的另一种配置方法是:
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="org.example.MyConverter"/>
</set>
</property>
<property name="formatters">
<set>
<bean class="org.example.MyFormatter"/>
<bean class="org.example.MyAnnotationFormatterFactory"/>
</set>
</property>
<property name="formatterRegistrars">
<set>
<bean class="org.example.MyFormatterRegistrar"/>
</set>
</property>
</bean>
这种配置同样不起作用!
最后尝试了这种配置:
@Configuration
@EnableWebMvc
public class WebAppConfig extends WebMvcConfigurerAdapter {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(new MyStringToNumberConverterFactory());
super.addFormatters(registry);
}
}
It works!
Springboot使用ConversionService还有一些问题——按照官方文档XML配置,却没有注册到服务中。参考https://github.com/spring-projects/spring-boot/issues/6222。所以推荐使用Override addFormatters的方式进行配置。亲测有效!
网易云新用户大礼包:https://www.163yun.com/gift
本文来自网易实践者社区,经作者葛志诚授权发布。