Spring 自定义属性解析器源码解析:PropertyEditor 注册与注入全链路(source-code-hunter)
【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter
本文以 source-code-hunter 仓库中的 Spring-Custom-attribute-resolver.md 为核心,结合仓库内其他 IoC 源码笔记,从「自定义属性编辑器(PropertyEditor)」的使用案例出发,深入剖析 Spring 容器如何完成自定义属性编辑器的注册、配置与最终的类型转换,帮助读者真正理解 Spring IoC 中字符串属性值到目标类型对象之间的转换机制。
一、为什么需要自定义属性解析器
在 Spring 的 XML 配置中,<property name="xxx" value="...">注入的永远是一个字符串。当目标属性是java.util.Date、自定义实体对象(如Address)等非字符串类型时,Spring 必须借助属性编辑器(PropertyEditor)将字符串转换为目标类型。
Java 原生提供了java.beans.PropertyEditor接口,Spring 在其基础上扩展出PropertyEditorRegistrar、PropertyEditorRegistry等组件,并提供了CustomEditorConfigurer这个BeanFactoryPostProcessor作为统一的注册入口。整个链路可以概括为:
- 实现
PropertyEditorSupport子类,定义字符串 → 目标类型的转换规则; - 通过
PropertyEditorRegistrar将编辑器注册到PropertyEditorRegistry; - 在 XML 中配置
CustomEditorConfigurer,把注册器或customEditors映射注入进去; - Bean 实例化填充属性(
populateBean→applyPropertyValues)时,Spring 找到对应编辑器完成转换。
仓库中 BeanFactoryPostProcessor.md 也给出了同一机制的另一个完整可运行示例(String → Address 类型转换),可相互印证。
二、完整用例:把字符串 "2020-01-01 01:01:01" 注入到 Date 属性
2.1 XML 配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean class="org.springframework.beans.factory.config.CustomEditorConfigurer"> <property name="propertyEditorRegistrars"> <list> <bean class="com.huifer.source.spring.bean.DatePropertyRegister"/> </list> </property> <property name="customEditors"> <map> <entry key="java.util.Date" value="com.huifer.source.spring.bean.DatePropertyEditor"> </entry> </map> </property> </bean> <bean id="apple" class="com.huifer.source.spring.bean.Apple"> <property name="date" value="2020-01-01 01:01:01"/> </bean> </beans>配置要点:
propertyEditorRegistrars:注入一个PropertyEditorRegistrar实现列表,由注册器负责把编辑器绑定到具体类型;customEditors:以 Map 形式直接声明「目标类型 → 编辑器类名」的映射,key 为目标类型全限定名,value 为编辑器类全限定名;- 目标 bean
apple的date属性接收字符串"2020-01-01 01:01:01",需要被转换为Date才能注入成功。
2.2 方式一:实现 PropertyEditorRegistrar 注册器
public class DatePropertyRegister implements PropertyEditorRegistrar { @Override public void registerCustomEditors(PropertyEditorRegistry registry) { registry.registerCustomEditor(Date.class, new CustomDateEditor( new SimpleDateFormat("yyyy-MM-dd"), true) ); } }CustomDateEditor是 Spring 自带的标准日期编辑器,true表示允许空字符串。这里通过registry.registerCustomEditor(Date.class, ...)把Date类型绑定到自定义的日期编辑器上。
2.3 方式二:继承 PropertyEditorSupport 自定义转换规则
public class DatePropertyEditor extends PropertyEditorSupport { private String format = "yyyy-MM-dd"; public String getFormat() { return format; } public void setFormat(String format) { this.format = format; } @Override public void setAsText(String text) throws IllegalArgumentException { System.out.println(text); SimpleDateFormat sdf = new SimpleDateFormat(format); try { Date date = sdf.parse(text); this.setValue(date); } catch (Exception e) { e.printStackTrace(); } } }要点:
PropertyEditorSupport是java.beans.PropertyEditor的默认适配实现;- 核心方法
setAsText(String)接收配置中的字符串,解析成目标对象后调用setValue(...)暂存; - Spring 最终通过
editor.getValue()取回转换结果。
三、PropertyEditorRegistrar 注册流程解析
在DatePropertyRegister.registerCustomEditors方法上打断点,可以看到完整的调用堆栈(图片 1),其调用层次揭示了注册发生的阶段:容器创建 Bean 期间(doCreateBean→instantiateBean等)触发注册逻辑,最终进入自定义注册器。
3.1 registerCustomEditor 的两个重载
断点进入后,最先经过PropertyEditorRegistry接口的第一个重载方法,它把参数转交给带propertyPath的重载:
@Override public void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor) { registerCustomEditor(requiredType, null, propertyEditor); }第二个重载是实际执行的实现(来自PropertyEditorRegistrySupport):
@Override public void registerCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath, PropertyEditor propertyEditor) { if (requiredType == null && propertyPath == null) { throw new IllegalArgumentException("Either requiredType or propertyPath is required"); } if (propertyPath != null) { if (this.customEditorsForPath == null) { this.customEditorsForPath = new LinkedHashMap<>(16); } this.customEditorsForPath.put(propertyPath, new CustomEditorHolder(propertyEditor, requiredType)); } else { if (this.customEditors == null) { this.customEditors = new LinkedHashMap<>(16); } // 放入 customEditors map对象中 this.customEditors.put(requiredType, propertyEditor); this.customEditorCache = null; } }从这个实现可以提炼出两个关键数据结构:
customEditorsForPath:按属性路径(如apple.date)维度注册编辑器,适用于只针对某个具体属性生效的场景;customEditors:按目标类型(如java.util.Date)维度注册编辑器,对全局所有该类型的属性生效——本用例走的是这一分支。
3.2 registry 对象从哪来:AbstractBeanFactory#registerCustomEditors
调试时展开registry参数,其运行时类型是PropertyEditorRegistrySupport(图片 2 展示了 IDE 中查看this.customEditors变量的结果:LinkedHashMap中java.util.Date映射到CustomDateEditor,其内部dateFormat的 pattern 为yyyy-MM-dd)。
这个registry对象由org.springframework.beans.factory.support.AbstractBeanFactory#registerCustomEditors传入,该方法会在 Bean 工厂初始化、需要类型转换时被调用:
protected void registerCustomEditors(PropertyEditorRegistry registry) { PropertyEditorRegistrySupport registrySupport = (registry instanceof PropertyEditorRegistrySupport ? (PropertyEditorRegistrySupport) registry : null); if (registrySupport != null) { registrySupport.useConfigValueEditors(); } if (!this.propertyEditorRegistrars.isEmpty()) { for (PropertyEditorRegistrar registrar : this.propertyEditorRegistrars) { try { /** * {@link ResourceEditorRegistrar#registerCustomEditors(org.springframework.beans.PropertyEditorRegistry)}或者 * {@link PropertyEditorRegistrar#registerCustomEditors(org.springframework.beans.PropertyEditorRegistry)} */ registrar.registerCustomEditors(registry); } catch (BeanCreationException ex) { Throwable rootCause = ex.getMostSpecificCause(); if (rootCause instanceof BeanCurrentlyInCreationException) { BeanCreationException bce = (BeanCreationException) rootCause; String bceBeanName = bce.getBeanName(); if (bceBeanName != null && isCurrentlyInCreation(bceBeanName)) { if (logger.isDebugEnabled()) { logger.debug("PropertyEditorRegistrar [" + registrar.getClass().getName() + "] failed because it tried to obtain currently created bean '" + ex.getBeanName() + "': " + ex.getMessage()); } onSuppressedException(ex); continue; } } throw ex; } } } if (!this.customEditors.isEmpty()) { this.customEditors.forEach((requiredType, editorClass) -> registry.registerCustomEditor(requiredType, BeanUtils.instantiateClass(editorClass))); } }流程解读:
- 若
registry是PropertyEditorRegistrySupport,先调用useConfigValueEditors()启用默认的配置值编辑器(如字符串转数组、集合、Class 等); - 遍历
propertyEditorRegistrars列表,逐个调用registrar.registerCustomEditors(registry)——本用例中DatePropertyRegister正好实现了接口void registerCustomEditors(PropertyEditorRegistry registry);,因此会在这里被回调; - 如果注册器抛出的
BeanCreationException根因是BeanCurrentlyInCreationException(即注册器内部尝试获取正在创建中的 Bean,常见于循环依赖场景),会记录日志并跳过该注册器继续执行; - 最后遍历
customEditors映射,通过BeanUtils.instantiateClass(editorClass)实例化每个编辑器类,并注册到registry。
propertyEditorRegistrars与customEditors正是定义在AbstractBeanFactory中的成员变量,也是 XML 中CustomEditorConfigurer两个property注入的目标。
3.3 为什么最终拿到的是 DatePropertyEditor
顺着疑问「为什么注册的结果是com.huifer.source.spring.bean.DatePropertyEditor」回到配置文件:
<property name="customEditors"> <map> <entry key="java.util.Date" value="com.huifer.source.spring.bean.DatePropertyEditor"> </entry> </map> </property>对应的 setter 方法(位于CustomEditorConfigurer):
public void setCustomEditors(Map<Class<?>, Class<? extends PropertyEditor>> customEditors) { this.customEditors = customEditors; }也就是说,XML 中customEditors的<entry key="java.util.Date" value="...">被装配成一个Map<Class<?>, Class<? extends PropertyEditor>>注入到CustomEditorConfigurer,随后由AbstractBeanFactory#registerCustomEditors中的this.customEditors.forEach((requiredType, editorClass) -> registry.registerCustomEditor(requiredType, BeanUtils.instantiateClass(editorClass)))完成「类型 → 编辑器实例」的最终注册。这里BeanUtils.instantiateClass会通过反射调用无参构造器创建DatePropertyEditor实例。
补充说明:CustomEditorConfigurer本质上是BeanFactoryPostProcessor的一个实现,其postProcessBeanFactory会把propertyEditorRegistrars通过beanFactory.addPropertyEditorRegistrar(...)加入 Bean 工厂、把customEditors逐个registerCustomEditor注册进去。仓库文档 BeanFactoryPostProcessor.md 中以Address对象为例给出了完整可运行示例(AddressParse extends PropertyEditorSupport解析"四川,成都"为Address(province=四川, city=成都)),并指出:注册器真正被用到是在 Bean 填充属性阶段。
四、applyPropertyValues:属性注入时的类型转换
编辑器注册完成后,真正触发转换发生在 Bean 创建流程的populateBean→applyPropertyValues。该方法定义于AbstractAutowireCapableBeanFactory:
protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) { if (pvs.isEmpty()) { return; } if (System.getSecurityManager() != null && bw instanceof BeanWrapperImpl) { ((BeanWrapperImpl) bw).setSecurityContext(getAccessControlContext()); } MutablePropertyValues mpvs = null; // 没有解析的属性 List<PropertyValue> original; if (pvs instanceof MutablePropertyValues) { mpvs = (MutablePropertyValues) pvs; if (mpvs.isConverted()) { //MutablePropertyValues 对象中存在转换后对象直接赋值 // Shortcut: use the pre-converted values as-is. try { bw.setPropertyValues(mpvs); return; } catch (BeansException ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Error setting property values", ex); } } original = mpvs.getPropertyValueList(); } else { original = Arrays.asList(pvs.getPropertyValues()); } // 自定义转换器 TypeConverter converter = getCustomTypeConverter(); if (converter == null) { converter = bw; } // 创建BeanDefinitionValueResolver BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver(this, beanName, mbd, converter); // Create a deep copy, resolving any references for values. // 解析后的对象集合 List<PropertyValue> deepCopy = new ArrayList<>(original.size()); boolean resolveNecessary = false; for (PropertyValue pv : original) { // 解析过的属性 if (pv.isConverted()) { deepCopy.add(pv); } // 没有解析过的属性 else { // 属性名称 String propertyName = pv.getName(); // 属性值,直接读取到的 Object originalValue = pv.getValue(); // 解析值 Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue); Object convertedValue = resolvedValue; /** * 1. isWritableProperty: 属性可写 * 2. isNestedOrIndexedProperty: 是否循环嵌套 */ boolean convertible = bw.isWritableProperty(propertyName) && !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName); if (convertible) { // 转换器解析 convertedValue = convertForProperty(resolvedValue, propertyName, bw, converter); } // Possibly store converted value in merged bean definition, // in order to avoid re-conversion for every created bean instance. if (resolvedValue == originalValue) { if (convertible) { // 设置解析值 pv.setConvertedValue(convertedValue); } deepCopy.add(pv); } // 类型解析 else if (convertible && originalValue instanceof TypedStringValue && !((TypedStringValue) originalValue).isDynamic() && !(convertedValue instanceof Collection || ObjectUtils.isArray(convertedValue))) { pv.setConvertedValue(convertedValue); deepCopy.add(pv); } else { resolveNecessary = true; deepCopy.add(new PropertyValue(pv, convertedValue)); } } } if (mpvs != null && !resolveNecessary) { // 转换成功的标记方法 mpvs.setConverted(); } // Set our (possibly massaged) deep copy. try { bw.setPropertyValues(new MutablePropertyValues(deepCopy)); } catch (BeansException ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Error setting property values", ex); } }关键路径梳理:
- 若
pvs是MutablePropertyValues且已标记isConverted(),说明属性值已被提前转换过,直接bw.setPropertyValues(mpvs)赋值并返回——这是避免重复转换的快捷路径; - 否则取出
PropertyValue列表,创建BeanDefinitionValueResolver用于解析属性值(如引用、占位符、类型化字符串); - 逐条处理:
resolveValueIfNecessary解析原始值 → 判断属性可写且非嵌套属性 → 调用convertForProperty进行类型转换; - 转换成功后将结果缓存回
pv.setConvertedValue(...),这样同一定义创建的多个 Bean 实例无需重复转换; - 全部完成后,把深拷贝后的属性值统一
bw.setPropertyValues(...)写入BeanWrapper。
调试applyPropertyValues时可以看到(图片 3):propertyValueList中PropertyValue的name = "date",原始value是TypedStringValue,内容为"2020-01-01 01:01:01",而beanName = "apple"——这正是待转换的原始数据。
五、convertForProperty 与 doConvertTextValue:转换的最后一公里
convertForProperty是属性级转换的入口,定义于AbstractAutowireCapableBeanFactory:
@Nullable private Object convertForProperty( @Nullable Object value, String propertyName, BeanWrapper bw, TypeConverter converter) { if (converter instanceof BeanWrapperImpl) { return ((BeanWrapperImpl) converter).convertForProperty(value, propertyName); } else { PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName); MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd); return converter.convertIfNecessary(value, pd.getPropertyType(), methodParam); } }- 当转换器是
BeanWrapperImpl时,直接调用其convertForProperty(value, propertyName),内部会读取属性描述符PropertyDescriptor,拿到 setter 对应的MethodParameter与目标类型; - 否则走通用分支:
convertIfNecessary(value, pd.getPropertyType(), methodParam)。
最终字符串 → 对象的文本转换落在TypeConverterDelegate的doConvertTextValue:
private Object doConvertTextValue(@Nullable Object oldValue, String newTextValue, PropertyEditor editor) { try { editor.setValue(oldValue); } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("PropertyEditor [" + editor.getClass().getName() + "] does not support setValue call", ex); } // Swallow and proceed. } // 调用子类实现方法 editor.setAsText(newTextValue); return editor.getValue(); }该方法依次执行:
- 先尝试
editor.setValue(oldValue)设置旧值(若编辑器不支持则吞掉异常继续); - 调用编辑器的
setAsText(newTextValue)——这是真正执行转换的子类钩子方法; - 返回
editor.getValue()得到转换后的目标对象。
在本用例中,setAsText会被回调到我们编写的DatePropertyEditor实现:
@Override public void setAsText(String text) throws IllegalArgumentException { System.out.println(text); SimpleDateFormat sdf = new SimpleDateFormat(format); try { Date date = sdf.parse(text); this.setValue(date); } catch (Exception e) { e.printStackTrace(); } }调试断点停在doConvertTextValue中时,可以清晰看到(图片 4):convertedValue已被转换成一个Date对象(Wed Jan 01 00:00:00 CST 2020),且下方代码行的standardConversion标志为false——说明该次转换走的是自定义编辑器分支,而非 Spring 标准转换服务(ConversionService)。整个方法的返回值正是TypeConverterDelegate#convertIfNecessary(String, Object, Object, Class<T>, TypeDescriptor)的处理结果。
六、与 BeanFactoryPostProcessor 机制的联动
CustomEditorConfigurer是BeanFactoryPostProcessor体系的一员。回顾 BeanFactoryPostProcessor.md 中的调用链:容器refresh()过程中执行invokeBeanFactoryPostProcessors(...),对每个 BFPP 调用postProcessBeanFactory(beanFactory),CustomEditorConfigurer正是利用该回调把自定义属性编辑器提前注入到 Bean 工厂:
@Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { if (this.propertyEditorRegistrars != null) { for (PropertyEditorRegistrar propertyEditorRegistrar : this.propertyEditorRegistrars) { // 把它加入Bean工厂里后面可以进行调用 beanFactory.addPropertyEditorRegistrar(propertyEditorRegistrar); } } if (this.customEditors != null) { this.customEditors.forEach(beanFactory::registerCustomEditor); } }这与本文档第三部分的AbstractBeanFactory#registerCustomEditors正好首尾呼应:BFPP 负责「注册登记」,属性填充阶段才真正「消费使用」。
七、全链路总结
把整条链路串起来,一次自定义属性转换的完整生命周期如下:
容器 refresh() └─ invokeBeanFactoryPostProcessors() └─ CustomEditorConfigurer.postProcessBeanFactory(beanFactory) ├─ beanFactory.addPropertyEditorRegistrar(registrar) // 方式一:注册器 └─ beanFactory.registerCustomEditor(type, editor) // 方式二:customEditors Bean 创建(doCreateBean) └─ populateBean() └─ applyPropertyValues() ├─ BeanDefinitionValueResolver.resolveValueIfNecessary() // 解析原始字符串值 ├─ convertForProperty() // 属性级转换入口 │ └─ BeanWrapperImpl.convertForProperty() │ └─ TypeConverterDelegate.convertIfNecessary() │ └─ doConvertTextValue() │ ├─ editor.setValue(oldValue) │ ├─ editor.setAsText(text) // 调用自定义转换规则 │ └─ return editor.getValue() // 得到 Date 对象 └─ bw.setPropertyValues(deepCopy) // 完成赋值值得记住的三个核心结论:
- 两类注册入口:
propertyEditorRegistrars(编程式注册器,可注册多个编辑器)与customEditors(类型 → 编辑器类的声明式 Map),两者最终都汇入PropertyEditorRegistrySupport的customEditors映射; - 两个存储维度:按类型(
customEditors)与按属性路径(customEditorsForPath)注册,前者对全局该类型生效,后者仅对指定属性生效; - 一套转换模板:
TypeConverterDelegate#doConvertTextValue固定执行setValue→setAsText→getValue三步,所有自定义PropertyEditor只需要实现setAsText即可完成字符串到任意对象的转换。
如需进一步深入属性注入与类型转换的完整实现,可继续阅读仓库中的 4、依赖注入(DI).md.md)(其中同样包含convertForProperty、getCustomTypeConverter等方法的调用上下文),以及 Spring-beanFactory.md 了解 Bean 创建的整体脉络。
【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考