likes
comments
collection
share

宝,你了解 Spring 中的类型转换体系吗

作者站长头像
站长
· 阅读数 31

也许大家对 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 开始,类型转换领域迎来了三位新的王者,分别是:ConverterConverterFactoryGenericConverter,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 中类型转换体系的应用场景与委派模型。

宝,你了解 Spring 中的类型转换体系吗

BeanWrapperDataBinder都从属于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 类型转换系统,干嘛还使用它呦。