← 返回题目列表

Spring 是怎么把字符串配置/请求参数转成各种类型的?Converter 与 ConversionService 是什么?

中等 第 30 / 30 题 更新于 2026/07/28
类型转换ConverterConversionServiceFormatter

简化版

Spring 的类型转换体系解决的是「怎么把一种类型的数据转成另一种类型」——最常见的场景是:@Value("${port}") 拿到的是字符串 “8080”,但要注入到 int port 字段;HTTP 请求参数 ?age=18 是字符串,但 Controller 方法参数是 Integer age。Spring 用一套统一的转换体系搞定:Converter<S, T>——最基础的转换器接口,实现 convert(S source) 把类型 S 转成类型 T(如 Converter<String, Integer>);ConversionService——转换服务的「总入口」,内部注册了一大堆 Converter,convert(source, targetType) 时它自动找到合适的 Converter 来转;Formatter<T>——专门处理「字符串 ↔ 对象」且**带本地化(Locale)**的转换(如日期按不同地区格式解析/显示),用在 Web 层。扩展方式:实现自己的 Converter 并注册(如把字符串转成自定义枚举、把 “2026-01-01” 转成 LocalDate),Spring 就能自动在注入、参数绑定等场景用上它。核心:一套统一的、可扩展的类型转换 SPI,替代了各处零散的手动转换。

详细版

三个核心接口

接口作用典型场景
Converter<S,T>把 S 转成 T(单向、无 Locale)String→Integer、String→枚举
ConverterFactory<S,R>一族转换(S 转成 R 的任意子类)String→所有 Enum
GenericConverter最灵活(可访问字段注解、多对多)集合/数组转换、带注解的转换
Formatter<T>字符串↔对象 + Locale 本地化日期、货币、数字格式化
ConversionService转换总入口,聚合所有转换器统一 convert 调用

转换的调用流程

需要把 "8080"(String)转成 int:

conversionService.convert("8080", Integer.class)

ConversionService 在注册的转换器里找:
  谁能把 String 转成 Integer?→ StringToNumberConverter

调用它的 convert("8080") → 8080
// 自定义 Converter:把字符串转成自定义枚举
public class StringToStatusConverter implements Converter<String, Status> {
    @Override
    public Status convert(String source) {
        return Status.fromCode(source);   // 如 "1" → Status.ACTIVE
    }
}

// 注册(Spring Boot 里)
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToStatusConverter());
    }
}
// 之后 @Value、@RequestParam Status status 都能自动用它转换

⚠️ 类型转换是 Spring 里「无处不在但常被忽视」的基础设施——你写 @Value("${timeout}") 注入到 Duration@RequestParam LocalDate date@ConfigurationProperties 把配置绑定到各种类型的字段,背后全靠这套转换体系默默工作。它替代了「到处 Integer.parseIntSimpleDateFormat.parse」的零散手动转换,把转换逻辑收敛成可复用、可扩展的 Converter。当你遇到「注入/绑定自定义类型失败」时,往往就是「缺一个对应的 Converter」——写一个注册上即可,而不是在每个用到的地方手动转。

完整版教学

一、为什么需要类型转换体系

先理解「Spring 处处需要类型转换」这个背景:

Spring 里数据的"入口"常常是字符串,但目标是各种类型:
  ① 配置注入:@Value("${port}") → "8080"(String) → int port
  ② 请求参数:?age=18&date=2026-01-01 → Integer age, LocalDate date
  ③ 配置绑定:@ConfigurationProperties 把 yml 的字符串绑到各种字段
  ④ 表单提交:表单字段都是字符串,要绑到对象的各种类型属性

如果没有统一体系,会怎样?
  → 每个地方都手写 Integer.parseInt、SimpleDateFormat.parse...
  → 转换逻辑散落各处、重复、易错、难统一(如日期格式不一致)

解决:一套统一的类型转换体系
  → 转换逻辑收敛成 Converter,一处定义、处处复用

