Spring 是怎么把字符串配置/请求参数转成各种类型的?Converter 与 ConversionService 是什么?
简化版
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.parseInt、SimpleDateFormat.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=18 转 Integer、@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」。