3. 搞定收工,PropertyEditor 就到这
分享、成长,拒绝浅藏辄止。搜索公众号【BAT 的乌托邦】,回复关键字
专栏
有 Spring 技术栈、中间件等小而美的原创专栏供以免费学习。本文已被 [https://www.yourbatman.cn]
✍前言
你好,我是 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。
说明:该 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 来维护,代表注册表。此处也不例外:
PropertyEditorRegistrySupport 使用了 4 个 Map 来维护不同来源的编辑器,作为查找的 “数据源”。
这 4 个 Map 可分为两大组,并且有如下规律:
默认编辑器组:defaultEditors 和 overriddenDefaultEditors
- overriddenDefaultEditors 优先级 高于 defaultEditors
自定义编辑器组:customEditors 和 customEditorsForPath
- 它俩为互斥关系
细心的小伙伴会发现还有一个 Map 咱还未提到:
从属性名上理解,它表示customEditors
属性的缓存。那么问题来了:customEditors 和 customEditorCache 的数据结构一毛一样(都是 Map),谈何缓存呢?直接从 customEditors 里获取值不更香吗?
customEditorCache 作用解释
customEditorCache 用于缓存自定义的编辑器,辅以成员属性 customEditors 属性一起使用。具体(唯一)使用方式在私有方法:根据类型获取自定义编辑器PropertyEditorRegistrySupport#getCustomEditor
这段逻辑不难理解,此流程用一张图可描绘如下:
因为遍历customEditors
属于比较重的操作(复杂度为 O(n)),从而使用了 customEditorCache 避免每次出现父子类的匹配情况就去遍历一次,大大提高匹配效率。
什么时候 customEditorCache 会发挥作用?也就说什么时候会出现父子类匹配情况呢?为了加深理解,下面搞个例子玩一玩
代码示例
准备两个具有继承关系的实体类型
书写针对于父类(父接口)类型的编辑器:
说明:由于此部分只关注查找/匹配过程逻辑,因此对编辑器内部处理逻辑并不关心
注册此编辑器,对应的类型为父类型:Animal
运行程序,结果为:
结论:
类型精确匹配优先级最高
若没精确匹配到结果且本类型的父类型已注册上去,则最终也会匹配成功
customEditorCache 的作用可总结为一句话:帮助 customEditors 属性装载对已匹配上的子类型的编辑器,从而避免了每次全部遍历,有效的提升了匹配效率。
**值得注意的是,每次调用 API 向customEditors
添加新元素时,customEditorCache 就会被清空,因此因尽量避免在运行期注册编辑器,以避免缓存失效而降低性能**
customEditorsForPath 作用解释
上面说了,它是和 customEditors 互斥的。
customEditorsForPath 的作用是能够实现更精准匹配,针对属性级别精准处理。此 Map 的值通过此 API 注册进来:
说明:propertyPath 不能为 null 才进此处,否则会注册进 customEditors 喽
可能你会想,有了 customEditors 为何还需要 customEditorsForPath 呢?这里就不得不说两者的最大区别了:
customEditors
:粒度较粗,通用性强。key 为类型,即该类型的转换全部交给此编辑器处理
- 如:registerCustomEditor(UUID.class,new UUIDEditor())
,那么此编辑器就能处理全天下所有的String <-> UUID
转换工作
customEditorsForPath
:粒度细精确到属性(字段)级别,有点专车专座的意思
- 如:registerCustomEditor(Person.class, "cat.uuid" , new UUIDEditor())
,那么此编辑器就有且仅能处理Person.cat.uuid
属性,其它的一概不管
有了这种区别,注册中心在findCustomEditor(requiredType,propertyPath)
匹配的时候也是按照优先级顺序执行匹配的:
若指定了 propertyPath(不为 null),就先去
customEditorsForPath
里找。否则就去customEditors
里找若没有指定 propertyPath(为 null),就直接去
customEditors
里找
为了加深理解,讲上场景用代码实现如下。
代码示例
创建一个 Person 类,关联 Cat
现在的需求场景是:
UUID 类型统一交给 UUIDEditor 处理(当然包括 Cat 里面的 UUID 类型)
Person 类里面的 Cat 的 UUID 类型,需要单独特殊处理,因此格式不一样需要“特殊照顾”
很明显这就需要两个不同的属性编辑器来实现,然后组织起来协同工作。Spring 内置了 UUIDEditor 可以处理一般性的 UUID 类型(通用),而 Person 专用的 UUID 编辑器,自定义如下:
向注册中心注册编辑器,并且书写测试代码如下:
运行程序,打印输出:
完美。
customEditorsForPath 相当于给你留了钩子,当你在某些特殊情况需要特殊照顾的时候,你可以借助它来搞定,十分的方便。
此方式有必要记住并且尝试,在实际开发中使用得还是比较多的。特别在你不想全局定义,且要确保向下兼容性的时候,使用抽象接口类型 + 此种方式缩小影响范围将十分有用
说明:propertyPath 不仅支持 Java Bean 导航方式,还支持集合数组方式,如
Person.cats[0].uuid
这样格式也是 ok 的
PropertyEditorRegistrar
Registrar:登记员。它一般和 xxxRegistry 配合使用,其实内核还是 Registry,只是运用了倒排思想屏蔽一些内部实现而已。
同样的,Spring 内部也有很多类似实现模式:
PropertyEditorRegistrar 接口在 Spring 体系内唯一实现为:ResourceEditorRegistrar
。它可值得我们絮叨絮叨。
ResourceEditorRegistrar
从命名上就知道它和 Resource 资源有关,实际上也确实如此:主要负责将ResourceEditor
注册到注册中心里面去,用于处理形如 Resource、File、URI 等这些资源类型。
你配置 classpath:xxx.xml 用来启动 Spring 容器的配置文件,String -> Resource 转换就是它的功劳喽
唯一构造器为:
resourceLoader:一般传入 ApplicationContext
propertyResolver:一般传入 Environment
很明显,它的设计就是服务于 ApplicationContext 上下文,在 Bean 创建过程中辅助BeanWrapper
实现资源加载、转换。
BeanFactory
在初始化的准备过程中就将它实例化,从而具备资源处理能力:
这也是 PropertyEditorRegistrar 在 Spring Framework 的唯一使用处,值的关注。
PropertyEditor 自动发现机制
最后介绍一个使用中的奇淫小技巧:PropertyEditor 自动发现机制。
一般来说,我们自定义一个 PropertyEditor 是为了实现自定义类型 <-> 字符串的自动转换,它一般需要有如下步骤:
为自定义类型写好一个 xxxPropertyEditor(实现 PropertyEditor 接口)
将写好的编辑器注册到注册中心 PropertyEditorRegistry
显然步骤 1 属个性化行为无法替代,但步骤 2 属于标准行为,重复劳动是可以标准化的。自动发现机制就是用来解决此问题,对自定义的编辑器制定了如下标准:
实现了 PropertyEditor 接口,具有空构造器
与自定义类型同包(在同一个 package 内),名称必须为:
targetType.getName() + "Editor"
这样你就无需再手动注册到注册中心了(当然手动注册了也不碍事),Spring 能够自动发现它,这在有大量自定义类型编辑器的需要的时候将很有用。
说明:此段核心逻辑在
BeanUtils#findEditorByConvention()
里,有兴趣者可看看
值得注意的是:此机制属 Spring 遵循 Java Bean 规范而单独提供,在单独使用PropertyEditorRegistry
时并未开启,而是在使用 Spring 产品级能力TypeConverter
时有提供,这在后文将有体现,欢迎保持关注。
✍总结
本文在了解 PropertyEditor 基础支持之上,主要介绍了其注册中心PropertyEditorRegistry
的使用。PropertyEditorRegistrySupport 作为其“唯一”实现,负责管理 PropertyEditor,包括通用处理和专用处理。最后介绍了 PropertyEditor 的自动发现机制,其实在实际生产中我并不建议使用自动机制,因为对于可能发生改变的因素,显示指定优于隐式约定。
关于 Spring 类型转换 PropertyEditor 相关内容就介绍到这了,虽然它很“古老”但并没有退出历史舞台,在排查问题,甚至日常扩展开发中还经常会碰到,因此强烈建议你掌握。下面起将介绍 Spring 类型转换的另外一个重点:新时代的类型转换服务ConversionService
及其周边。
✔✔✔推荐阅读✔✔✔
【Spring 类型转换】系列:
【Jackson】系列:
【数据校验 Bean Validation】系列:
【新特性】系列:
[IntelliJ IDEA 2020.1 正式发布,你要的 Almost 都在这!]()
【程序人生】系列:
还有诸如【Spring 配置类】【Spring-static】【Spring 数据绑定】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原创专栏,关注BAT的乌托邦
回复专栏
二字即可全部获取,分享、成长,拒绝浅藏辄止。
有些专栏已完结,有些正在连载中,期待你的关注、共同进步
---------
♥关注 A 哥♥
Author | A哥(YourBatman)
-------- | -----
个人站点 | www.yourbatman.cn
E-mail | yourbatman@qq.com
微 信 | fsx1056342982
公众号 | BAT的乌托邦(ID:BAT-utopia)
知识星球 | BAT的乌托邦
每日文章推荐 | 每日文章推荐
版权声明: 本文为 InfoQ 作者【YourBatman】的原创文章。
原文链接:【http://xie.infoq.cn/article/73f9eb5784f68e0f084b430d2】。文章转载请联系作者。
评论