类型转换的背景是「Spring 的数据入口多是字符串,但目标类型五花八门」——配置注入、请求参数、配置绑定、表单提交都要「字符串 → 目标类型」。没有统一体系就得处处手写 parseInt/parse,散落重复易错。Spring 用统一的转换体系把转换逻辑收敛成可复用的 Converter。理解「Spring 处处要字符串转各种类型、统一转换体系替代零散手动转换」,就理解了这套体系存在的必要性。

二、Converter:最基础的转换器

Converter<S, T> 是转换体系的「基本单元」:

public interface Converter<S, T> {
    T convert(S source);   // 把 S 类型转成 T 类型
}

例子:
  StringToIntegerConverter implements Converter<String, Integer>
    convert("123") → 123
  StringToLocalDateConverter implements Converter<String, LocalDate>
    convert("2026-01-01") → LocalDate.of(2026,1,1)

特点:
  - 单向(S→T),无状态,可复用
  - 一个 Converter 只管一种转换(String→Integer)
  - Spring 内置了几十个(String↔各种基本类型/包装类/集合...)

Converter<S, T> 是「一种类型到另一种类型的单向转换」——实现一个 convert(S) 方法。它无状态、可复用,一个 Converter 只负责一种转换。Spring 内置了几十个常用 Converter(String↔基本类型/包装类等)。它是整个转换体系的基本单元。理解「Converter<S,T> 是基础转换器、单向 convert(S)→T、无状态可复用、一个只管一种转换」,就理解了转换的基本单元。

三、ConversionService:转换的总入口

单个 Converter 只管一种转换,谁来「统一调度所有 Converter」?答案是 ConversionService

ConversionService = 转换服务的总入口,聚合所有 Converter
  核心方法:
    boolean canConvert(sourceType, targetType)  能不能转
    T convert(source, targetType)               转!

工作方式:
  内部注册了一大堆 Converter(一个转换器注册表)
  convert("8080", Integer.class) 时:
    → 在注册的转换器里找"能把 String 转 Integer 的"
    → 找到 StringToNumberConverter,调它转换

DefaultConversionService / DefaultFormattingConversionService
  是常用实现,Spring 启动时会准备好一个,注册了所有内置 + 自定义转换器

好处:调用方只需 conversionService.convert(x, TargetType.class)
  不用关心"具体哪个 Converter 干的活"→ 统一入口,屏蔽细节

ConversionService 是「转换的总入口」——它内部是一个「Converter 注册表」,convert(source, targetType)自动找到合适的 Converter 来干活。调用方只需一个统一的 convert 调用,不用关心「具体哪个 Converter」。它是 Facade(门面)模式的体现——把一堆 Converter 藏在统一入口后面。理解「ConversionService 是转换总入口、内部聚合所有 Converter、convert 时自动找合适的转换器、屏蔽细节」,就理解了转换体系的调度中枢。

四、Converter 家族:三种粒度

除了基础的 Converter,还有更灵活的两种,形成「三种粒度」:

① Converter<S, T>:一对一转换(最简单)
   String → Integer(就这一种)

② ConverterFactory<S, R>:一对"一族"转换
   String → R(R 的任意子类)
   典型:StringToEnumConverterFactory
   → 一个工厂搞定"String 转任意枚举",不用为每个枚举写一个 Converter

③ GenericConverter:最灵活、最底层
   - 能处理"多对多"(一批源类型↔一批目标类型)
   - 能访问字段的注解和泛型信息(TypeDescriptor)
   典型:集合↔集合、数组↔集合的转换(要知道元素类型)
   或带注解的转换(如 @NumberFormat 影响转换行为)

粒度选择:
  简单一对一 → Converter
  一转一族(枚举)→ ConverterFactory
  要类型上下文/多对多 → GenericConverter

