Spring Framework 7.0.9 深度剖析-12-类型转换与格式化体系
12.1 本章导读
Spring 容器里发生的一切"赋值",本质上都在跟类型打交道。XML 配置文件里写的 value="42" 是个字符串,而 bean 的 age 属性是 int;@Value("${timeout}") 注入进来的是字符串,而字段声明是 Duration;表单提交上来的请求参数全是 String,而命令对象的字段是 BigDecimal、LocalDate、List<Long>。把这些"声明出来的字符串"变成"运行期真正需要的对象",就是类型转换体系的职责。
本章围绕 Spring Framework 7.0.9 源码,完整剖析这套体系。它由两代技术叠加而成:
- JavaBeans PropertyEditor 体系(JDK 原生,Spring 1.x 起就有):以
java.beans.PropertyEditor为核心,Spring 在spring-beans模块里封装为PropertyEditorRegistry、PropertyEditorRegistrySupport、TypeConverter、TypeConverterSupport、TypeConverterDelegate。这是一套按 bean 实例持有、非线程安全的体系。 - ConversionService 体系(Spring 3.0 引入):以
org.springframework.core.convert.ConversionService为核心,配套Converter、ConverterFactory、GenericConverter、ConditionalConverter、TypeDescriptor等 SPI,实现在spring-core模块。这是一套可线程安全共享、可注入容器的体系。 - 格式化体系(Spring 3.0 引入,位于
spring-context):Formatter<T>、Printer<T>、Parser<T>、AnnotationFormatterFactory<A>、FormattingConversionService、DefaultFormattingConversionService,以及@NumberFormat、@DateTimeFormat两个注解。它本质上是 ConversionService 体系在"字段级格式化"场景下的扩展——把Formatter桥接成GenericConverter注册进GenericConversionService。
这三套东西不是三选一的关系,而是长期并存、互相桥接、由同一个执行者统一调度。在 7.0.9 中,真正的执行入口仍然是 TypeConverterDelegate.convertIfNecessary(...):它先问自定义 PropertyEditor,再问 ConversionService,再退回默认 PropertyEditor,最后走一条硬编码的标准转换规则链。读懂这一个方法,就读懂了 Spring 数据绑定的半壁江山;再读懂 GenericConversionService 的查找算法与缓存结构,就读懂了另一半。
本章涉及的模块与路径:
spring-beans/src/main/java/org/springframework/beans/:TypeConverter.java、TypeConverterSupport.java、TypeConverterDelegate.java、PropertyEditorRegistry.java、PropertyEditorRegistrySupport.java。spring-core/src/main/java/org/springframework/core/convert/:ConversionService.java、TypeDescriptor.java、ConversionFailedException.java、ConverterNotFoundException.java。spring-core/src/main/java/org/springframework/core/convert/converter/:Converter.java、ConverterFactory.java、ConverterRegistry.java、GenericConverter.java(含嵌套类ConvertiblePair)、ConditionalConverter.java、ConditionalGenericConverter.java。spring-core/src/main/java/org/springframework/core/convert/support/:GenericConversionService.java、DefaultConversionService.java、ConfigurableConversionService.java、ConversionServiceFactory.java、ConversionUtils.java、ConvertingPropertyEditorAdapter.java,以及四十余个内置转换器。spring-context/src/main/java/org/springframework/format/:Formatter.java、Printer.java、Parser.java、FormatterRegistry.java、AnnotationFormatterFactory.java、FormatterRegistrar.java,以及annotation/、number/、datetime/子包。spring-context/src/main/java/org/springframework/format/support/:FormattingConversionService.java、DefaultFormattingConversionService.java、FormattingConversionServiceFactoryBean.java。spring-context/src/main/java/org/springframework/validation/DataBinder.java、spring-context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java。
前置知识:Java 反射基础(java.beans.PropertyEditor、泛型擦除、MethodParameter)、Spring bean 生命周期中属性填充(populateBean)的位置(本章只讲与转换相关的切面)。
读完本章你将获得:① 一次 convertIfNecessary 从入口到结果的完整调用栈与每一步的判定条件;② GenericConversionService 的注册表、缓存结构与类型层次查找算法;③ 7.0.9 默认注册的全部转换器清单及各自实现要点(以本仓库实际代码统计为准);④ @NumberFormat/@DateTimeFormat 如何通过 AnnotationFormatterFactory 变成条件转换器;⑤ ConversionService 在容器中的装配路径与 DataBinder 的整合方式;⑥ 常见坑的源码级解释。
两个版本事实先立在这里(后文会反复回到):其一,7.0.9 的 GenericConversionService 已经没有旧资料里常讲的 classCache/cachedConverters 字段,类型层次列表在缓存未命中时是每次重新计算的;其二,旧资料里常引用的 org.springframework.context.support.ApplicationConversionService 在 7.0.9 源码树中已经不存在(它后来被挪出了框架核心、归 Spring Boot 所有),框架层默认配置入口就是 DefaultConversionService 与 DefaultFormattingConversionService 两个静态方法。
12.2 核心概念:两套体系的抽象与术语
12.2.1 PropertyEditor:JDK 的"字符串 ↔ 对象"契约
java.beans.PropertyEditor 是 JDK 自带的属性编辑接口,核心方法只有两个:void setAsText(String text) 把字符串解析成对象(结果存于内部,用 getValue() 取出),String getAsText() 把对象格式化成字符串。它的设计目标是 GUI 属性编辑器,但 Spring 从 1.x 开始把它借过来做数据绑定。
这个体系在 Spring 里的抽象位于 spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistry.java:
public interface PropertyEditorRegistry {
void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor);
void registerCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath,
PropertyEditor propertyEditor);
@Nullable PropertyEditor findCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath);
}
registerCustomEditor:注册自定义编辑器。两个重载——按类型注册,或按"类型 + 属性路径"注册(路径粒度更细,例如只为user.address.zipcode这个属性换一个编辑器)。findCustomEditor:反查。给定期望类型与属性路径,返回匹配的编辑器。
注意它的设计特征:编辑器实例是有状态的、不可共享的(每次 setAsText 后内部保存当前值),因此 Spring 的 TypeConverter 接口注释里明确写着 TypeConverter 本身不是线程安全的——这是它必须被 BeanWrapperImpl 这类"每 bean 一个"的对象持有的根本原因。
12.2.2 TypeConverter:Spring 的转换语义接口
spring-beans/src/main/java/org/springframework/beans/TypeConverter.java 是转换语义的抽象,与 PropertyEditorRegistry 配套使用(但不强制):
public interface TypeConverter {
<T> @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType) throws TypeMismatchException;
<T> @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
@Nullable MethodParameter methodParam) throws TypeMismatchException;
<T> @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
@Nullable Field field) throws TypeMismatchException;
default <T> @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
@Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {
throw new UnsupportedOperationException("TypeDescriptor resolution not supported");
}
}
四个重载的差异只在类型上下文的丰富程度:只给 Class → 丢失泛型;给 MethodParameter/Field → 可以解析出 List<User> 里的 User;直接给 TypeDescriptor → 连字段上的注解(如 @DateTimeFormat)都能拿到。方法名 convertIfNecessary("如有必要则转换")是灵魂:如果 value 本来就是 requiredType 的实例,就什么都不做直接返回。
12.2.3 ConversionService:可共享的转换服务
spring-core/src/main/java/org/springframework/core/convert/ConversionService.java:
public interface ConversionService {
boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
<T> @Nullable T convert(@Nullable Object source, Class<T> targetType);
default @Nullable Object convert(@Nullable Object source, TypeDescriptor targetType) {
return convert(source, TypeDescriptor.forObject(source), targetType);
}
@Nullable Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
}
与 TypeConverter 的本质区别:① 入参出参都是无状态的纯值,没有"当前对象上下文",实现天然可做成线程安全、全容器共享;② 类型上下文用 TypeDescriptor 显式传递,而不是靠反射现场解析。接口注释里有一条重要提醒(canConvert 的 javadoc):对集合/数组/Map 之间的转换,canConvert 返回 true 不代表 convert 一定成功——元素层面的可转换性是运行期才检查的,调用方要自己处理 ConversionException。
12.2.4 Converter SPI 四件套与 TypeDescriptor
spring-core/.../convert/converter/ 包下是注册侧 SPI:
Converter<S,T>:函数式接口,单方向、单类型对,T convert(S source)。注释明确:实现必须线程安全、可共享。ConverterFactory<S,R>:"区间工厂"——getConverter(Class<T> targetType)为同一个源类型 S 派生出指向 R 的任意子类型 T 的转换器,典型如NumberToNumberConverterFactory(Number→Byte/Integer/Double…)。GenericConverter:最灵活也最复杂的 SPI,Set<ConvertiblePair> getConvertibleTypes()声明自己能转哪些类型对,Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType)在转换时能拿到字段级上下文(泛型、注解)。ConditionalConverter:boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType)——运行期根据字段上下文(比如目标字段是否带@DateTimeFormat)决定自己是否本次生效。ConditionalGenericConverter只是它与GenericConverter的接口组合。ConverterRegistry:注册 API(addConverter三个重载 +addConverterFactory+removeConvertible)。ConfigurableConversionService:ConversionService(读)与ConverterRegistry(写)的接口组合。
TypeDescriptor(spring-core/.../convert/TypeDescriptor.java)是这套体系的"类型上下文载体":它包装一个 ResolvableType(携带泛型信息)+ 字段注解集合,提供 getElementTypeDescriptor()(集合元素类型)、getMapKeyTypeDescriptor()/getMapValueTypeDescriptor()、hasAnnotation(Class)/getAnnotation(Class)、isAssignableTo(TypeDescriptor) 等方法。没有它,"把 List<String> 转成 List<Long>"这种转换就不可能知道元素该往哪转。
12.2.5 Formatter:面向"字段显示"的格式化抽象
spring-context/.../format/Formatter.java:
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
一个空接口,继承 Printer<T>(String print(T field, Locale locale),对象→文本)与 Parser<T>(T parse(String text, Locale locale),文本→对象)。它和 Converter 的分工:Converter 回答"这个对象能不能变成那个对象",Formatter 回答"这个字段在特定 Locale 下该长什么样、该怎么被读回来"。AnnotationFormatterFactory<A extends Annotation> 则把"注解 → Formatter"的映射抽象成工厂:Set<Class<?>> getFieldTypes() 声明 A 能标注在哪些字段类型上,getPrinter(A annotation, Class<?> fieldType)/getParser(A annotation, Class<?> fieldType) 根据注解属性现造格式化器。
12.2.6 核心概念关系图
PropertyEditor 体系(spring-beans,每 bean 持有,非线程安全)
PropertyEditorRegistry ──┐
PropertyEditorRegistrySupport ── 默认编辑器表 + 自定义编辑器表
TypeConverterSupport ── extends PropertyEditorRegistrySupport implements TypeConverter
TypeConverterDelegate ── 真正的执行引擎:Editor/ConversionService 双通道
ConversionService 体系(spring-core,线程安全,全容器共享)
ConversionService(canConvert/convert)┐
ConverterRegistry(addConverter*3/addConverterFactory/removeConvertible)├─ ConfigurableConversionService
GenericConversionService ──┬─ converters: ConvertiblePair→ConvertersForPair + globalConverters
├─ converterCache: ConverterCacheKey→GenericConverter
└─ DefaultConversionService ── 注册全部内置转换器
格式化体系(spring-context)
FormattingConversionService extends GenericConversionService implements FormatterRegistry
Formatter/Printer/Parser ──(桥接)──> PrinterConverter/ParserConverter (GenericConverter)
AnnotationFormatterFactory ──(桥接)──> AnnotationPrinterConverter/AnnotationParserConverter (ConditionalGenericConverter)
DefaultFormattingConversionService ── addDefaultConverters + addDefaultFormatters
后续章节按"先执行引擎(TypeConverterDelegate),再服务内核(GenericConversionService),再注册表(Default*),再格式化层,再容器集成"的顺序展开。
12.3 源码剖析(一):PropertyEditor 注册表与默认编辑器
12.3.1 PropertyEditorRegistrySupport 的五个容器
spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java 是整个 PropertyEditor 体系的存储核心。字段区(第 95-112 行):
private @Nullable ConversionService conversionService;
private boolean defaultEditorsActive = false;
private boolean configValueEditorsActive = false;
private @Nullable PropertyEditorRegistrar defaultEditorRegistrar;
@SuppressWarnings("NullAway.Init")
private Map<Class<?>, PropertyEditor> defaultEditors;
private @Nullable Map<Class<?>, PropertyEditor> overriddenDefaultEditors;
private @Nullable Map<Class<?>, PropertyEditor> customEditors;
private @Nullable Map<String, CustomEditorHolder> customEditorsForPath;
private @Nullable Map<Class<?>, PropertyEditor> customEditorCache;
逐容器说明:
conversionService:本注册表关联的 ConversionService。注意它就躺在 PropertyEditor 体系的类里——这是两代体系握手的地方,TypeConverterDelegate正是通过getConversionService()拿到它的(见 12.4)。setConversionService是个普通 setter,谁装配它后面会讲(AbstractBeanFactory.getTypeConverter()与BeanWrapperImpl)。defaultEditorsActive/configValueEditorsActive:两个开关。默认编辑器是惰性注册的——registerDefaultEditors()只把开关打开,真正建表推迟到第一次getDefaultEditor。configValueEditorsActive控制是否注册StringArrayPropertyEditor这类"配置专用"编辑器(注释明确说它们不适合数据绑定场景)。defaultEditors:类型→默认编辑器的主表,createDefaultEditors()时一次性建好。overriddenDefaultEditors:6.2.3 引入的"覆盖默认编辑器"表。overrideDefaultEditor的 javadoc 点破了一条关键优先级规则:覆盖编辑器在语义上仍是默认编辑器,ConversionService 会盖过它;而 custom editors 通常会盖过 ConversionService。这条规则直接决定了 12.4 里TypeConverterDelegate先查 custom editor、再问 ConversionService 的顺序。customEditors:类型→自定义编辑器(用户显式registerCustomEditor的)。customEditorsForPath:属性路径→编辑器持有者(CustomEditorHolder),如user.addresses[0].city。customEditorCache:按超类/接口匹配命中的结果缓存,避免每次查找都遍历customEditors做可赋值性判断。
12.3.2 createDefaultEditors:默认编辑器全清单(7.0.9 实测)
createDefaultEditors()(第 212-281 行)一次性把默认编辑器铺进 defaultEditors 表。按代码原文分四组:
第一组(JDK 没有默认编辑器的简单类型,第 217-234 行):
this.defaultEditors.put(Charset.class, new CharsetEditor());
this.defaultEditors.put(Class.class, new ClassEditor());
this.defaultEditors.put(Class[].class, new ClassArrayEditor());
this.defaultEditors.put(Currency.class, new CurrencyEditor());
this.defaultEditors.put(File.class, new FileEditor());
this.defaultEditors.put(InputStream.class, new InputStreamEditor());
this.defaultEditors.put(InputSource.class, new InputSourceEditor());
this.defaultEditors.put(Locale.class, new LocaleEditor());
this.defaultEditors.put(Path.class, new PathEditor());
this.defaultEditors.put(Pattern.class, new PatternEditor());
this.defaultEditors.put(Properties.class, new PropertiesEditor());
this.defaultEditors.put(Reader.class, new ReaderEditor());
this.defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor());
this.defaultEditors.put(TimeZone.class, new TimeZoneEditor());
this.defaultEditors.put(URI.class, new URIEditor());
this.defaultEditors.put(URL.class, new URLEditor());
this.defaultEditors.put(UUID.class, new UUIDEditor());
this.defaultEditors.put(ZoneId.class, new ZoneIdEditor());
第二组(集合/Map 编辑器默认实例,第 238-242 行):Collection、Set、SortedSet、List 各一个 CustomCollectionEditor,SortedMap 一个 CustomMapEditor。注释说明这些可以被用户注册同类型 custom editor 覆盖。
第三组(基本类型与数字包装,第 245-271 行):byte[]/char[] 两个基本数组编辑器;char/Character 用 CharacterEditor(JDK 没有 char 的默认编辑器);boolean/Boolean 用 Spring 的 CustomBooleanEditor(比 JDK 默认接受更多标志值);随后是 byte/Byte、short/Short、int/Integer、long/Long、float/Float、double/Double 六对全部换成 CustomNumberEditor,外加 BigDecimal、BigInteger。注释解释了动机:JDK 不为 number wrapper 类型提供默认编辑器,Spring 用自己的 CustomNumberEditor 统一接管。
第四组(配置值编辑器,第 274-280 行,仅 configValueEditorsActive 时注册):
if (this.configValueEditorsActive) {
StringArrayPropertyEditor sae = new StringArrayPropertyEditor();
this.defaultEditors.put(String[].class, sae);
this.defaultEditors.put(short[].class, sae);
this.defaultEditors.put(int[].class, sae);
this.defaultEditors.put(long[].class, sae);
}
即把逗号分隔字符串拆成数组/数值数组——这是 @Value("${...}") 注入 String[]、int[] 时能工作的原因之一。
12.3.3 findCustomEditor:路径优先、类型兜底的两级查找
findCustomEditor(requiredType, propertyPath)(第 325-349 行)是 TypeConverterDelegate 转换流程第一站调用的方法:
public @Nullable PropertyEditor findCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath) {
Class<?> requiredTypeToUse = requiredType;
if (propertyPath != null) {
if (this.customEditorsForPath != null) {
// Check property-specific editor first.
PropertyEditor editor = getCustomEditor(propertyPath, requiredType);
if (editor == null) {
List<String> strippedPaths = new ArrayList<>();
addStrippedPropertyPaths(strippedPaths, "", propertyPath);
for (Iterator<String> it = strippedPaths.iterator(); it.hasNext() && editor == null;) {
String strippedPath = it.next();
editor = getCustomEditor(strippedPath, requiredType);
}
}
if (editor != null) {
return editor;
}
}
if (requiredType == null) {
requiredTypeToUse = getPropertyType(propertyPath);
}
}
// No property-specific editor -> check type-specific editor.
return getCustomEditor(requiredTypeToUse);
}
逐段解读:
- 先精确路径。
getCustomEditor(propertyPath, requiredType)直接查customEditorsForPath这张"路径→持有者"表。 - 再剥键重试。路径没命中时,用
addStrippedPropertyPaths递归地把map[key]、list[index]这类键逐次剥掉,生成accounts、accounts[key]、accounts[0]、accounts[0].name等一系列变体,逐个再查。这样用户注册在accounts上的编辑器,对accounts[0].name的元素转换也能生效。 - 类型兜底。路径粒度都没有命中,才走
getCustomEditor(requiredTypeToUse)按类型查;若调用方连requiredType都没给(集合元素场景常见),就调getPropertyType(propertyPath)反推——这个方法在本类返回 null,由BeanWrapperImpl覆盖实现去问内省结果。
按类型查的 getCustomEditor(Class)(第 408-439 行)还有一层"超类匹配 + 缓存":
private @Nullable PropertyEditor getCustomEditor(@Nullable Class<?> requiredType) {
if (requiredType == null || this.customEditors == null) {
return null;
}
// Check directly registered editor for type.
PropertyEditor editor = this.customEditors.get(requiredType);
if (editor == null) {
// Check cached editor for type, registered for superclass or interface.
if (this.customEditorCache != null) {
editor = this.customEditorCache.get(requiredType);
}
if (editor == null) {
// Find editor for superclass or interface.
for (Map.Entry<Class<?>, PropertyEditor> entry : this.customEditors.entrySet()) {
Class<?> key = entry.getKey();
if (key.isAssignableFrom(requiredType)) {
editor = entry.getValue();
// Cache editor for search type, to avoid the overhead
// of repeated assignable-from checks.
if (this.customEditorCache == null) {
this.customEditorCache = new HashMap<>();
}
this.customEditorCache.put(requiredType, editor);
if (editor != null) {
break;
}
}
}
}
}
return editor;
}
即:直接注册的类型没命中 → 查 customEditorCache → 遍历 customEditors 找"注册类型是 requiredType 的父类/接口"的编辑器,找到后把结果缓存。例如用户为 Number 注册了一个编辑器,那么给 Integer 属性找编辑器时就能命中它。注意 registerCustomEditor 在按类型注册成功后会执行 this.customEditorCache = null(第 320 行)——新编辑器注册进来,旧的超类匹配缓存整体作废,这是缓存一致性的代价。
12.3.4 CustomEditorHolder:路径编辑器的类型适配
路径注册的编辑器存在 CustomEditorHolder(第 530-568 行)里,它持有编辑器本身 + 注册时声明的类型。getPropertyEditor(requiredType) 的判定逻辑值得一看:
private PropertyEditor getPropertyEditor(@Nullable Class<?> requiredType) {
// Special case: If no required type specified, which usually only happens for
// Collection elements, or required type is not assignable to registered type,
// which usually only happens for generic properties of type Object -
// then return PropertyEditor if not registered for Collection or array type.
if (this.registeredType == null ||
(requiredType != null &&
(ClassUtils.isAssignable(this.registeredType, requiredType) ||
ClassUtils.isAssignable(requiredType, this.registeredType))) ||
(requiredType == null &&
(!Collection.class.isAssignableFrom(this.registeredType) && !this.registeredType.isArray()))) {
return this.propertyEditor;
}
else {
return null;
}
}
三种放行:① 注册时根本没声明类型(路径专用编辑器,直接放行);② 声明类型与查询类型双向可赋值其一(父子关系,放行);③ 查询类型为 null(集合元素)且该编辑器不是为集合/数组类型注册的——此时假设它是"给元素用的",放行。其余情况返回 null,让查找继续。这个"放行规则"解释了一个常见现象:为什么你在路径上注册的编辑器对 List<Foo> 属性生效、但对 List 本身不生效。
12.3.5 PropertyEditor 体系的桥接者:ConvertingPropertyEditorAdapter
两代体系除了"同一个执行者内部双通道"之外,还有反向桥接:把 ConversionService 包装成 PropertyEditor。spring-core/src/main/java/org/springframework/core/convert/support/ConvertingPropertyEditorAdapter.java:
public class ConvertingPropertyEditorAdapter extends PropertyEditorSupport {
private final ConversionService conversionService;
private final TypeDescriptor targetDescriptor;
private final boolean canConvertToString;
public ConvertingPropertyEditorAdapter(ConversionService conversionService, TypeDescriptor targetDescriptor) {
Assert.notNull(conversionService, "ConversionService must not be null");
Assert.notNull(targetDescriptor, "TypeDescriptor must not be null");
this.conversionService = conversionService;
this.targetDescriptor = targetDescriptor;
this.canConvertToString = conversionService.canConvert(this.targetDescriptor, TypeDescriptor.valueOf(String.class));
}
@Override
public void setAsText(@Nullable String text) throws IllegalArgumentException {
setValue(this.conversionService.convert(text, TypeDescriptor.valueOf(String.class), this.targetDescriptor));
}
@Override
public @Nullable String getAsText() {
if (this.canConvertToString) {
return (String) this.conversionService.convert(getValue(), this.targetDescriptor, TypeDescriptor.valueOf(String.class));
}
else {
return null;
}
}
}
它是一个 PropertyEditorSupport 子类:setAsText 把字符串交给 ConversionService 转成目标类型存入 value;getAsText 反向把当前对象转回字符串。构造时预先问一句 canConvert(targetDescriptor, String.class) 决定 getAsText 是否可用——不可转字符串时返回 null 而不是抛异常,这符合 PropertyEditor 契约里"返回 null 表示不支持"的惯例。有了它,任何只接受 PropertyEditor 的老代码路径(例如某些 PropertyEditorRegistrar、JavaBeans 内省工具)都能复用 ConversionService 里注册的转换器。
12.4 源码剖析(二):TypeConverterDelegate 的双通道执行链
12.4.1 TypeConverterSupport:四个重载如何收拢成一次委托
spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java 是 TypeConverter 的基础实现,也是 BeanWrapperImpl 的父类链一环。它继承 PropertyEditorRegistrySupport(注册表能力)并实现 TypeConverter:
public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport implements TypeConverter {
@Nullable TypeConverterDelegate typeConverterDelegate;
@Override
public <T> @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType) throws TypeMismatchException {
return convertIfNecessary(null, value, requiredType, TypeDescriptor.valueOf(requiredType));
}
@Override
public <T> @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
@Nullable MethodParameter methodParam) throws TypeMismatchException {
return convertIfNecessary((methodParam != null ? methodParam.getParameterName() : null), value, requiredType,
(methodParam != null ? new TypeDescriptor(methodParam) : TypeDescriptor.valueOf(requiredType)));
}
@Override
public <T> @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
@Nullable Field field) throws TypeMismatchException {
return convertIfNecessary((field != null ? field.getName() : null), value, requiredType,
(field != null ? new TypeDescriptor(field) : TypeDescriptor.valueOf(requiredType)));
}
@Override
public <T> @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
@Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {
return convertIfNecessary(null, value, requiredType, typeDescriptor);
}
private <T> @Nullable T convertIfNecessary(@Nullable String propertyName, @Nullable Object value,
@Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {
Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate");
try {
return this.typeConverterDelegate.convertIfNecessary(
propertyName, null, value, requiredType, typeDescriptor);
}
catch (ConverterNotFoundException | IllegalStateException ex) {
throw new ConversionNotSupportedException(value, requiredType, ex);
}
catch (ConversionException | IllegalArgumentException ex) {
throw new TypeMismatchException(value, requiredType, ex);
}
}
}
逐段解读:
- 上下文收拢。四个 public 重载只是把不同的类型来源统一翻译成"属性名 + TypeDescriptor"二元组:
Class→TypeDescriptor.valueOf;MethodParameter→new TypeDescriptor(methodParam)(连带参数名、泛型、注解);Field→new TypeDescriptor(field)。注意propertyName也在这里确定:方法参数取参数名、字段取字段名,其余为 null。 - 委托。真正干活的是包可见的
TypeConverterDelegate,由子类(BeanWrapperImpl、SimpleTypeConverter)在构造时装配。 - 异常翻译。这一层是异常语义的分界:
ConverterNotFoundException(ConversionService 里找不到转换器)与IllegalStateException("no matching editors or conversion strategy found")被翻译成ConversionNotSupportedException——"系统根本不会这种转换";ConversionException(转换器执行失败)与IllegalArgumentException(编辑器/构造器失败)被翻译成TypeMismatchException——"转换尝试了但值不对"。数据绑定层正是靠这两个异常类型区分"配置问题"与"用户输入问题"。
12.4.2 TypeConverterDelegate 的构造与字段
spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java:
class TypeConverterDelegate {
private static final Log logger = LogFactory.getLog(TypeConverterDelegate.class);
private final PropertyEditorRegistrySupport propertyEditorRegistry;
private final @Nullable Object targetObject;
public TypeConverterDelegate(PropertyEditorRegistrySupport propertyEditorRegistry) {
this(propertyEditorRegistry, null);
}
public TypeConverterDelegate(PropertyEditorRegistrySupport propertyEditorRegistry, @Nullable Object targetObject) {
this.propertyEditorRegistry = propertyEditorRegistry;
this.targetObject = targetObject;
}
propertyEditorRegistry:编辑器注册表(就是宿主BeanWrapperImpl自己),custom editor、default editor、关联的 ConversionService 都从这里拿。targetObject:正在操作的目标 bean 实例。它只被attemptToConvertStringToEnum用于拿 ClassLoader——当目标类型声明为裸Enum时,允许把"com.xxx.Color.RED"这种全限定名字符串解析成枚举常量。
12.4.3 主方法:convertIfNecessary 的七段判定链
带 TypeDescriptor 的五参版本是全章最重要的方法(第 113-284 行)。按执行顺序切成七段逐段讲。
第 1 段:找 custom editor(第 117-118 行)
// Custom editor for this type?
PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);
第一件事就是查 custom editor——用户显式注册的编辑器优先级最高,这与 overrideDefaultEditor javadoc 里"custom editors usually override the ConversionService"的规则严格一致。
第 2 段:有 editor 就跳过;否则先问 ConversionService(第 120-135 行)
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;
}
}
四个条件缺一不可:没有 custom editor、有 ConversionService、新值非 null、目标 TypeDescriptor 非 null。满足后用 TypeDescriptor.forObject(newValue) 从值的实际类型反推源类型描述符,先 canConvert 再 convert。注意 newValue 本身必须非 null——null 走不了 ConversionService 通道,null 的语义在后文(convertNullSource/Optional.empty)单独处理。
失败被 catch 成 conversionAttemptEx 保存而不是直接抛出——这是"双通道互备"的关键设计:ConversionService 失败不代表世界末日,下面还有 PropertyEditor 与标准规则两条路;只有当所有路都走不通时,才把这个异常重新抛出去。
第 3 段:类型不匹配才走 PropertyEditor(第 137-159 行)
Object convertedValue = newValue;
// Value not of required type?
if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) {
if (typeDescriptor != null && requiredType != null && Collection.class.isAssignableFrom(requiredType)) {
TypeDescriptor elementTypeDesc = typeDescriptor.getElementTypeDescriptor();
if (elementTypeDesc != null) {
Class<?> elementType = elementTypeDesc.getType();
if (convertedValue instanceof String text) {
if (Class.class == elementType || Enum.class.isAssignableFrom(elementType)) {
convertedValue = StringUtils.commaDelimitedListToStringArray(text);
}
if (editor == null && String.class != elementType) {
editor = findDefaultEditor(elementType.arrayType());
}
}
}
}
if (editor == null) {
editor = findDefaultEditor(requiredType);
}
convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor);
}
进入编辑器通道的条件:editor != null(第 1 段已找到 custom editor)或值还不是 requiredType 的实例。第二个条件就是"如有必要则转换"的字面实现——值类型已经对,整个 if 块跳过。
集合元素的特殊处理:目标是 Collection 且值是字符串时,先把它按逗号拆成数组;若元素类型不是 String,再为"元素数组类型"找默认编辑器(例如 Integer[] 的默认编辑器),让逗号串里的每个元素后续能被逐个转换。
findDefaultEditor(requiredType)(第 341-352 行)本身也是两级:先查注册表默认编辑器表,没有再 BeanUtils.findEditorByConvention(requiredType)——即 JDK 约定俗成的 XxxEditor 同包同名查找。
第 4 段:标准转换规则链(第 161-233 行)
经过编辑器通道后(或根本没进),requiredType != null 时进入硬编码标准规则。按 if-else 顺序:
if (convertedValue != null) {
if (Object.class == requiredType) {
return (T) convertedValue;
}
else if (requiredType.isArray()) {
// Array required -> apply appropriate conversion of elements.
if (convertedValue instanceof String text &&
Enum.class.isAssignableFrom(requiredType.componentType())) {
convertedValue = StringUtils.commaDelimitedListToStringArray(text);
}
return (T) convertToTypedArray(convertedValue, propertyName, requiredType.componentType());
}
else if (convertedValue.getClass().isArray()) {
if (Collection.class.isAssignableFrom(requiredType)) {
convertedValue = convertToTypedCollection(CollectionUtils.arrayToList(convertedValue),
propertyName, requiredType, typeDescriptor);
standardConversion = true;
}
else if (Array.getLength(convertedValue) == 1) {
convertedValue = Array.get(convertedValue, 0);
standardConversion = true;
}
}
else if (convertedValue instanceof Collection<?> coll) {
convertedValue = convertToTypedCollection(coll, propertyName, requiredType, typeDescriptor);
standardConversion = true;
}
else if (convertedValue instanceof Map<?, ?> map) {
convertedValue = convertToTypedMap(map, propertyName, requiredType, typeDescriptor);
standardConversion = true;
}
要点:
Object类型直接放行;目标是数组时走convertToTypedArray——它对集合、数组、单值三种输入分别处理,元素逐个递归调用convertIfNecessary(带propName[i]的索引属性名)。- 输入是数组而目标是集合 → 数组转 List 后
convertToTypedCollection;数组长度为 1 且目标是单值 → 拆开取第一个元素。这就是"42"经StringArrayPropertyEditor变成["42"]后,还能喂给int属性的原因。 Collection/Map输入 →convertToTypedCollection/convertToTypedMap:从typeDescriptor.getElementTypeDescriptor()拿元素泛型类型,逐元素/逐键值递归转换;originalAllowed标记记录"元素是否原封不动",全没动就返回原集合,动了才造新集合。这里能正确处理List<String>→List<Long>,全靠 TypeDescriptor 携带的泛型信息——这就是TypeConverter.convertIfNecessary(value, Long.class, methodParam)与convertIfNecessary(value, Long.class)两个重载行为不同的根源。
接着是字符串与数字两条收尾规则:
if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) {
// We can stringify any primitive value...
return (T) convertedValue.toString();
}
else if (convertedValue instanceof String text && !requiredType.isInstance(convertedValue)) {
if (conversionAttemptEx == null && !requiredType.isInterface() && !requiredType.isEnum()) {
try {
Constructor<T> strCtor = requiredType.getConstructor(String.class);
return BeanUtils.instantiateClass(strCtor, convertedValue);
}
catch (NoSuchMethodException ex) {
// proceed with field lookup
……
}
}
String trimmedValue = text.trim();
if (requiredType.isEnum() && trimmedValue.isEmpty()) {
// It's an empty enum identifier: reset the enum value to null.
return null;
}
convertedValue = attemptToConvertStringToEnum(requiredType, trimmedValue, convertedValue);
standardConversion = true;
}
else if (convertedValue instanceof Number num && Number.class.isAssignableFrom(requiredType)) {
convertedValue = NumberUtils.convertNumberToTargetClass(num, (Class<Number>) requiredType);
standardConversion = true;
}
- 基本类型 → String:直接
toString(),短路返回。 - String → 自定义类型:若 ConversionService 没试过(
conversionAttemptEx == null)、目标不是接口也不是枚举,就找public Xxx(String)构造器直接反射实例化。这条规则解释了为什么java.net.URI、java.util.Locale这类带 String 构造器的类型在 XML 里能直接配字符串——当然前提是没有更专门的编辑器/转换器抢先。 - 空串 → 枚举:直接返回 null("空枚举标识符重置为 null")。
- 非空串 → 枚举:
attemptToConvertStringToEnum——先按裸Enum全限定名解析(com.xxx.Color.RED),再退到requiredType.getField(trimmedValue)反射拿枚举常量。 - Number → Number 子类:
NumberUtils.convertNumberToTargetClass(带溢出/精度处理,见 12.11 的坑)。
第 5 段:null 的归宿(第 234-239 行)
else {
// convertedValue == null
if (requiredType == Optional.class) {
convertedValue = Optional.empty();
}
}
null 值唯一的特殊待遇:目标声明是 Optional 时给 Optional.empty(),其余就保持 null。
第 6 段:最终校验与 ConversionService 兜底(第 241-272 行)
if (!ClassUtils.isAssignableValue(requiredType, convertedValue)) {
if (conversionAttemptEx != null) {
// Original exception from former ConversionService call above...
throw conversionAttemptEx;
}
else if (conversionService != null && typeDescriptor != null) {
// ConversionService not tried before, probably custom editor found
// but editor couldn't produce the required type...
TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
}
}
// Definitely doesn't match: throw IllegalArgumentException/IllegalStateException
StringBuilder msg = new StringBuilder();
msg.append("Cannot convert value of type '").append(ClassUtils.getDescriptiveType(newValue));
msg.append("' to required type '").append(ClassUtils.getQualifiedName(requiredType)).append('\'');
if (propertyName != null) {
msg.append(" for property '").append(propertyName).append('\'');
}
if (editor != null) {
msg.append(": PropertyEditor [").append(editor.getClass().getName()).append(
"] returned inappropriate value of type '").append(
ClassUtils.getDescriptiveType(convertedValue)).append('\'');
throw new IllegalArgumentException(msg.toString());
}
else {
msg.append(": no matching editors or conversion strategy found");
throw new IllegalStateException(msg.toString());
}
}
产物仍不是 requiredType 的实例时:① 之前 ConversionService 已经失败过(conversionAttemptEx != null)→ 原异常抛出;② 之前没试过(通常因为第 1 段就找到了 custom editor,但编辑器没产出正确类型)→ 现在补问 ConversionService;③ 都不行 → 拼异常消息并抛出,区分两种情形:编辑器产出了错误类型(IllegalArgumentException,"PropertyEditor X returned inappropriate value")与根本没有任何策略(IllegalStateException,"no matching editors or conversion strategy found")。这条消息正是 TypeConverterSupport 里把 IllegalStateException 翻译成 ConversionNotSupportedException 的那个异常——异常消息里带原类型、目标类型、属性名、编辑器类名,定位问题非常友好。
第 7 段:ConversionService 失败被 PropertyEditor 成功吸收(第 275-281 行)
if (conversionAttemptEx != null) {
if (editor == null && !standardConversion && requiredType != null && Object.class != requiredType) {
throw conversionAttemptEx;
}
logger.debug("Original ConversionService attempt failed - ignored since " +
"PropertyEditor based conversion eventually succeeded", conversionAttemptEx);
}
走到这里说明前面所有路径都成功产出了类型匹配的值。但如果 ConversionService 早先失败过,是否要吞掉那个异常?判定:custom editor 存在、或发生过标准规则转换、或目标就是 Object——满足任一,说明 PropertyEditor/标准规则这条路后来成功了,就把 ConversionService 的失败降级为 debug 日志吞掉;只有"editor 为 null、标准规则也没用、目标不是 Object"这种彻底没别的路的情况,才把原始 conversionAttemptEx 抛出去。这就是"双通道"的最终语义:两条路都失败才算失败。
12.4.4 doConvertValue:PropertyEditor 的两种调用形态
private Object doConvertValue(@Nullable Object oldValue, @Nullable Object newValue,
@Nullable Class<?> requiredType, @Nullable PropertyEditor editor) {
Object convertedValue = newValue;
if (editor != null && !(convertedValue instanceof String)) {
// Not a String -> use PropertyEditor's setValue.
try {
editor.setValue(convertedValue);
Object newConvertedValue = editor.getValue();
if (newConvertedValue != convertedValue) {
convertedValue = newConvertedValue;
// Reset PropertyEditor: It already did a proper conversion.
// Don't use it again for a setAsText call.
editor = null;
}
}
catch (Exception ex) { …… }
}
Object returnValue = convertedValue;
if (requiredType != null && !requiredType.isArray() && convertedValue instanceof String[] array) {
convertedValue = StringUtils.arrayToCommaDelimitedString(array);
}
if (convertedValue instanceof String newTextValue) {
if (editor != null) {
return doConvertTextValue(oldValue, newTextValue, editor);
}
else if (String.class == requiredType) {
returnValue = convertedValue;
}
}
return returnValue;
}
- 非字符串值 + 编辑器:先走
setValue让编辑器做"对象→对象"转换;若getValue返回了不同对象,说明编辑器真的做了事,随后把editor置 null——防止同一个编辑器被再setAsText一次造成重复转换。 - String[] → 逗号串:非数组目标收到 String[] 时,拼成 CSV 再交给编辑器
setAsText。 - 字符串值 + 编辑器:走
doConvertTextValue——先editor.setValue(oldValue)把旧值喂回去(很多编辑器实现用旧值做增量编辑的上下文),再setAsText(newTextValue),最后getValue()取结果。
至此,一次 convertIfNecessary 的完整判定顺序可以总结为:custom editor → ConversionService → 集合元素预处理 → 默认编辑器/JDK 约定编辑器 → 标准规则(数组/集合/Map/字符串构造器/枚举/数字)→ 最终校验(补试 ConversionService / 抛异常)。这就是 Spring 数据绑定的"总调度室",后文 12.8 会把它放回 populateBean 与 doBind 的调用栈里。
12.5 源码剖析(三):ConversionService 接口族
12.5.1 ConversionService:读侧契约
spring-core/src/main/java/org/springframework/core/convert/ConversionService.java 全文核心就四个方法(源码见 12.2.3,不再贴整段),这里逐条讲设计:
boolean canConvert(Class, Class):粗粒度探测。两个Class只能问"大概能不能转"。boolean canConvert(TypeDescriptor, TypeDescriptor):细粒度探测,带着字段上下文。注意 javadoc 的特别声明:集合/数组/Map 之间,即使canConvert返回 true,convert仍可能因元素不可转而抛ConversionException——也就是说canConvert对容器类只承诺"容器结构可转换",不承诺元素。<T> T convert(Object, Class):最常用的入口,源类型从值本身反推。default Object convert(Object, TypeDescriptor):6.1 新增的默认方法,源码一行:convert(source, TypeDescriptor.forObject(source), targetType)——只指定目标描述符,源描述符现场构造。Object convert(Object, TypeDescriptor, TypeDescriptor):三参终极版本。注意它的契约:sourceType == null时source必须也是 null(Assert.isTrue(source == null, ...));sourceType非 null 时,source必须是它的实例(第 175-178 行的IllegalArgumentException校验)。
12.5.2 ConverterRegistry:写侧契约
spring-core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java:
public interface ConverterRegistry {
void addConverter(Converter<?, ?> converter);
<S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter);
void addConverter(GenericConverter converter);
void addConverterFactory(ConverterFactory<?, ?> factory);
void removeConvertible(Class<?> sourceType, Class<?> targetType);
}
三个 addConverter 重载对应三种注册来源:
- 裸
Converter:类型对从它的泛型参数里反射解析(Converter<String, Long>→ String→Long)。解析失败抛IllegalArgumentException。 - 显式类型对 + Converter:允许同一个 Converter 实例挂到多个类型对上复用,不必为每对都造一个类。
GenericConverter:类型对由getConvertibleTypes()自报家门。
addConverterFactory 注册"区间工厂";removeConvertible(source, target) 删掉指定类型对的所有转换器。
ConfigurableConversionService(spring-core/.../convert/support/ConfigurableConversionService.java)只是 interface ConfigurableConversionService extends ConversionService, ConverterRegistry {}——把读侧与写侧合成一个接口,注释说明这是为了让"在 ConfigurableEnvironment 启动代码里临时增删转换器"方便。GenericConversionService 直接 implements ConfigurableConversionService。
12.5.3 Converter<S,T>:函数式 SPI
@FunctionalInterface
public interface Converter<S, T extends @Nullable Object> {
T convert(S source);
default <U> Converter<S, @Nullable U> andThen(Converter<? super T, ? extends @Nullable U> after) {
Assert.notNull(after, "'after' Converter must not be null");
return (S s) -> {
T initialResult = convert(s);
return (initialResult != null ? after.convert(initialResult) : null);
};
}
}
- 注解是
@FunctionalInterface,所以可以用 lambda 写转换器:Converter<String, Account> c = s -> accountRepo.find(s);。 - 泛型上界
T extends @Nullable Object——7.0.9 里用 JSpecify 的@Nullable标注了"目标可以是 null"(即转换器允许返回 null),而source契约是"never null"。 - 5.3 加的
andThen是经典组合子:StringToNumber.andThen(NumberToLong)链式转换。返回的 lambda 里对中间结果做了 null 短路——中间转成 null 就不再调 after。
12.5.4 ConverterFactory<S,R>:区间工厂
public interface ConverterFactory<S, R> {
<T extends R> Converter<S, ? extends @Nullable T> getConverter(Class<T> targetType);
}
一个工厂为"源类型 S → 基类 R 的任意子类型 T"一族转换负责。NumberToNumberConverterFactory(S=Number, R=Number)为每个具体数字子类派一个 NumberToNumber<T> 实例。与直接注册 N 个 Converter 相比,工厂省了 N 个类,也让"同方向一族"的策略可以集中演化。
12.5.5 GenericConverter 与 ConvertiblePair
public interface GenericConverter {
@Nullable Set<ConvertiblePair> getConvertibleTypes();
@Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
final class ConvertiblePair {
private final Class<?> sourceType;
private final Class<?> targetType;
// 构造器 Assert.notNull;equals/hashCode 用 == 比对 Class;toString: "S -> T"
}
}
要点:
getConvertibleTypes()返回 null 是合法的——但只允许条件转换器这么做(注册时Converters.add会断言converter instanceof ConditionalConverter),表示"我声明不了类型对,运行期 matches() 说了算"。convert能拿到 source/target 两侧的TypeDescriptor,因此可见泛型与字段注解——这是CollectionToCollectionConverter知道"目标集合元素是 Long"的依据。ConvertiblePair是值对象:equals用==比Class(Class 对象全局唯一,引用相等即可,比getName()快得多);hashCode = sourceType.hashCode()*31 + targetType.hashCode()。它是注册表 Map 的 key。
12.5.6 ConditionalConverter:运行期资格审查
public interface ConditionalConverter {
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}
public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}
matches 的典型用途 javadoc 里给了两个:① String→Date 时,目标字段带没带 @DateTimeFormat;② String→Account 时,目标类有没有 public static findAccount(String)。ConditionalGenericConverter 是纯接口组合,把"多类型对 + 上下文敏感"两个能力合在一个契约里——FormattingConversionService 的注解转换器、CollectionToCollectionConverter、ObjectToObjectConverter 都实现它。
12.5.7 TypeDescriptor:被忽略的一等公民
TypeDescriptor(spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java)是泛型擦除世界里的"类型全息图"。关键静态构造器:
public static @Nullable TypeDescriptor forObject(@Nullable Object source) { … } // 第 561 行
public static TypeDescriptor valueOf(@Nullable Class<?> type) { … } // 第 575 行
forObject(source):从值对象反推描述符(值非 null 时用value.getClass(),并带着ResolvableType.forType(value.getClass(), ...)做窄化——第 222 行ResolvableType.forType(value.getClass(), getResolvableType()),保留了声明类型里的泛型上下文)。- 实例方法:
getResolvableType()(第 161 行)、hasAnnotation(Class)(第 280 行)、isAssignableTo(TypeDescriptor)(第 320 行)、getElementTypeDescriptor()(第 371-377 行:数组取组件类型、Stream 取泛型 0、其余按集合泛型 0)、getMapKeyTypeDescriptor()/getMapValueTypeDescriptor()(第 418/453 行)。
为什么它重要?JDK 泛型擦除后,运行期拿到一个 ArrayList 实例,无从知道它声明时是 List<String> 还是 List<Long>。TypeDescriptor 把"字段/参数的声明类型"(通过 ResolvableType)与"值的实际类型"合并成一个描述对象,转换器才能问出"目标集合元素是 Long"。TypeConverterSupport 把 MethodParameter/Field 包成 new TypeDescriptor(methodParam) 传下来,正是为了这个。
12.5.8 异常体系
三个异常(同包 spring-core/.../convert/):
ConversionException(基类)。ConversionFailedException:转换过程启动了但失败了——构造时收 sourceType/targetType/source/cause 四元组。ConversionUtils.invokeConverter(见 12.6.5)把转换器抛的任意Throwable包装成它,已有的ConversionFailedException原样透传。ConverterNotFoundException:根本找不到能处理这个类型对的转换器。
这套异常在 TypeConverterSupport 里再翻译成 TypeMismatchException/ConversionNotSupportedException(见 12.4.1),形成"底层精确、上层语义化"的两层异常模型。
12.6 源码剖析(四):GenericConversionService 的注册表与查找算法
spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java 是整个 ConversionService 体系的内核。先把它的"三件套"字段摆出来(第 63-79 行):
public class GenericConversionService implements ConfigurableConversionService {
/**
* General NO-OP converter used when conversion is not required.
*/
private static final GenericConverter NO_OP_CONVERTER = new NoOpConverter("NO_OP");
/**
* Used as a cache entry when no converter is available.
* This converter is never returned.
*/
private static final GenericConverter NO_MATCH = new NoOpConverter("NO_MATCH");
private final Converters converters = new Converters();
private final Map<ConverterCacheKey, GenericConverter> converterCache = new ConcurrentReferenceHashMap<>(64);
两个哨兵 + 两个容器:
NO_OP_CONVERTER:"不需要转换"的占位——当源类型可直接赋值给目标类型时命中,convert里原样返回 source。NO_MATCH:"查过了,没有"的缓存哨兵——避免每次转换失败都重新跑一遍昂贵的类型层次搜索。它永远不会被返回,只作为缓存标记。converters:Converters内部类实例,存所有已注册转换器。converterCache:ConverterCacheKey(source, target) → GenericConverter的结果缓存,ConcurrentReferenceHashMap(软引用、带容量 64 初始),这就是它线程安全与低内存压力兼得的原因。
版本差异提醒:旧版 Spring(5.x-6.x 前期)这里还有一个 Map<Class<?>, Set<ConvertiblePair>> classCache(缓存"某个类涉及哪些类型对")与 cachedConverters。7.0.9 中这两个缓存已被移除,类型层次列表在缓存未命中时由 getClassHierarchy 现场计算。老资料里"converterCache + converters + classCache 三缓存"的说法在本版本不再成立,请以这里的两容器为准。
12.6.1 注册路径:addConverter 的三个入口
入口一:裸 Converter(第 85-95 行)
@Override
public void addConverter(Converter<?, ?> converter) {
ResolvableType[] typeInfo = getRequiredTypeInfo(converter.getClass(), Converter.class);
if (typeInfo == null && converter instanceof DecoratingProxy decoratingProxy) {
typeInfo = getRequiredTypeInfo(decoratingProxy.getDecoratedClass(), Converter.class);
}
if (typeInfo == null) {
throw new IllegalArgumentException("Unable to determine source type <S> and target type <T> for your " +
"Converter [" + converter.getClass().getName() + "]; does the class parameterize those types?");
}
addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1]));
}
getRequiredTypeInfo(第 260-272 行)用 ResolvableType.forClass(converterClass).as(Converter.class).getGenerics() 反射解析 <S, T>。两道补救:① 泛型解析不到但 converter 是 DecoratingProxy(AOP 代理对象)时,对被代理的原类再解析一次;② 还解析不到就抛异常——异常消息直接点破:"does the class parameterize those types?"。这就是为什么你的 Converter 必须写成 class MyConverter implements Converter<String, Long> 而不能裸写 Converter——类型对必须在 implements 处静态参数化。解析成功后包成 ConverterAdapter 走 GenericConverter 注册通道。
入口二:显式类型对(第 98-101 行)
public <S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter) {
addConverter(new ConverterAdapter(
converter, ResolvableType.forClass(sourceType), ResolvableType.forClass(targetType)));
}
跳过泛型解析,类型对由调用方直接给。
入口三:GenericConverter 与工厂(第 104-121 行)
public void addConverter(GenericConverter converter) {
this.converters.add(converter);
invalidateCache();
}
public void addConverterFactory(ConverterFactory<?, ?> factory) {
ResolvableType[] typeInfo = getRequiredTypeInfo(factory.getClass(), ConverterFactory.class);
……
addConverter(new ConverterFactoryAdapter(factory,
new ConvertiblePair(typeInfo[0].toClass(), typeInfo[1].toClass())));
}
共同点:注册动作完成后都调 invalidateCache()——converterCache.clear()(第 274-276 行)。注册是写操作、查转换是读操作,这里不做细粒度失效,而是整体清空缓存。由于容器内注册转换器通常发生在启动期(远早于批量转换),这个粗暴策略在实践上几乎零成本,但它也意味着:运行期动态注册转换器会触发全缓存失效,高吞吐线上慎在运行期注册。
12.6.2 Converters 与 ConvertersForPair:注册的存储结构
private static class Converters {
private final Set<GenericConverter> globalConverters = new CopyOnWriteArraySet<>();
private final Map<ConvertiblePair, ConvertersForPair> converters = new ConcurrentHashMap<>(256);
public void add(GenericConverter converter) {
Set<ConvertiblePair> convertibleTypes = converter.getConvertibleTypes();
if (convertibleTypes == null) {
Assert.state(converter instanceof ConditionalConverter,
"Only conditional converters may return null convertible types");
this.globalConverters.add(converter);
}
else {
for (ConvertiblePair convertiblePair : convertibleTypes) {
getMatchableConverters(convertiblePair).add(converter);
}
}
}
private ConvertersForPair getMatchableConverters(ConvertiblePair convertiblePair) {
return this.converters.computeIfAbsent(convertiblePair, key -> new ConvertersForPair());
}
}
注册时分流:
getConvertibleTypes()返回 null → 必须是ConditionalConverter,进globalConverters(CopyOnWriteArraySet——注册极少、读取频繁,COW 集合适配)。- 返回集合 → 每个
ConvertiblePair一个ConvertersForPair桶,computeIfAbsent建桶。
ConvertersForPair(第 612-642 行):
private static class ConvertersForPair {
private final Deque<GenericConverter> converters = new ConcurrentLinkedDeque<>();
public void add(GenericConverter converter) {
this.converters.addFirst(converter);
}
public @Nullable GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
// Look for proper match among all converters (taking full generics into account)
for (GenericConverter converter : this.converters) {
if (!(converter instanceof ConditionalGenericConverter genericConverter) ||
genericConverter.matches(sourceType, targetType)) {
return converter;
}
}
// Fallback to pre-6.2.3 behavior: accept Class match for unresolvable generics
for (GenericConverter converter : this.converters) {
if (converter instanceof ConverterAdapter converterAdapter &&
converterAdapter.matchesFallback(sourceType, targetType)) {
return converter;
}
}
return null;
}
}
两个要点:
addFirst+ 队列顺序:后注册的转换器排在队头,查找时先命中——后注册优先。这就是"用户自定义转换器可以盖过内置转换器"的实现基础:DefaultConversionService先注册内置,用户后注册的同类型对转换器自然插队头。- 两轮匹配:第一轮按"全泛型精确匹配"(
ConditionalGenericConverter.matches不过就跳过);第一轮全军覆没时,第二轮走matchesFallback——6.2.3 之前的旧行为:对泛型不可解析的场景放宽到 Class 匹配。这是为了兼容"原始类型/泛型擦除导致 matches 过严"的历史行为,注释写明了是 fallback。
12.6.3 getConverter:缓存优先、层次遍历、默认兜底
protected @Nullable GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);
GenericConverter converter = this.converterCache.get(key);
if (converter != null) {
return (converter != NO_MATCH ? converter : null);
}
converter = this.converters.find(sourceType, targetType);
if (converter == null) {
converter = getDefaultConverter(sourceType, targetType);
}
if (converter != null) {
this.converterCache.put(key, converter);
return converter;
}
this.converterCache.put(key, NO_MATCH);
return null;
}
流程三步:① 查 converterCache,命中直接用(NO_MATCH 翻译回 null);② 未命中走 converters.find 穷尽搜索;③ 注册表里没有就问 getDefaultConverter——它只做一件事:
protected @Nullable GenericConverter getDefaultConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
return (sourceType.isAssignableTo(targetType) ? NO_OP_CONVERTER : null);
}
源可赋值到目标 → NO_OP(无需转换);否则 null。无论找到与否都写回缓存——找到了存真转换器,没找到存 NO_MATCH 哨兵。这就是"同一类型对第二次转换零查找成本"与"失败类型对不会重复搜索"的机制。
12.6.4 Converters.find:类型层次的双重循环
public @Nullable GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {
// Search the full type hierarchy
List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType());
List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType());
for (Class<?> sourceCandidate : sourceCandidates) {
for (Class<?> targetCandidate : targetCandidates) {
ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate);
GenericConverter converter = getRegisteredConverter(sourceType, targetType, convertiblePair);
if (converter != null) {
return converter;
}
}
}
return null;
}
为什么需要层次遍历?用户注册的是 Converter<Number, String>,而值的实际类型是 Integer——注册表的 key 是 Number→String,拿 Integer→String 直接查查不到。于是算法把源类型与目标类型各自的完整层次(父类 + 接口序列)展开,做笛卡尔积逐一尝试。
getClassHierarchy(Class)(第 541-567 行)产出的顺序有讲究:先 resolvePrimitiveIfNecessary 把 int 规整成 Integer;然后 BFS 式地展开——自己排最前,父类紧随(跳过 Object 与 Enum),所有接口也并入;枚举类型最后补 Enum.class 与其接口;末尾再补 Object.class。越靠前越具体,所以双重循环天然实现了"最具体的转换器优先":Integer→String 比 Number→String 先被尝试,Number→String 又比 Object→String 先。
getRegisteredConverter(第 516-534 行)对每个候选类型对:
private @Nullable GenericConverter getRegisteredConverter(TypeDescriptor sourceType,
TypeDescriptor targetType, ConvertiblePair convertiblePair) {
// Check specifically registered converters
ConvertersForPair convertersForPair = this.converters.get(convertiblePair);
if (convertersForPair != null) {
GenericConverter converter = convertersForPair.getConverter(sourceType, targetType);
if (converter != null) {
return converter;
}
}
// Check ConditionalConverters for a dynamic match
for (GenericConverter globalConverter : this.globalConverters) {
if (((ConditionalConverter) globalConverter).matches(sourceType, targetType)) {
return globalConverter;
}
}
return null;
}
先查类型对桶(桶内还要过 matches 精确筛选),再遍历全局条件转换器做动态匹配。注意全局转换器的遍历发生在每一个候选类型对上——所以全局转换器数量要克制,它们是 O(层次积 × 全局数) 的开销。
12.6.5 convert 主流程与异常包装
public @Nullable Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
Assert.notNull(targetType, "Target type to convert to cannot be null");
if (sourceType == null) {
Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
return handleResult(null, targetType, convertNullSource(null, targetType));
}
if (source != null && !sourceType.getObjectType().isInstance(source)) {
throw new IllegalArgumentException("Source to convert from must be an instance of [" +
sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
}
GenericConverter converter = getConverter(sourceType, targetType);
if (converter != null) {
Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
return handleResult(sourceType, targetType, result);
}
return handleConverterNotFound(source, sourceType, targetType);
}
- sourceType 为 null ⇒ source 必须为 null ⇒ 走
convertNullSource(第 206-211 行:目标是Optional返回Optional.empty(),否则 null;子类可覆盖)。 - 类型契约校验:source 非 null 时必须是 sourceType 的实例。
- 找到转换器 →
ConversionUtils.invokeConverter:
public static @Nullable Object invokeConverter(GenericConverter converter, @Nullable Object source,
TypeDescriptor sourceType, TypeDescriptor targetType) {
try {
return converter.convert(source, sourceType, targetType);
}
catch (ConversionFailedException ex) {
throw ex;
}
catch (Throwable ex) {
throw new ConversionFailedException(sourceType, targetType, source, ex);
}
}
转换器内部抛的任意异常(含 IllegalArgumentException)都被包装成 ConversionFailedException——这正是 TypeConverterDelegate 第 2 段 catch 的那个异常类型。
handleResult:结果为 null 时assertNotPrimitiveTargetType——null 不能赋给基本类型,否则抛ConversionFailedException(cause 为 "A null value cannot be assigned to a primitive type")。这条规则解释了int属性收到空串时为什么是转换失败而不是静默 0。- 找不到转换器 →
handleConverterNotFound(第 278-290 行):source 为 null → 断言目标非基本类型后返回 null;source 实际可赋值到目标 → 直接返回 source(兜底兼容);否则抛ConverterNotFoundException。
12.7 源码剖析(五):适配器三兄弟与默认注册表
12.7.1 ConverterAdapter:把 Converter 伪装成 GenericConverter
裸 Converter 与 ConverterFactory 注册进来后,都被适配成 ConditionalGenericConverter。ConverterAdapter(GenericConversionService.java 第 311-364 行):
private final class ConverterAdapter implements ConditionalGenericConverter {
private final Converter<Object, Object> converter;
private final ConvertiblePair typeInfo;
private final ResolvableType targetType;
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(this.typeInfo);
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
// Check raw type first...
if (this.typeInfo.getTargetType() != targetType.getObjectType()) {
return false;
}
// Full check for complex generic type match required?
ResolvableType rt = targetType.getResolvableType();
if (!(rt.getType() instanceof Class) && !rt.isAssignableFromResolvedPart(this.targetType)) {
return false;
}
return !(this.converter instanceof ConditionalConverter conditionalConverter) ||
conditionalConverter.matches(sourceType, targetType);
}
public boolean matchesFallback(TypeDescriptor sourceType, TypeDescriptor targetType) {
return (this.typeInfo.getTargetType() == targetType.getObjectType() &&
this.targetType.hasUnresolvableGenerics() &&
(!(this.converter instanceof ConditionalConverter conditionalConverter) ||
conditionalConverter.matches(sourceType, targetType)));
}
@Override
public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return convertNullSource(sourceType, targetType);
}
return this.converter.convert(source);
}
}
matches 三段:
- 裸类型快筛:适配器记录的目标类型与请求目标描述符的 raw type 不等 → 直接 false。注意这里用
!=比 Class——因为find阶段已经按类型层次把 raw type 对齐过一次,能进到桶里的 raw type 必然相等,这一步实际是双保险。 - 泛型精确校验:请求目标不是裸 Class(即携带泛型,如
List<Long>)时,要求请求泛型与注册泛型isAssignableFromResolvedPart兼容。这是"后注册优先"之外的第二道闸门:同桶里Converter<String, List>与Converter<String, List<Long>>并存时,靠它选对。 - 用户条件:如果被适配的 Converter 自己还实现了
ConditionalConverter(例如StringToEnumConverterFactory家族),把matches委托给它。
matchesFallback 是 6.2.3 引入的回退:泛型不可解析(hasUnresolvableGenerics(),典型如反射得到的原始类型)时,放宽到 raw Class 相等即接受——配合 12.6.2 讲的两轮匹配。
12.7.2 ConverterFactoryAdapter:工厂的运行期派生
private final class ConverterFactoryAdapter implements ConditionalGenericConverter {
private final ConverterFactory<Object, Object> converterFactory;
private final ConvertiblePair typeInfo;
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
boolean matches = true;
if (this.converterFactory instanceof ConditionalConverter conditionalConverter) {
matches = conditionalConverter.matches(sourceType, targetType);
}
if (matches) {
Converter<?, ?> converter = this.converterFactory.getConverter(targetType.getType());
if (converter instanceof ConditionalConverter conditionalConverter) {
matches = conditionalConverter.matches(sourceType, targetType);
}
}
return matches;
}
@Override
public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return convertNullSource(sourceType, targetType);
}
return this.converterFactory.getConverter(targetType.getObjectType()).convert(source);
}
注意它的 matches 是两级的:工厂自己可以条件化;工厂现造出来的 Converter 也可以条件化——两层都过才算数。convert 里 getConverter(targetType.getObjectType()) 每次按实际目标类型现取派生转换器——工厂的"区间"语义就在这里兑现:注册表只存 String→Number 一个 ConvertiblePair,真转 String→Integer 时现造 StringToNumber(Integer)。
12.7.3 ConverterCacheKey 与 canConvert 族
private static final class ConverterCacheKey implements Comparable<ConverterCacheKey> {
// equals: sourceType && targetType 各自 equals;hashCode: source*29 + target
// compareTo: 两侧 ResolvableType.toString 字典序
}
key 的相等性完全委托给两个 TypeDescriptor.equals(后者比对 ResolvableType 与注解数组)。实现 Comparable 是为了潜在的有序容器使用(当前 Map 用不上,但无害)。
canConvert 两个重载(第 133-143 行)就是 getConverter 的布尔化:
public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
Assert.notNull(targetType, "Target type to convert to cannot be null");
return (sourceType == null || getConverter(sourceType, targetType) != null);
}
注意:sourceType 为 null(即 source 为 null 的探测)时直接返回 true——null 永远"可以转",真正的 null 兼容性校验(基本类型拒绝)在 convert 执行时才发生。这呼应了 ConversionService javadoc 里"canConvert 乐观、convert 才严格"的契约。另有个 canBypassConvert(第 156-159 行):判断是否可绕过转换(即命中 NO_OP),供性能敏感场景判断"这次转换是否零成本"。
12.7.4 DefaultConversionService:7.0.9 内置转换器全清单
spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java。构造器一行 addDefaultConverters(this);addDefaultConverters(第 89-105 行)分三段注册,以下清单逐行对照仓库源码统计:
标量段 addScalarConverters(第 139-182 行):
converterRegistry.addConverterFactory(new NumberToNumberConverterFactory());
converterRegistry.addConverterFactory(new StringToNumberConverterFactory());
converterRegistry.addConverter(Number.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToCharacterConverter());
converterRegistry.addConverter(Character.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new NumberToCharacterConverter());
converterRegistry.addConverterFactory(new CharacterToNumberFactory());
converterRegistry.addConverter(new StringToBooleanConverter());
converterRegistry.addConverter(Boolean.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverterFactory(new StringToEnumConverterFactory());
converterRegistry.addConverter(new EnumToStringConverter((ConversionService) converterRegistry));
converterRegistry.addConverterFactory(new IntegerToEnumConverterFactory());
converterRegistry.addConverter(new EnumToIntegerConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new StringToLocaleConverter());
converterRegistry.addConverter(Locale.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToCharsetConverter());
converterRegistry.addConverter(Charset.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToCurrencyConverter());
converterRegistry.addConverter(Currency.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToPropertiesConverter());
converterRegistry.addConverter(new PropertiesToStringConverter());
converterRegistry.addConverter(new StringToUUIDConverter());
converterRegistry.addConverter(UUID.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToPatternConverter());
converterRegistry.addConverter(Pattern.class, String.class, new ObjectToStringConverter());
if (KotlinDetector.isKotlinPresent()) {
converterRegistry.addConverter(new StringToRegexConverter());
converterRegistry.addConverter(kotlin.text.Regex.class, String.class, new ObjectToStringConverter());
}
逐个要点:
NumberToNumberConverterFactory(Number→Number 区间,实现见 12.7.5)。StringToNumberConverterFactory+ 反向Number→String(ObjectToStringConverter就是调toString())。- char 一族四个:
StringToCharacterConverter、NumberToCharacterConverter、CharacterToNumberFactory、Character→String。 StringToBooleanConverter(实现:true/on/yes/1→TRUE,false/off/no/0→FALSE,其余抛IllegalArgumentException;空串→null)。- 枚举一族四个:
StringToEnumConverterFactory(Enum.valueOf,空串→null)、EnumToStringConverter、IntegerToEnumConverterFactory、EnumToIntegerConverter——任务提纲里猜的 "EnumToIntegerConverter?" 在 7.0.9 确实存在并被注册(构造还收 ConversionService,因为枚举序号转换要回退到枚举名转换)。 - 随后成对注册
String→Locale/Charset/Currency/UUID/Pattern及其反向X→String。 StringToPropertiesConverter/PropertiesToStringConverter:Properties↔字符串行对。- Kotlin 在场时才追加
StringToRegexConverter与kotlin.text.Regex→String——条件注册的范例。
集合段 addCollectionConverters(第 114-137 行):
converterRegistry.addConverter(new ArrayToCollectionConverter(conversionService));
converterRegistry.addConverter(new CollectionToArrayConverter(conversionService));
converterRegistry.addConverter(new ArrayToArrayConverter(conversionService));
converterRegistry.addConverter(new CollectionToCollectionConverter(conversionService));
converterRegistry.addConverter(new MapToMapConverter(conversionService));
converterRegistry.addConverter(new ArrayToStringConverter(conversionService));
converterRegistry.addConverter(new StringToArrayConverter(conversionService));
converterRegistry.addConverter(new ArrayToObjectConverter(conversionService));
converterRegistry.addConverter(new ObjectToArrayConverter(conversionService));
converterRegistry.addConverter(new CollectionToStringConverter(conversionService));
converterRegistry.addConverter(new StringToCollectionConverter(conversionService));
converterRegistry.addConverter(new CollectionToObjectConverter(conversionService));
converterRegistry.addConverter(new ObjectToCollectionConverter(conversionService));
converterRegistry.addConverter(new StreamConverter(conversionService));
注意这些转换器构造器都收 ConversionService 自身——因为它们是"容器级转换器":自己只负责拆/装容器,元素转换递归回 ConversionService 处理。CollectionToCollectionConverter.convert 里那句 this.conversionService.convert(sourceElement, …)(第 89 行)就是递归点。StreamConverter 支持 Stream 与集合/数组互转。
尾部追加(第 93-104 行):
converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new DateToInstantConverter());
converterRegistry.addConverter(new InstantToDateConverter());
converterRegistry.addConverter(new StringToTimeZoneConverter());
converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());
converterRegistry.addConverter(new ObjectToObjectConverter());
converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new FallbackObjectToStringConverter());
converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new OptionalToObjectConverter((ConversionService) converterRegistry));
- 时间一系:
Date↔Instant双向、String→TimeZone、ZoneId→TimeZone、ZonedDateTime→Calendar。 ObjectToObjectConverter(反射桥,见 12.7.6)、IdToEntityConverter(按静态查找方法把 id 转实体)、FallbackObjectToStringConverter(兜底 toString)、Optional 双向两个。
静态工具方法 getSharedInstance()(第 69-81 行)是双重检查锁的全局共享实例:volatile 字段 + synchronized 双检。javadoc 泼了冷水:强烈建议自定义场景自己 new,这个共享实例只是给"够不到长生命周期 ConversionService 的代码路径"兜底用的——共享意味着不可定制。
12.7.5 两个数字转换器的实现要点
NumberToNumberConverterFactory(全文见 12.3 节阅读记录,此处讲设计):它 implements ConverterFactory<Number, Number>, ConditionalConverter,matches 一句 !sourceType.equals(targetType)——同类型不转(避免 Integer→Integer 白绕一圈)。getConverter 每次 new NumberToNumber<>(targetType),转换委托 NumberUtils.convertNumberToTargetClass。
StringToNumberConverterFactory:StringToNumber.convert 里 if (source.isEmpty()) return null; 然后 NumberUtils.parseNumber(source, targetType)——空串→null 是数字一族的统一约定。
12.7.6 ObjectToObjectConverter 与 FallbackObjectToStringConverter:反射桥与兜底
ObjectToObjectConverter(spring-core/.../convert/support/ObjectToObjectConverter.java)是 Object→Object 的条件转换器,转换算法 javadoc 列了四步(源码第 42-60 行注释):① 源对象上找 to{TargetSimpleName}() 实例方法;② 目标类上找静态 valueOf/of/from(sourceType);③ 目标类上单参 sourceType 构造器;④ 都没有抛异常。关键实现细节:
conversionExecutableCache(ConcurrentReferenceHashMap,32 初始)缓存"目标类 → 上次命中的 Executable";isApplicable校验缓存的可执行物是否适用于本次 sourceType。determineToMethod/determineFactoryMethod里两处if (String.class == targetClass) return null;——明确拒绝toString()与String.valueOf:对象→字符串不归它管,归FallbackObjectToStringConverter。- 未命中时抛的
IllegalStateException消息模板会展开成 "No toInteger() method exists on java.lang.Number, and no static valueOf/of/from(java.lang.Number) method or Integer(java.lang.Number) constructor exists on java.lang.Integer.",排查极友好。
FallbackObjectToStringConverter:Object→String 条件转换器,matches 规则——源不是 String 本身,且源是 CharSequence/StringWriter,或 ObjectToObjectConverter.hasConversionMethodOrConstructor(source, String.class)(即源类有 String 构造器或 valueOf(String)/of(String)/from(String) 静态工厂)。convert 就一句 source.toString()。它是"任何东西→字符串"的最后兜底,所以排在所有显式 X→String 之后生效。
12.8 源码剖析(六):格式化体系——Formatter 如何变成转换器
12.8.1 Printer / Parser / Formatter 三接口
spring-context/src/main/java/org/springframework/format/ 下三个接口:
public interface Printer<T> {
String print(T field, Locale locale);
}
public interface Parser<T> {
T parse(String text, Locale locale) throws ParseException;
}
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
注意签名里的 Locale 参数——格式化是与 Locale 绑定的,这是它与 Converter 的本质分野:Converter 问"能不能转",Formatter 问"在这个 Locale 下显示成什么样、怎么读回来"。Formatter 是空接口,只是同时继承两者。
12.8.2 FormatterRegistry:注册侧 API
spring-context/.../format/FormatterRegistry.java 继承 ConverterRegistry,即格式化注册表首先就是一个转换注册表,再追加六个方法:
void addPrinter(Printer<?> printer);
void addParser(Parser<?> parser);
void addFormatter(Formatter<?> formatter);
void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
javadoc 里有一条关键语义(addFormatterForFieldType):打印时若字段类型与 Formatter 的 T 不一致,会先强转到 T 再打印;解析后若结果不能直接赋给字段类型,也会先强转再返回。也就是说 Formatter 层允许"类型接力",而这层强转恰恰是 FormattingConversionService 自己实现的(见下)。
12.8.3 FormattingConversionService:两个缓存 + 四个桥接转换器
spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java 继承 GenericConversionService 并实现 FormatterRegistry, EmbeddedValueResolverAware。字段只有三个:
private @Nullable StringValueResolver embeddedValueResolver;
private final Map<AnnotationConverterKey, GenericConverter> cachedPrinters = new ConcurrentHashMap<>(64);
private final Map<AnnotationConverterKey, GenericConverter> cachedParsers = new ConcurrentHashMap<>(64);
注册入口全部翻译成 GenericConverter 注册:
public void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter) {
addConverter(new PrinterConverter(fieldType, formatter, this));
addConverter(new ParserConverter(fieldType, formatter, this));
}
public void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser) {
addConverter(new PrinterConverter(fieldType, printer, this));
addConverter(new ParserConverter(fieldType, parser, this));
}
一个 Formatter 拆成两个方向相反的 GenericConverter:PrinterConverter 注册类型对 fieldType → String,ParserConverter 注册 String → fieldType。注册进父类的 converters 表后,就完全融入 GenericConversionService 的查找体系。
PrinterConverter(第 142-185 行):
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (!sourceType.isAssignableTo(this.printerObjectType)) {
source = this.conversionService.convert(source, sourceType, this.printerObjectType);
}
if (source == null) {
return "";
}
return this.printer.print(source, LocaleContextHolder.getLocale());
}
这就是 12.8.2 javadoc 说的"先强转再打印":字段类型不直接是 Printer 的 T 时,先借 ConversionService 把对象转成 Printer 能吃的类型(例如 BigDecimal 字段先转 Number),再 print。空值打印成空串而不是 "null"。Locale 从 LocaleContextHolder.getLocale() 取——这就是 Spring MVC 里 LocaleResolver 影响格式化输出的接线点。
ParserConverter(第 188-234 行):
public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
String text = (String) source;
if (!StringUtils.hasText(text)) {
return null;
}
Object result;
try {
result = this.parser.parse(text, LocaleContextHolder.getLocale());
}
catch (IllegalArgumentException ex) {
throw ex;
}
catch (Throwable ex) {
throw new IllegalArgumentException("Parse attempt failed for value [" + text + "]", ex);
}
TypeDescriptor resultType = TypeDescriptor.valueOf(result.getClass());
if (!resultType.isAssignableTo(targetType)) {
result = this.conversionService.convert(result, resultType, targetType);
}
return result;
}
空串→null(与前面枚举/数字的空串约定一致);ParseException 等 Throwable 包装成 IllegalArgumentException("Parse attempt failed for value [x]")——这个异常后来会被 TypeConverterDelegate 一路翻成 TypeMismatchException 进 BindingResult;解析产物与目标字段类型不符时再借 ConversionService 强转一次(Parser 返回 Long、字段是 AtomicLong 这类场景)。
12.8.4 注解桥接:AnnotationPrinterConverter / AnnotationParserConverter
addFormatterForFieldAnnotation(第 100-111 行):
public void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory) {
Class<? extends Annotation> annotationType = getAnnotationType(annotationFormatterFactory);
if (this.embeddedValueResolver != null &&
annotationFormatterFactory instanceof EmbeddedValueResolverAware embeddedValueResolverAware) {
embeddedValueResolverAware.setEmbeddedValueResolver(this.embeddedValueResolver);
}
Set<Class<?>> fieldTypes = annotationFormatterFactory.getFieldTypes();
for (Class<?> fieldType : fieldTypes) {
addConverter(new AnnotationPrinterConverter(annotationType, annotationFormatterFactory, fieldType));
addConverter(new AnnotationParserConverter(annotationType, annotationFormatterFactory, fieldType));
}
}
三步走:① 反射解析工厂的泛型参数拿到注解类型 A;② 若工厂自己实现了 EmbeddedValueResolverAware(NumberFormatAnnotationFormatterFactory 就是),把容器的占位符解析器塞给它——这是 @NumberFormat(pattern = "${fmt.pattern}") 能生效的接线;③ 对工厂声明的每个字段类型,注册一对条件转换器。
AnnotationParserConverter(第 291-342 行)是"@DateTimeFormat 生效"的核心:
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return targetType.hasAnnotation(this.annotationType);
}
@Override
public @SuppressWarnings("unchecked")
public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
Annotation ann = targetType.getAnnotation(this.annotationType);
if (ann == null) {
throw new IllegalStateException(
"Expected [" + this.annotationType.getName() + "] to be present on " + targetType);
}
AnnotationConverterKey converterKey = new AnnotationConverterKey(ann, targetType.getObjectType());
GenericConverter converter = cachedParsers.get(converterKey);
if (converter == null) {
Parser<?> parser = this.annotationFormatterFactory.getParser(
converterKey.getAnnotation(), converterKey.getFieldType());
converter = new ParserConverter(this.fieldType, parser, FormattingConversionService.this);
cachedParsers.put(converterKey, converter);
}
return converter.convert(source, sourceType, targetType);
}
matches:目标字段描述符带不带这个注解——带,才轮到它解析。这就是ConditionalConverter教科书式的用法:同一个String→LocalDate类型对,普通路径走ObjectToObjectConverter/Temporal 解析,带@DateTimeFormat的字段走这条注解路径。convert:从 targetType 现场取出注解实例(getAnnotation),以"注解实例 + 字段类型"为 key 查cachedParsers——同一种注解配置只造一次 Parser,第二次起直接复用。未命中才调工厂的getParser(ann, fieldType)现场造Parser,包成ParserConverter缓存后执行。AnnotationPrinterConverter镜像对称:matches看的是sourceType.hasAnnotation(打印方向,注解在字段上即源描述符携带),结果缓存进cachedPrinters。AnnotationConverterKey(第 345-374 行):equals 比 fieldType(==)+ annotation(annotation.equals)——同一字段类型下同值注解视为同一个 key,@DateTimeFormat(pattern="yyyy")不同 pattern 的字段各自缓存一份 Parser。
12.8.5 getFieldType:泛型实参解析
static Class<?> getFieldType(T instance, Class<T> genericInterface) {
Class<?> fieldType = GenericTypeResolver.resolveTypeArgument(instance.getClass(), genericInterface);
if (fieldType == null && instance instanceof DecoratingProxy decoratingProxy) {
fieldType = GenericTypeResolver.resolveTypeArgument(decoratingProxy.getDecoratedClass(), genericInterface);
}
Assert.notNull(fieldType, () -> "Unable to extract the parameterized field type from " +
ClassUtils.getShortName(genericInterface) + " [" + instance.getClass().getName() +
"]; does the class parameterize the <T> generic type?");
return fieldType;
}
与 GenericConversionService.addConverter(Converter) 同款逻辑:Formatter 必须写成 class PhoneFormatter implements Formatter<Phone>,泛型实参解析不到(含 AOP 代理补救一次)就断言失败。这就是"自定义 Formatter 必须显式参数化"的来源。
12.9 源码剖析(七):DefaultFormattingConversionService 与 @NumberFormat/@DateTimeFormat
12.9.1 组装顺序
spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java 构造链(第 86-96 行):
public DefaultFormattingConversionService(
@Nullable StringValueResolver embeddedValueResolver, boolean registerDefaultFormatters) {
if (embeddedValueResolver != null) {
setEmbeddedValueResolver(embeddedValueResolver);
}
DefaultConversionService.addDefaultConverters(this);
if (registerDefaultFormatters) {
addDefaultFormatters(this);
}
}
注意顺序:先 addDefaultConverters(装全部内置转换器),再 addDefaultFormatters(装格式化器)。由于转换器注册是 addFirst(后注册排前),格式化器桥接出来的 Printer/Parser 在同类型对上天然优先于通用 Converter——例如 String→LocalDate,带注解的字段永远走 AnnotationParserConverter。
addDefaultFormatters(第 105-123 行):
public static void addDefaultFormatters(FormatterRegistry formatterRegistry) {
// Default handling of number values
formatterRegistry.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
// Default handling of monetary values
if (JSR_354_PRESENT) {
formatterRegistry.addFormatter(new CurrencyUnitFormatter());
formatterRegistry.addFormatter(new MonetaryAmountFormatter());
formatterRegistry.addFormatterForFieldAnnotation(new Jsr354NumberFormatAnnotationFormatterFactory());
}
// Default handling of date-time values
// just handling JSR-310 specific date and time types
new DateTimeFormatterRegistrar().registerFormatters(formatterRegistry);
// regular DateFormat-based Date, Calendar, Long converters
new DateFormatterRegistrar().registerFormatters(formatterRegistry);
}
JSR_354_PRESENT 是静态块里 ClassUtils.isPresent("javax.money.MonetaryAmount", classLoader) 的探测结果——类路径上有 JSR-354 API 才注册货币格式化器,这是 Spring 一贯的"可选依赖按存在性装配"策略。
12.9.2 @NumberFormat 与 NumberFormatAnnotationFormatterFactory
注解本体(spring-context/.../format/annotation/NumberFormat.java):@Target({METHOD, FIELD, PARAMETER, ANNOTATION_TYPE})、@Retention(RUNTIME),两个属性 style()(DEFAULT/NUMBER/PERCENT/CURRENCY)与 pattern(),javadoc 明确 pattern 优先于 style。
工厂实现(spring-context/.../format/number/NumberFormatAnnotationFormatterFactory.java):
public class NumberFormatAnnotationFormatterFactory extends EmbeddedValueResolutionSupport
implements AnnotationFormatterFactory<NumberFormat> {
@Override
public Set<Class<?>> getFieldTypes() {
return NumberUtils.STANDARD_NUMBER_TYPES;
}
@Override
public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation);
}
@Override
public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation);
}
private Formatter<Number> configureFormatterFrom(NumberFormat annotation) {
String pattern = resolveEmbeddedValue(annotation.pattern());
if (StringUtils.hasLength(pattern)) {
return new NumberStyleFormatter(pattern);
}
else {
Style style = annotation.style();
if (style == Style.CURRENCY) {
return new CurrencyStyleFormatter();
}
else if (style == Style.PERCENT) {
return new PercentStyleFormatter();
}
else {
return new NumberStyleFormatter();
}
}
}
}
getFieldTypes()直接复用NumberUtils.STANDARD_NUMBER_TYPES——Byte/Short/Integer/Long/Float/Double/BigDecimal/BigInteger 这组标准数字类型都可以挂@NumberFormat。- getPrinter/getParser 同一个
configureFormatterFrom——数字格式化是双向对称的。 resolveEmbeddedValue(annotation.pattern()):继承EmbeddedValueResolutionSupport,pattern 里的${...}占位符先解析再用于建格式化器——配合 12.8.4 讲的setEmbeddedValueResolver注入。- 三分支:有 pattern →
NumberStyleFormatter(pattern);style=CURRENCY →CurrencyStyleFormatter;PERCENT →PercentStyleFormatter;否则默认NumberStyleFormatter()。
AbstractNumberFormatter(number 包)内部包 java.text.NumberFormat,parse 走 NumberFormat.parse,print 走 format——Locale 敏感。
12.9.3 @DateTimeFormat 的双工厂分发
@DateTimeFormat 注解(spring-context/.../format/annotation/DateTimeFormat.java)属性:style()(默认 "SS")、iso()(NONE/DATE/TIME/DATE_TIME)、pattern()、fallbackPatterns()。javadoc 有一条对 JDK 20+ 的警告:style 模式依赖 locale-sensitive 模式串,JDK 20 起可能有不兼容变化,建议用 ISO 或自管 pattern,并给出 fallbackPatterns 作为兼容手段。
注解注册后由两个工厂分工处理(注意它们管的字段类型完全不重叠):
旧时代(spring-context/.../format/datetime/DateTimeFormatAnnotationFormatterFactory.java):
private static final Set<Class<?>> FIELD_TYPES = Set.of(Date.class, Calendar.class, Long.class);
三个字段类型:java.util.Date、Calendar、Long。getFormatter 建一个 DateFormatter,把注解的 iso/style/pattern/fallbackPatterns 逐个设进去(style 与 pattern 同样过 resolveEmbeddedValue)。
JSR-310 新时代(spring-context/.../format/datetime/standard/Jsr310DateTimeFormatAnnotationFormatterFactory.java):
private static final Set<Class<?>> FIELD_TYPES = Set.of(
Instant.class,
LocalDate.class,
LocalTime.class,
LocalDateTime.class,
ZonedDateTime.class,
OffsetDateTime.class,
OffsetTime.class,
YearMonth.class,
MonthDay.class);
九个 java.time 类型。getPrinter 有个性能细节:
DateTimeFormatter formatter = getFormatter(annotation, fieldType);
// Efficient ISO_LOCAL_* variants for printing since they are twice as fast...
if (formatter == DateTimeFormatter.ISO_DATE) {
if (isLocal(fieldType)) {
formatter = DateTimeFormatter.ISO_LOCAL_DATE;
}
}
……
return new TemporalAccessorPrinter(formatter);
本地时间类型打印时,把 ISO_DATE/TIME/DATE_TIME 换成不带时区处理的 ISO_LOCAL_* 变体——注释说快一倍。getParser 则把 fallbackPatterns 解析后连同 formatter 交给 TemporalAccessorParser(fieldType, formatter, fallbackPatterns, annotation):主 pattern 解析失败时按顺序试兜底 pattern。
12.9.4 DateTimeFormatterRegistrar:JSR-310 的默认注册员
spring-context/.../format/datetime/standard/DateTimeFormatterRegistrar.java 实现 FormatterRegistrar(FormatterRegistrar 接口只有一个方法 registerFormatters(FormatterRegistry),是"一组格式化器的自描述工厂"——DefaultFormattingConversionService 直接 new 出来调它)。registerFormatters(第 156-202 行)注册内容:
public void registerFormatters(FormatterRegistry registry) {
DateTimeConverters.registerConverters(registry);
DateTimeFormatter df = getFormatter(Type.DATE);
DateTimeFormatter tf = getFormatter(Type.TIME);
DateTimeFormatter dtf = getFormatter(Type.DATE_TIME);
registry.addFormatterForFieldType(LocalDate.class,
new TemporalAccessorPrinter(
df == DateTimeFormatter.ISO_DATE ? DateTimeFormatter.ISO_LOCAL_DATE : df),
new TemporalAccessorParser(LocalDate.class, df));
// LocalTime / LocalDateTime / ZonedDateTime / OffsetDateTime / OffsetTime 同款……
registry.addFormatterForFieldType(Instant.class, new InstantFormatter());
registry.addFormatterForFieldType(Period.class, new PeriodFormatter());
registry.addFormatterForFieldType(Duration.class, new DurationFormatter());
registry.addFormatterForFieldType(Year.class, new YearFormatter());
registry.addFormatterForFieldType(Month.class, new MonthFormatter());
registry.addFormatterForFieldType(YearMonth.class, new YearMonthFormatter());
registry.addFormatterForFieldType(MonthDay.class, new MonthDayFormatter());
registry.addFormatterForFieldAnnotation(new Jsr310DateTimeFormatAnnotationFormatterFactory());
registry.addFormatterForFieldAnnotation(new DurationFormatAnnotationFormatterFactory());
}
要点:
DateTimeConverters.registerConverters(registry)先补一批 JSR-310 类型之间的双向转换器(如LocalDate→Date之类桥接)。- 默认格式化风格由
getFallbackFormatter决定:DATE/TIME/DATE_TIME 全部FormatStyle.SHORT——即默认用当前 Locale 的短格式解析LocalDate等。用户可以通过 setter(setDateStyle/setUseIsoFormat/setDateFormatter等)覆盖,这就是"全局统一日期格式"的配置入口。 - 末尾注册两个注解工厂:
@DateTimeFormat(JSR-310 九个类型)与@DurationFormat(Duration/Period)。
至此格式化层全貌闭合:DefaultFormattingConversionService = DefaultConversionService 的全部转换器 + 数字注解工厂 + 货币格式化器(可选)+ JSR-310 默认格式化器 + DateFormatterRegistrar(老 Date/Calendar 的 SHORT 风格默认格式化)。
12.10 源码剖析(八):容器中的集成点
12.10.1 容器如何拿到 ConversionService:finishBeanFactoryInitialization
spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java 第 953-958 行:
// Initialize conversion service for this context.
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
beanFactory.setConversionService(
beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
}
这是容器级装配的唯一入口,发生在 finishBeanFactoryInitialization 最早期(所有单例 bean 创建之前)。判定条件两条:① 容器里存在名为 conversionService(CONVERSION_SERVICE_BEAN_NAME)的 bean;② 它的类型确实是 ConversionService。满足就把它取出来塞进 BeanFactory。命名是契约:你自定义的 ConversionService 必须起名叫 conversionService——随便起名的 ConversionService bean 容器看不到,这是初学者最常踩的坑。
注意这里没有什么 ConversionServiceBeanFactoryPostProcessor——任务提纲里的这个猜测名在 7.0.9 不存在,机制就是这段直截了当的 bean 名探测。
12.10.2 AbstractBeanFactory:BeanFactory 持有的 ConversionService
spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java:
/** Spring ConversionService to use instead of PropertyEditors. */
private @Nullable ConversionService conversionService; // 第 134 行
public void setConversionService(@Nullable ConversionService conversionService) { // 第 867 行
this.conversionService = conversionService;
}
它有两个消费点。其一,getTypeConverter()(第 927-939 行):
public TypeConverter getTypeConverter() {
TypeConverter customConverter = getCustomTypeConverter();
if (customConverter != null) {
return customConverter;
}
else {
// Build default TypeConverter, registering custom editors.
SimpleTypeConverter typeConverter = new SimpleTypeConverter();
typeConverter.setConversionService(getConversionService());
registerCustomEditors(typeConverter);
return typeConverter;
}
}
BeanFactory 需要临时转个值(比如解析 @Value、查 isTypeMatch)时,就 new 一个 SimpleTypeConverter,把容器的 ConversionService 塞进去。其二,prepareBeanWrapper 路径(第 1297 行 bw.setConversionService(getConversionService()))——给正在装配的 bean 的 BeanWrapper 注入同一个 ConversionService。于是链条闭合:容器级 ConversionService → 每个 BeanWrapper 的 TypeConverterDelegate → 每次 convertIfNecessary 的第 2 段。
12.10.3 BeanWrapperImpl:属性填充时的转换
BeanWrapperImpl(spring-beans/.../BeanWrapperImpl.java)构造时 this.typeConverterDelegate = new TypeConverterDelegate(this, this.wrappedObject)(第 137 行)——把自己(注册表)和目标 bean 一起交给 delegate。真正赋值走父类 AbstractNestablePropertyAccessor,其中第 298/324/357/365 行都调 convertIfNecessary(...),最终收拢到第 580-586 行:
private @Nullable Object convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue,
Object newValue, @Nullable Class<?> requiredType, @Nullable TypeDescriptor td) {
……
return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td);
}
即 12.4 讲的那个七段方法。AbstractAutowireCapableBeanFactory.applyPropertyValues(spring-beans/.../support/AbstractAutowireCapableBeanFactory.java 第 1660 行起)做的事就是:遍历 PropertyValues,通过 bw.setPropertyValues(mpvs) 把每个属性值交给 BeanWrapper,而 BeanWrapper 在 setPropertyValue 内部对每个属性调上面这个转换。属性值里的 SpEL、${} 占位符在更早的阶段(BeanDefinitionValueResolver/embeddedValueResolvers)已经解析成具体对象,这里拿到的基本就是"字符串或已解析对象"——转换层不再管占位符,只管类型。
12.10.4 ConversionServiceFactoryBean:XML 时代的配置入口
spring-context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java:
public class ConversionServiceFactoryBean implements FactoryBean<ConversionService>, InitializingBean {
private @Nullable Set<?> converters;
private @Nullable GenericConversionService conversionService;
public void setConverters(Set<?> converters) {
this.converters = converters;
}
@Override
public void afterPropertiesSet() {
this.conversionService = createConversionService();
ConversionServiceFactory.registerConverters(this.converters, this.conversionService);
}
protected GenericConversionService createConversionService() {
return new DefaultConversionService();
}
}
- 默认产物是
DefaultConversionService(全内置转换器),子类可覆盖createConversionService换成DefaultFormattingConversionService。 setConverters收一个集合,元素可以是Converter、ConverterFactory、GenericConverter三者之一——ConversionServiceFactory.registerConverters(spring-core/.../support/ConversionServiceFactory.java)按 instanceof 分发注册:
public static void registerConverters(@Nullable Set<?> converters, ConverterRegistry registry) {
if (converters != null) {
for (Object candidate : converters) {
if (candidate instanceof GenericConverter genericConverter) {
registry.addConverter(genericConverter);
}
else if (candidate instanceof Converter<?, ?> converter) {
registry.addConverter(converter);
}
else if (candidate instanceof ConverterFactory<?, ?> converterFactory) {
registry.addConverterFactory(converterFactory);
}
else {
throw new IllegalArgumentException("Each converter object must implement one of the " +
"Converter, ConverterFactory, or GenericConverter interfaces");
}
}
}
}
注意它注册出来的 bean 默认 singleton、bean 名若为 conversionService,就会被 12.10.1 的探测接住。@Configuration 风格下不需要它:直接 @Bean public ConversionService conversionService() { … } 返回一个配好的 DefaultFormattingConversionService 即可——javadoc 明说了这一点。也请注意:框架层并不会自动收集容器里所有 Converter 类型的 bean;"@Component 实现 Converter 就自动注册"是 Spring Boot 的 ConversionService 后置收集逻辑,不是本框架的行为。纯框架里自定义转换器要么显式 addConverter,要么通过 ConversionServiceFactoryBean.setConverters 列举。
12.10.5 DataBinder:Web 数据绑定的转换整合
spring-context/src/main/java/org/springframework/validation/DataBinder.java 第 130 行 public class DataBinder implements PropertyEditorRegistry, TypeConverter——它自己就实现了转换两接口,内部委托给一个 ExtendedTypeConverter(静态内部类,第 1468-1469 行:extends SimpleTypeConverter implements PropertyEditorRegistrar)。懒创建(第 381-388 行):
protected SimpleTypeConverter getSimpleTypeConverter() {
if (this.typeConverter == null) {
this.typeConverter = new ExtendedTypeConverter();
this.typeConverter.setConversionService(this.conversionService);
}
return this.typeConverter;
}
绑定主链:
public void bind(PropertyValues pvs) { // 第 1213 行
if (shouldNotBindPropertyValues()) {
return;
}
MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues mutablePropertyValues ?
mutablePropertyValues : new MutablePropertyValues(pvs));
doBind(mpvs);
}
protected void doBind(MutablePropertyValues mpvs) { // 第 1241 行
checkAllowedFields(mpvs);
checkRequiredFields(mpvs);
applyPropertyValues(mpvs);
}
protected void applyPropertyValues(MutablePropertyValues mpvs) { // 第 1355 行
try {
// Bind request parameters onto target object.
getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
}
catch (PropertyBatchUpdateException ex) {
// Use bind error processor to create FieldErrors.
for (PropertyAccessException pae : ex.getPropertyAccessExceptions()) {
getBindingErrorProcessor().processPropertyAccessException(pae, getInternalBindingResult());
}
}
……
}
链条:请求参数 → PropertyValues → checkAllowedFields(剥掉不在白名单的字段,防路径穿越与越权绑定)→ checkRequiredFields(缺失必填字段记 FieldError)→ getPropertyAccessor().setPropertyValues——这个 accessor 就是包着目标对象的 BeanWrapper,于是每个属性值再次走进 TypeConverterDelegate.convertIfNecessary。转换抛出来的 TypeMismatchException 被 PropertyBatchUpdateException 聚合,再由 BindingErrorProcessor 逐个翻译成 BindingResult 里的 FieldError——类型转换失败在 Web 层不是异常抛出,而是错误收集,这就是表单回显"输入 4.5 到 age 字段时页面提示 typeMismatch"的完整链路。
setConversionService(第 749-750 行)有一句守卫:Assert.state(this.conversionService == null, "DataBinder is already initialized with ConversionService")——DataBinder 一旦完成初始化就不许换 ConversionService,避免绑定制程中途策略突变。WebDataBinder(spring-web 包)在此之上做 Web 相关扩展,细节归 Web 章,本章只需要知道:MVC 的参数绑定最终也落在同一个 DataBinder → BeanWrapperImpl → TypeConverterDelegate 链上。
12.11 运行机制:一次"字符串属性值 → bean 属性对象"的完整旅程
把前面所有源码串成一条时序。以 XML 里 <property name="age" value="42"/> 装配 User.age: int 为例:
容器启动
AbstractApplicationContext.refresh()
└─ finishBeanFactoryInitialization(beanFactory)
├─ 探测名为 conversionService 的 bean(类型匹配 ConversionService)
│ └─ beanFactory.setConversionService(cs) ← 12.10.1
└─(随后开始创建所有非懒加载单例)
创建 user bean
AbstractAutowireCapableBeanFactory.doCreateBean
└─ populateBean(beanName, mbd, bw)
├─(bw = BeanWrapperImpl,构造时 new TypeConverterDelegate(this, user))
├─ prepareBeanWrapper: bw.setConversionService(beanFactory.getConversionService()) ← 12.10.2
└─ applyPropertyValues("age"="42", …)
└─ bw.setPropertyValues(mpvs)
└─ AbstractNestablePropertyAccessor.setPropertyValue → setPropertyValue(BeanWrapperImpl#)
└─ convertIfNecessary("age", null, "42", int.class, td) ← AbstractNestablePropertyAccessor:580
└─ TypeConverterDelegate.convertIfNecessary(...) ← 12.4
① findCustomEditor(int, "age") → null
② cs != null → canConvert(String, int) → true
│ └─ GenericConversionService.convert:
│ getConverter(String,int):
│ converterCache 未命中 → converters.find
│ (层次遍历:String→Integer? …→String→Number?)
│ 命中 StringToNumberConverterFactory 派生的 StringToNumber(Integer)
│ (首次)写入 converterCache
│ invokeConverter → NumberUtils.parseNumber("42", Integer) → 42
③ return 42
└─ bw.setProperty("age", 42)(内省写方法,反射 setAge(42))
关键时序结论:
- 占位符先于类型转换。
"42"若写成"${user.age}",解析发生在更早:AbstractBeanFactory.embeddedValueResolvers(环境PropertySources)在属性值进入 BeanWrapper 之前就把它解成字符串42。顺序永远是:占位符/SpEL 求值 → 类型转换。所以@Value("${timeout:1000}")注入Duration时,先拿到字符串"1000",再由 TypeConverterDelegate 走 ConversionService 转成 Duration。 - 第一次转换慢、后续快。
converterCache在第一次canConvert/convert同类型对时填充;之后String→int这类热路径只是一次ConcurrentReferenceHashMap.get。 - Editor 与 ConversionService 是双通道互备,不是互斥。
conversionAttemptEx的捕获与重放(12.4.3 第 2/6/7 段)保证了:ConversionService 失败时 PropertyEditor 还能救场,两边都失败才算失败。 - Web 层同一链路。请求参数字符串 →
DataBinder.bind→doBind→applyPropertyValues→ 同一个 BeanWrapper → 同一个convertIfNecessary;区别仅在"异常不抛出而进 BindingResult",以及 DataBinder 自己挂的 ConversionService 是WebDataBinder初始化时从容器拿到的那一个。
12.12 扩展用法与最佳实践
12.12.1 三种自定义转换器写法与选型
写法一:Converter<S,T>(首选,90% 场景)
public class StringToOrderConverter implements Converter<String, Order> {
private final OrderRepository repo;
public StringToOrderConverter(OrderRepository repo) { this.repo = repo; }
@Override public Order convert(String source) { return repo.findById(source).orElseThrow(); }
}
单类型对、无字段上下文需求时用它。注意泛型实参必须写在 implements 处——注册时靠反射解析。
写法二:ConverterFactory<S,R>(一族转换)
当你要为同一个源类型适配一组目标子类型时(如 String→Duration、String→Period、String→Instant 共享一套解析逻辑),实现 ConverterFactory<String, Temporal>,在 getConverter(targetType) 里按目标类型分派。
写法三:ConditionalGenericConverter(需要看字段上下文)
public class MyConditionalConverter implements ConditionalGenericConverter {
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(String.class, BigDecimal.class));
}
public boolean matches(TypeDescriptor source, TypeDescriptor target) {
return target.hasAnnotation(MyMoney.class); // 只接带注解的字段
}
public Object convert(Object source, TypeDescriptor s, TypeDescriptor t) { … }
}
凡是"同一类型对、不同字段要不同行为"的(注解驱动、泛型元素类型驱动),都走这条。FormattingConversionService 的注解转换器就是范例。
选型口诀:Converter 不够用 → ConverterFactory;需要字段上下文 → ConditionalGenericConverter;需要 Locale/文本格式 → 直接写 Formatter<T> 并注册成 FormatterRegistry(打印/解析双向白得)。
12.12.2 注册方式(框架层,非 Boot)
纯框架下,转换器不会因为实现了接口就自动进 ConversionService。三条正规路径:
@Bean方法显式组装:
@Bean
public ConversionService conversionService(OrderRepository repo) {
DefaultFormattingConversionService cs = new DefaultFormattingConversionService();
cs.addConverter(new StringToOrderConverter(repo));
cs.addFormatterForFieldType(Phone.class, new PhoneFormatter());
return cs;
}
bean 名必须是 conversionService(12.10.1 的契约)。返回 DefaultFormattingConversionService 可同时获得内置转换器 + 格式化支持;只要 DefaultConversionService 就没有 @NumberFormat 能力。
2. XML:<bean id="conversionService" class="...ConversionServiceFactoryBean"><property name="converters"><set>…</set></property></bean>。
3. 编程式临时使用:DefaultConversionService.getSharedInstance()(兜底场景,javadoc 明确不推荐定制)。
12.12.3 性能与线程安全
GenericConversionService的注册结构(ConcurrentHashMap/ConcurrentLinkedDeque/ConcurrentReferenceHashMap/CopyOnWriteArraySet)与缓存都为并发读设计——注册完成后,convert 调用是线程安全的。转换器实现本身也被接口契约要求无状态、可共享。- 热路径成本:命中
converterCache≈ 一次哈希查找;未命中要做源/目标类型层次笛卡尔积遍历(find)。层次平均十几层,未命中开销不可小觑——所以缓存设计成"失败也缓存(NO_MATCH)"。 - 运行期不要注册转换器:
addConverter会converterCache.clear(),高并发下全缓存重建有毛刺。启动期注册完就别动。 ObjectToObjectConverter的conversionExecutableCache、FormattingConversionService的cachedPrinters/cachedParsers都是"按目标类/注解配置"的缓存——转换器/格式化器一旦注册,运行期反射探查成本只付一次。- PropertyEditor 一侧相反:
BeanWrapperImpl每 bean 一个,不可跨线程共享——这是体系代差,不是设计失误。
12.12.4 Formatter 与 Converter 的选择
需要"值 ↔ 字符串"且与 Locale/注解格式相关(货币、百分比、日期时间、带格式的数字显示)→ Formatter;需要"任意两种对象类型"、无 Locale 语义(String→实体、Number→Number、List→List)→ Converter。混用没问题:DefaultFormattingConversionService 本来就是两者叠加。
12.13 常见问题与坑
12.13.1 Number 转换的溢出与精度
NumberToNumberConverterFactory 与 TypeConverterDelegate 第 4 段的数字规则都最终走 NumberUtils.convertNumberToTargetClass。源码行为:Long.MAX_VALUE 转 Integer 时截断抛 IllegalArgumentException("Overflow"),而 Double 4.5 转 Integer 是四舍五入还是取整?——以 NumberUtils 实现为准(doConvertNumberToTargetClass 用 xxxValue() 强转,即取整截断,小数部分丢弃不报错)。坑的本质:宽转窄、浮点转整型时的精度损失是静默的。建议:对金额/计数场景,字段直接声明 BigDecimal,让 NumberStyleFormatter/CustomNumberEditor 按字符串解析保住精度,不要走 Double 中转。
12.13.2 String → Enum 的名称匹配
StringToEnumConverterFactory.StringToEnum.convert(12.7.4 已贴):
public @Nullable T convert(String source) {
if (source.isEmpty()) {
return null;
}
return (T) Enum.valueOf(this.enumType, source.trim());
}
三个行为要点:① 严格按枚举常量名匹配(Enum.valueOf),大小写敏感——"red" 匹配不到 RED;② 匹配前 trim(),首尾空格容忍;③ 空串直接 null,不抛异常。而在 ConversionService 之前的 TypeConverterDelegate 第 4 段还有一条 PropertyEditor 时代的枚举路径:空串→null、非空串→attemptToConvertStringToEnum(先全限定名后 getField)——两条路径对空串语义一致(null),但对非法值:Converter 路径抛 IllegalArgumentException(被包成 ConversionFailedException),Editor 路径 trace 日志后保持原值再由最终校验抛错。另外注意 ConversionUtils.getEnumType:targetType 是枚举子类($1 匿名子类)时向上找最近的 enum 父类——这就是 enum Color { RED } 写成 Color.RED 后反射类型是匿名子类仍能正确转换的原因。
12.13.3 泛型擦除与 TypeDescriptor 的必要性
最经典的现场:
List<Integer> ids = bean.getIds(); // 字段
// 注入:@Value("1,2,3") List<Long> ids
没有泛型信息时,运行期拿到的只是 List,转换器不知道元素该转成 Long。Spring 靠 TypeConverterSupport.convertIfNecessary(value, Long.class, methodParam) 把 MethodParameter 包成 TypeDescriptor,getElementTypeDescriptor() 解出 Long,StringToCollectionConverter/CollectionToCollectionConverter 再逐元素递归转换。坑:直接调 typeConverter.convertIfNecessary(value, List.class)(只传 Class)会导致元素不被转换——List 里还是 String。这是编程式使用 TypeConverter 时最隐蔽的 bug:必须传 methodParam/field/typeDescriptor 重载。
12.13.4 ObjectToObjectConverter 的反射桥副作用
它会静默地按约定找 toXxx()/静态 valueOf/of/from/单参构造器(12.7.6)。副作用场景:你的类恰好看似有 public Foo(String) 构造器——任何 String→Foo 都会被它接住并反射调用,包括你不希望自动发生的转换。例如构造器有副作用(建连、查表),属性绑定时就会触发。排查方法:异常消息里那句 "No to… method exists…" 或打开 DEBUG 日志看 GenericConversionService.toString() 列出的全部已注册转换器。另外它明确拒绝 String 方向(两处 String.class == targetClass → null),对象→字符串由 FallbackObjectToStringConverter 负责——别指望它给你 toString。
12.13.5 null 的转换语义汇总
把散在各章的 null 规则收拢成一张表:
| 场景 | 行为 | 源码位置 |
|---|---|---|
| source 为 null,目标 Optional | 返回 Optional.empty() | GenericConversionService.convertNullSource / TypeConverterDelegate 第 5 段 |
| source 为 null,目标其他类型 | 返回 null | 同上 |
| source 为 null,目标基本类型 | 抛 ConversionFailedException("null cannot be assigned to a primitive") | assertNotPrimitiveTargetType |
| 空串 → 数字/布尔/字符/枚举 | 返回 null(不抛错) | StringToNumberConverterFactory、StringToBooleanConverter、StringToEnumConverterFactory |
| 空串 → 注解格式化 Parser | 返回 null | ParserConverter(!StringUtils.hasText) |
| sourceType 为 null 的 canConvert | 永远 true | GenericConversionService.canConvert |
推论:int 字段收到空串,在 Web 绑定里最终是 FieldError 而不是 0;Integer 字段收到空串则静默变 null。
12.14 本章小结
Spring 的类型转换体系是三代技术叠出来的"夹心层":
- 执行引擎在 spring-beans:
TypeConverterDelegate.convertIfNecessary是唯一的总调度。它的优先级链——custom editor → ConversionService → 默认/JDK 约定编辑器 → 标准规则(数组/集合/Map/字符串构造器/枚举/数字)→ 补试与异常收尾——保证了"两条通道互备、后注册优先、失败信息友好"。conversionAttemptEx的捕获与重放是这套互备语义的精髓。 - 服务内核在 spring-core:
GenericConversionService用ConvertiblePair→ConvertersForPair注册表 +ConverterCacheKey→GenericConverter结果缓存(含 NO_OP/NO_MATCH 哨兵)+ 类型层次笛卡尔积查找,把"注册时多态、运行期精确、热路径零成本"三个目标同时达成。7.0.9 已无 classCache,层次列表在缓存未命中时现场计算——这是与旧资料的重要差异。 - 格式化在 spring-context:
FormattingConversionService把Printer/Parser/AnnotationFormatterFactory全部桥接成 GenericConverter 塞进同一个注册表,@NumberFormat/@DateTimeFormat靠ConditionalGenericConverter.matches在带注解字段上精确接管,且按"注解配置"缓存 Parser 实例。 - 容器集成靠命名契约:名为
conversionService的 bean 在finishBeanFactoryInitialization早期被探测并注入 BeanFactory,再由prepareBeanWrapper与getTypeConverter()分发给每个 BeanWrapper 与临时转换器;DataBinder 在 Web 侧复用同一条链,把转换异常翻译成 BindingResult 的 FieldError。
扩展时记住三件事:转换器必须静态参数化泛型(否则注册即报错);框架层不自动收集 Converter bean,要显式注册且 bean 名为 conversionService;null/空串在数字、枚举、注解 Parser 三处统一约定为"静默转 null",基本类型除外。