✍前言
你好,我是YourBatman。
上篇文章介绍了PropertyEditor在类型转换里的作用,以及举例说明了Spring内置实现的PropertyEditor们,它们各司其职完成 String <-> 各种类型 的互转。
在知晓了这些基础知识后,本文将更进一步,为你介绍Spring是如何注册、管理这些转换器,以及如何自定义转换器去实现私有转换协议。
版本约定
- Spring Framework:5.3.1
- Spring Boot:2.4.0
✍正文
稍微熟悉点Spring Framework的小伙伴就知道,Spring特别擅长API设计、模块化设计。后缀模式是它常用的一种管理手段,比如xxxRegistry注册中心在Spring内部就有非常多:
xxxRegistry用于管理(注册、修改、删除、查找)一类组件,当组件类型较多时使用注册中心统一管理是一种非常有效的手段。诚然,PropertyEditor就属于这种场景,管理它们的注册中心是PropertyEditorRegistry。
PropertyEditorRegistry
它是管理PropertyEditor的中心接口,负责注册、查找对应的PropertyEditor。
// @since 1.2.6 public interface PropertyEditorRegistry { // 注册一个转换器:该type类型【所有的属性】都将交给此转换器去转换(即使是个集合类型) // 效果等同于调用下方法:registerCustomEditor(type,null,editor); void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor); // 注册一个转换器:该type类型的【propertyPath】属性将交给此转换器 // 此方法是重点,详解见下文 void registerCustomEditor(Class<?> requiredType, String propertyPath, PropertyEditor propertyEditor); // 查找到一个合适的转换器 PropertyEditor findCustomEditor(Class<?> requiredType, String propertyPath); }
说明:该API是
1.2.6
这个小版本新增的。Spring 一般 不会在小版本里新增核心API以确保稳定性,但这并非100%。Spring认为该API对使用者无感的话(你不可能会用到它),增/减也是有可能的
此接口的继承树如下:
值得注意的是:虽然此接口看似实现者众多,但其实其它所有的实现关于PropertyEditor的管理部分都是委托给PropertyEditorRegistrySupport来管理,无一例外。因此,本文只需关注PropertyEditorRegistrySupport足矣,这为后面的高级应用(如数据绑定、BeanWrapper等)打好坚实基础。
用不太正确的理解可这么认为:PropertyEditorRegistry接口的唯一实现只有PropertyEditorRegistrySupport
PropertyEditorRegistrySupport
它是PropertyEditorRegistry接口的实现,提供对default editors和custom editors的管理,最终主要为BeanWrapperImpl和DataBinder服务。
一般来说,Registry注册中心内部会使用多个Map来维护,代表注册表。此处也不例外:
// 装载【默认的】编辑器们,初始化的时候会注册好 private Map<Class<?>, PropertyEditor> defaultEditors; // 如果想覆盖掉【默认行为】,可通过此Map覆盖(比如处理Charset类型你不想用默认的编辑器处理) // 通过API:overrideDefaultEditor(...)放进此Map里 private Map<Class<?>, PropertyEditor> overriddenDefaultEditors; // ======================注册自定义的编辑器====================== // 通过API:registerCustomEditor(...)放进此Map里(若没指定propertyPath) private Map<Class<?>, PropertyEditor> customEditors; // 通过API:registerCustomEditor(...)放进此Map里(若指定了propertyPath) private Map<String, CustomEditorHolder> customEditorsForPath;
PropertyEditorRegistrySupport使用了4个 Map来维护不同来源的编辑器,作为查找的 “数据源”。
这4个Map可分为两大组,并且有如下规律:
- 默认编辑器组:defaultEditors和overriddenDefaultEditors
- overriddenDefaultEditors优先级 高于 defaultEditors
- 自定义编辑器组:customEditors和customEditorsForPath
- 它俩为互斥关系
细心的小伙伴会发现还有一个Map咱还未提到:
private Map<Class<?>, PropertyEditor> customEditorCache;
从属性名上理解,它表示customEditors属性的缓存。那么问题来了:customEditors和customEditorCache的数据结构一毛一样(都是Map),谈何缓存呢?直接从customEditors里获取值不更香吗?
customEditorCache作用解释
customEditorCache用于缓存自定义的编辑器,辅以成员属性customEditors属性一起使用。具体(唯一)使用方式在私有方法:根据类型获取自定义编辑器PropertyEditorRegistrySupport#getCustomEditor
private PropertyEditor getCustomEditor(Class<?> requiredType) { if (requiredType == null || this.customEditors == null) { return null; } PropertyEditor editor = this.customEditors.get(requiredType); // 重点:若customEditors没有并不代表处理不了,因为还得考虑父子关系、接口关系 if (editor == null) { // 去缓存里查询,是否存在父子类作为key的情况 if (this.customEditorCache != null) { editor = this.customEditorCache.get(requiredType); } // 若缓存没命中,就得遍历customEditors了,时间复杂度为O(n) if (editor == null) { for (Iterator<Class<?>> it = this.customEditors.keySet().iterator(); it.hasNext() && editor == null;) { Class<?> key = it.next(); if (key.isAssignableFrom(requiredType)) { editor = this.customEditors.get(key); if (this.customEditorCache == null) { this.customEditorCache = new HashMap<Class<?>, PropertyEditor>(); } this.customEditorCache.put(requiredType, editor); } } } } return editor; }
这段逻辑不难理解,此流程用一张图可描绘如下:
因为遍历customEditors属于比较重的操作(复杂度为O(n)),从而使用了customEditorCache避免每次出现父子类的匹配情况就去遍历一次,大大提高匹配效率。
什么时候customEditorCache会发挥作用?也就说什么时候会出现父子类匹配情况呢?为了加深理解,下面搞个例子玩一玩
代码示例
准备两个具有继承关系的实体类型
@Data public abstract class Animal { private Long id; private String name; } public class Cat extends Animal { }
书写针对于父类(父接口)类型的编辑器:
public class AnimalPropertyEditor extends PropertyEditorSupport { @Override public String getAsText() { return null; } @Override public void setAsText(String text) throws IllegalArgumentException { } }
说明:由于此部分只关注查找/匹配过程逻辑,因此对编辑器内部处理逻辑并不关心
注册此编辑器,对应的类型为父类型:Animal
@Test public void test5() { PropertyEditorRegistry propertyEditorRegistry = new PropertyEditorRegistrySupport(); propertyEditorRegistry.registerCustomEditor(Animal.class, new AnimalPropertyEditor()); // 付类型、子类型均可匹配上对应的编辑器 PropertyEditor customEditor1 = propertyEditorRegistry.findCustomEditor(Cat.class, null); PropertyEditor customEditor2 = propertyEditorRegistry.findCustomEditor(Animal.class, null); System.out.println(customEditor1 == customEditor2); System.out.println(customEditor1.getClass().getSimpleName()); }
运行程序,结果为:
true AnimalPropertyEditor
结论:
- 类型精确匹配优先级最高
- 若没精确匹配到结果且本类型的父类型已注册上去,则最终也会匹配成功
customEditorCache的作用可总结为一句话:帮助customEditors属性装载对已匹配上的子类型的编辑器,从而避免了每次全部遍历,有效的提升了匹配效率。
值得注意的是,每次调用API向customEditors添加新元素时,customEditorCache就会被清空,因此因尽量避免在运行期注册编辑器,以避免缓存失效而降低性能