转换器有「三种粒度」:Converter(一对一,最简单);ConverterFactory(一对一族,如 String→任意枚举,不用为每个枚举写转换器);GenericConverter(最灵活,处理多对多、能访问字段注解和泛型信息 TypeDescriptor,如集合转换要知道元素类型)。粒度递增,灵活性递增,复杂度也递增。理解「转换器三种粒度:Converter 一对一、ConverterFactory 一对一族(枚举)、GenericConverter 最灵活可访问类型上下文」,就理解了转换器家族的层次。

五、Formatter:带本地化的字符串转换

Web 层还有一个专门的转换接口 Formatter——处理「字符串 ↔ 对象 + 本地化」:

Formatter<T> 接口(两个方法):
  T parse(String text, Locale locale)      字符串 → 对象(按地区)
  String print(T object, Locale locale)    对象 → 字符串(按地区)

和 Converter 的区别:
  Converter:纯类型转换,无 Locale 概念,双向要写两个
  Formatter:专门"字符串↔对象",且带 Locale(本地化)
    → 日期、货币、数字这类"显示格式和地区相关"的转换

为什么 Web 层用 Formatter:
  同一个日期,中国显示 2026/01/01,美国显示 01/01/2026
  同一个金额,不同地区货币符号/千分位不同
  → 需要按用户 Locale 来解析和格式化 → Formatter 天生带 Locale

例:@DateTimeFormat(pattern="yyyy-MM-dd") LocalDate date
  背后就是 Formatter 在按格式解析请求参数

Formatter<T> 是「字符串↔对象 + 本地化」的转换——它有 parse(字符串转对象)和 print(对象转字符串),都带 Locale。相比 Converter(纯类型转换、无 Locale),Formatter 专门处理「显示格式和地区相关」的转换(日期、货币、数字),用在 Web 层。@DateTimeFormat@NumberFormat 背后就是 Formatter。理解「Formatter 是字符串↔对象+Locale 本地化、用于日期/货币/数字这类地区相关的格式化、Web 层用、@DateTimeFormat 背后是它」,就理解了 Formatter 的定位。

六、扩展与应用:注册自定义转换器

实际开发中,最有用的是「注册自定义转换器」处理自定义类型:

场景:@RequestParam 或 @Value 要转成自定义类型(如枚举、值对象)
  默认没有对应 Converter → 转换失败

解决:写一个 Converter/Formatter 并注册

① 写转换器:
   class StringToMoneyConverter implements Converter<String, Money> {
       public Money convert(String s) { return Money.parse(s); }
   }

② 注册(Spring MVC / Boot):
   @Configuration
   class WebConfig implements WebMvcConfigurer {
       public void addFormatters(FormatterRegistry registry) {
           registry.addConverter(new StringToMoneyConverter());
       }
   }
   (或者直接把 Converter 声明成 @Bean,Boot 会自动注册)

③ 之后自动生效:
   @RequestParam Money price、@Value("${price}") Money price
   → 都能自动用你的 Converter 转换

统一体系的价值:一处注册,所有转换场景(注入/参数/绑定)都能用

扩展转换体系很简单——写一个 Converter/Formatter 并注册(实现 WebMvcConfigurer.addFormatters 或声明成 @Bean),之后所有转换场景(@Value@RequestParam@ConfigurationProperties)都能自动用上它。这正是统一体系的价值:一处注册、处处复用。遇到「自定义类型转换失败」时,往往就是缺一个 Converter——写一个注册即可,不用在每个用到的地方手动转。理解「注册自定义 Converter/Formatter 后所有转换场景自动生效、一处注册处处复用、转换失败常是缺 Converter」,就掌握了转换体系的实际用法。

记忆钩子:「Spring 类型转换体系解决’字符串转各种类型’(@Value ${port}→int、@RequestParam ?age=18→Integer、@ConfigurationProperties 绑定);三核心:① Converter<S,T>(基础单向转换单元,一个管一种)② ConversionService(转换总入口,聚合所有 Converter,convert 时自动找合适的)③ Formatter(字符串↔对象+Locale 本地化,Web 层日期/货币,@DateTimeFormat 背后);转换器三粒度:Converter 一对一 / ConverterFactory 一对一族(枚举) / GenericConverter 最灵活可访问类型上下文;扩展:写 Converter 注册(addFormatters/@Bean)→所有场景自动生效;转换失败常是缺 Converter」

