[实习记录]SpringBoot中Web请求参数转换的正确方法(亲测有效)

达芬奇密码2018-08-13 10:37

开发Web应用时,Controller层收到的HTTP Request请求的参数往往是Text字符串形式,需要转换成JavaIntegerDateBoolean等类型。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提供了两种方法配置:一种是通过@InitBinderController实现自己的PropertyEditor,另外一种是通过ConversionService实现全局配置。

1      Controller层通过@InitBinder定制PropertyEditor

@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

2      通过ConversionService全局配置

首先实现自己的转换器类。

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;

}

}

}


但如果我需要把LongFloatDouble等类型均进行类似转换,岂不是要写许多重复代码?对此,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进行转换。

好了,重点来了——如何将我们自定义的ConverterConverterFactory配置到框架中?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

本文来自网易实践者社区,经作者葛志诚授权发布。