宝,你了解 Spring 中的类型转换体系吗
也许大家对 Spring 所提供的类型转换能力没有明显的感知,但它一直在默默地为我们服务着,正所谓“润物细无声”。笔者这里梳理了一些常用的类型转换场景,如下所示:
-
当通过
@RequestParam boolean status
来接收 HTTP 请求参数中的status = on
时,字符串类型的 on 可以自动转换为布尔类型的 true; -
当通过
@RequestHeader User user
来接收 HTTP 请求头中的userId
时,字符串类型的 userId 可以自动转换为 User 类型; -
当通过
@PathVariable LocalDate begin
来接收 HTTP 请求路径中的2023-03-31
时,字符串类型的 2023-03-31 可以自动转换为 LocalDate 类型; -
当通过
@ConfigurationProperties 注解标注的 Bean
来接收外部配置源中的90s
时,字符串类型的 90s 可以自动转换为 Duration 类型; -
当通过
@ConfigurationProperties 注解标注的 Bean
来接收外部配置源中的2023-03-31T12:13:14
(必须是 ISO_8601 格式,否则类型转换将报错) 时,字符串类型的 2023-03-31T12:13:14 可以自动转换为 LocalDateTime 类型。
在 Spring 3.0 之前,类型转换主要由PropertyEditor
负责,Spring 内置了 40+ 个实现类,如果想自定义一个 PropertyEditor,只需要直接继承PropertyEditorSupport
同时覆盖 setAsText() 与 getAsText() 这俩方法即可;PropertyEditor 存在两个明显的缺陷,一是只能实现字符串类型与其他类型之间的转换,二是类型转换后的值由成员变量value
来承载,这也就导致 PropertyEditor 是线程不安全的。
从 Sprnig 3.0 开始,类型转换领域迎来了三位新的王者,分别是:Converter
、ConverterFactory
和GenericConverter
,Converter 用于实现一对一 (1:1) 类型转换,ConverterFactory 用于实现一对多 (1:N) 类型转换,GenericConverter 用于实现多对多类 (N:N) 型转换;同时,Spring 针对这些 Converter 提供了统一的访问入口,即ConversionService
,如果大家需要借助 Converter 来实现线程安全的类型转换,那直接将 ConversionService 注入到目标 Bean 中即可。
为了保持向后兼容,PropertyEditor 依然沿用至今,也就是说 Spring 中的类型转换体系主要由 PropertyEditor 和 Converter 三兄弟构成。为了屏蔽这两种类型转换系统的内部差异,从而给开发人员提供统一的编程模型,Spring 为大家带来了TypeConverterDelegate
,主要内容如下所示。
public class TypeConverterDelegate {
private final PropertyEditorRegistrySupport propertyEditorRegistry;
private final Object targetObject;
public TypeConverterDelegate(PropertyEditorRegistrySupport propertyEditorRegistry,
Object targetObject) {
this.propertyEditorRegistry = propertyEditorRegistry;
this.targetObject = targetObject;
}
public <T> T convertIfNecessary(String propertyName, Object oldValue,
Object newValue, Class<T> requiredType) throws IllegalArgumentException {
return convertIfNecessary(propertyName, oldValue, newValue, requiredType, TypeDescriptor.valueOf(requiredType));
}
public <T> T convertIfNecessary(String propertyName, Object oldValue, Object newValue,
Class<T> requiredType, TypeDescriptor typeDescriptor) throws IllegalArgumentException {
// Custom editor for this type?
PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);
ConversionFailedException conversionAttemptEx = null;
// No custom editor but custom ConversionService specified?
ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
try {
return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
} catch (ConversionFailedException ex) {
// fallback to default conversion logic below
conversionAttemptEx = ex;
}
}
}
Object convertedValue = newValue;
// Value not of required type?
// 这里忽略长串的逻辑,感兴趣的读者自行阅读
return (T) convertedValue;
}
}
从上述内容来看,TypeConverterDelegate 首先会获取合适的 PropertyEditor,然后委派其进行类型转换;如果未找到适合的 PropertyEditor,则终将委派 ConversionService 来完成类型转换工作。Spring 在 TypeConverterDelegate 与 PropertyEditor、ConversionService 之间加了一层,也就是 PropertyEditorRegistrySupport,这样灵活性会好一点。
下面用一幅图来描绘:Spring 中类型转换体系的应用场景与委派模型。
BeanWrapper
与DataBinder
都从属于TypeConverter
,它们针对 Java Bean 都提供了属性访问与属性设定的方法;BeanWrapper 与 DataBinder 并不是一个 Spring Bean,需要的时候直接实例化即可,在实例化过程中会注册一些默认的 PropertyEditor。在 Spring 内部,BeanWrapper 主要活跃于 Bean 加载流程中的属性填充阶段,而 DataBinder 尤其是WebDataBinder
则主要活跃于 Spring MVC 框架中的数据绑定阶段。具体地,对于 HTTP 请求参数,在数据绑定过程中,RequestParamMethodArgumentResolver
会委派 WebDataBinder 进行类型转换;对于 HTTP 请求头中的参数,RequestHeaderMethodArgumentResolver
依然会委派 WebDataBinder 完成类型转换;而对于 HTTP 请求路径中的参数,PathVariableMethodArgumentResolver
还是委派 WebDataBinder 来完成类型转换。但对于 HTTP 请求体中的参数,RequestResponseBodyMethodProcessor
则是通过HttpMessageConverter
来实现数据绑定的,这里面的确涉及转换,但不应该划分到类型转换的范畴,这应该是Jackson
组件的事情了。
在 Spring 官方文档 Spring Type Conversion中介绍 Converter 接口是一个 SPI 接口,这就在暗示我们:如果试图自定义 Converter,那么直接将其声明为一个 Bean 即可,这样 Spring 就会自动将其载入。咱们来看一看这里的载入逻辑。
@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public class WebMvcAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WebProperties.class)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
@Bean
@Override
public FormattingConversionService mvcConversionService() {
Format format = this.mvcProperties.getFormat();
WebConversionService conversionService = new WebConversionService(new DateTimeFormatters()
.dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
addFormatters(conversionService);
return conversionService;
}
}
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
@Override
public void addFormatters(FormatterRegistry registry) {
ApplicationConversionService.addBeans(registry, this.beanFactory);
}
}
}
紧接着跟进到ApplicationConversionService
中一探究竟。
public class ApplicationConversionService extends FormattingConversionService {
public static void addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory) {
Set<Object> beans = new LinkedHashSet<>();
beans.addAll(beanFactory.getBeansOfType(GenericConverter.class).values());
beans.addAll(beanFactory.getBeansOfType(Converter.class).values());
beans.addAll(beanFactory.getBeansOfType(Printer.class).values());
beans.addAll(beanFactory.getBeansOfType(Parser.class).values());
for (Object bean : beans) {
if (bean instanceof GenericConverter) {
registry.addConverter((GenericConverter) bean);
} else if (bean instanceof Converter) {
registry.addConverter((Converter<?, ?>) bean);
} else if (bean instanceof Formatter) {
registry.addFormatter((Formatter<?>) bean);
} else if (bean instanceof Printer) {
registry.addPrinter((Printer<?>) bean);
} else if (bean instanceof Parser) {
registry.addParser((Parser<?>) bean);
}
}
}
}
至于如何注册自定义的 PropertyEditor,本文就不介绍了,有了全新的 Converter 类型转换系统,干嘛还使用它呦。
转载自:https://juejin.cn/post/7216635223533125691