七、常见误区与追问

  • 误区:@Value 注入非字符串类型是 Spring「魔法」。 不是魔法——是类型转换体系在工作:拿到字符串后用 ConversionService 找到对应 Converter 转成目标类型(int/Duration/LocalDate 等),失败会报转换异常。
  • 误区:Converter 和 Formatter 是一回事。 Converter 是纯类型转换(S→T,无 Locale,双向要写两个);Formatter 专门「字符串↔对象」且带 Locale 本地化(parse/print),用于日期/货币/数字这类地区相关的格式化,主要在 Web 层。
  • 误区:转换逻辑要写在每个用到的地方。 恰恰相反——转换体系的价值是「收敛」:写一个 Converter 注册一次,@Value/@RequestParam/@ConfigurationProperties 等所有场景都自动复用,不用到处手动 parse。
  • 误区:自定义类型绑定失败无解。 通常就是缺一个对应的 Converter——写一个 Converter<String, 你的类型> 注册到 FormatterRegistry(或声明成 @Bean),绑定就能成功;不用改用字符串再手动转。
  • 追问:ConverterFactory 相比 Converter 好在哪? 一个 ConverterFactory 能处理「S 转成 R 的任意子类」——比如 StringToEnumConverterFactory 一个就搞定「String 转任意枚举」,不用为每个枚举类型写一个 Converter;适合「一转一族」的场景。
  • 追问:为什么 Web 层要用 Formatter 而不只是 Converter? Web 层的日期/货币/数字显示和解析与用户 Locale 相关(中国 2026/01/01、美国 01/01/2026),Formatter 的 parse/print 天生带 Locale 参数,能按地区格式化;纯 Converter 没有 Locale 概念,处理不了本地化。
  • 追问:@ConfigurationProperties 绑定各种类型靠什么? 靠类型转换体系——把 yml/properties 里的字符串值,通过 ConversionService 找对应 Converter 转成字段的目标类型(List、Map、Duration、DataSize、自定义类型等);所以自定义类型字段也需要对应的 Converter。

八、加强记忆

Spring 类型转换体系解决「字符串转各种类型」——@Value("${port}") 字符串转 int@RequestParam ?age=18Integer@ConfigurationProperties 把配置字符串绑到各种字段。三个核心Converter<S,T>——基础的单向转换单元(convert(S)→T,无状态可复用,一个只管一种,Spring 内置几十个);ConversionService——转换的总入口(内部聚合所有 Converter 组成注册表,convert(source, targetType) 时自动找到合适的 Converter,屏蔽细节,DefaultFormattingConversionService 是常用实现);Formatter<T>——字符串↔对象 + Locale 本地化parse/print,用于日期/货币/数字这类地区相关的格式化,Web 层用,@DateTimeFormat/@NumberFormat 背后)。转换器三种粒度Converter(一对一)、ConverterFactory(一对一族,如 String→任意枚举)、GenericConverter(最灵活,多对多、可访问字段注解和泛型 TypeDescriptor)。扩展:写一个 Converter/Formatter 注册(addFormatters 或声明 @Bean),所有转换场景自动生效——一处注册、处处复用;自定义类型绑定/注入失败,往往就是缺一个 Converter。一句话「Spring 类型转换:Converter<S,T> 是基础单元、ConversionService 是聚合所有 Converter 的总入口、Formatter 带 Locale 用于 Web 层格式化;三粒度 Converter/ConverterFactory/GenericConverter;写 Converter 注册后 @Value/@RequestParam/@ConfigurationProperties 全自动用,失败常是缺 Converter」。