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。
// @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就会被清空,因此因尽量避免在运行期注册编辑器,以避免缓存失效而降低性能
customEditorsForPath作用解释
上面说了,它是和customEditors互斥的。
customEditorsForPath的作用是能够实现更精准匹配,针对属性级别精准处理。此Map的值通过此API注册进来:
public void registerCustomEditor(Class<?> requiredType, String propertyPath, PropertyEditor propertyEditor);
说明: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
@Data
public class Cat extends Animal {
private UUID uuid;
}
@Data
public class Person {
private Long id;
private String name;
private Cat cat;
}
现在的需求场景是:
- UUID类型统一交给UUIDEditor处理(当然包括Cat里面的UUID类型)
- Person类里面的Cat的UUID类型,需要单独特殊处理,因此格式不一样需要“特殊照顾”
很明显这就需要两个不同的属性编辑器来实现,然后组织起来协同工作。Spring内置了UUIDEditor可以处理一般性的UUID类型(通用),而Person 专用的 UUID编辑器,自定义如下:
public class PersonCatUUIDEditor extends UUIDEditor {
private static final String SUFFIX = "_YourBatman";
@Override
public String getAsText() {
return super.getAsText().concat(SUFFIX);
}
@Override
public void setAsText(String text) throws IllegalArgumentException {
text = text.replace(SUFFIX, "");
super.setAsText(text);
}
}
向注册中心注册编辑器,并且书写测试代码如下:
@Test
public void test6() {
PropertyEditorRegistry propertyEditorRegistry = new PropertyEditorRegistrySupport();
// 通用的
propertyEditorRegistry.registerCustomEditor(UUID.class, new UUIDEditor());
// 专用的
propertyEditorRegistry.registerCustomEditor(Person.class, "cat.uuid", new PersonCatUUIDEditor());
String uuidStr = "1-2-3-4-5";
String personCatUuidStr = "1-2-3-4-5_YourBatman";
PropertyEditor customEditor = propertyEditorRegistry.findCustomEditor(UUID.class, null);
// customEditor.setAsText(personCatUuidStr); // 抛异常:java.lang.NumberFormatException: For input string: "5_YourBatman"
customEditor.setAsText(uuidStr);
System.out.println(customEditor.getAsText());
customEditor = propertyEditorRegistry.findCustomEditor(Person.class, "cat.uuid");
customEditor.setAsText(personCatUuidStr);
System.out.println(customEditor.getAsText());
}
运行程序,打印输出:
00000001-0002-0003-0004-000000000005
00000001-0002-0003-0004-000000000005_YourBatman
完美。
customEditorsForPath相当于给你留了钩子,当你在某些特殊情况需要特殊照顾的时候,你可以借助它来搞定,十分的方便。
此方式有必要记住并且尝试,在实际开发中使用得还是比较多的。特别在你不想全局定义,且要确保向下兼容性的时候,使用抽象接口类型 + 此种方式缩小影响范围将十分有用
说明:propertyPath不仅支持Java Bean导航方式,还支持集合数组方式,如
Person.cats[0].uuid
这样格式也是ok的
PropertyEditorRegistrar
Registrar:登记员。它一般和xxxRegistry配合使用,其实内核还是Registry,只是运用了倒排思想屏蔽一些内部实现而已。
public interface PropertyEditorRegistrar {
void registerCustomEditors(PropertyEditorRegistry registry);
}
同样的,Spring内部也有很多类似实现模式:
PropertyEditorRegistrar接口在Spring体系内唯一实现为:ResourceEditorRegistrar
。它可值得我们絮叨絮叨。
ResourceEditorRegistrar
从命名上就知道它和Resource资源有关,实际上也确实如此:主要负责将ResourceEditor
注册到注册中心里面去,用于处理形如Resource、File、URI等这些资源类型。
你配置classpath:xxx.xml用来启动Spring容器的配置文件,String -> Resource转换就是它的功劳喽
唯一构造器为:
public ResourceEditorRegistrar(ResourceLoader resourceLoader, PropertyResolver propertyResolver) {
this.resourceLoader = resourceLoader;
this.propertyResolver = propertyResolver;
}
- resourceLoader:一般传入ApplicationContext
- propertyResolver:一般传入Environment
很明显,它的设计就是服务于ApplicationContext上下文,在Bean创建过程中辅助BeanWrapper
实现资源加载、转换。
BeanFactory
在初始化的准备过程中就将它实例化,从而具备资源处理能力:
AbstractApplicationContext:
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
...
beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));
...
}
这也是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】系列:
- 1. 初识Jackson -- 世界上最好的JSON库
- 2. 妈呀,Jackson原来是这样写JSON的
- 3. 懂了这些,方敢在简历上说会用Jackson写JSON
- 4. JSON字符串是如何被解析的?JsonParser了解一下
- 5. JsonFactory工厂而已,还蛮有料,这是我没想到的
- 6. 二十不惑,ObjectMapper使用也不再迷惑
- 7. Jackson用树模型处理JSON是必备技能,不信你看
【数据校验Bean Validation】系列:
- 1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知
- 2. Bean Validation声明式校验方法的参数、返回值
- 3. 站在使用层面,Bean Validation这些标准接口你需要烂熟于胸
- 4. Validator校验器的五大核心组件,一个都不能少
- 5. Bean Validation声明式验证四大级别:字段、属性、容器元素、类
- 6. 自定义容器类型元素验证,类级别验证(多字段联合验证)
【新特性】系列:
- IntelliJ IDEA 2020.3正式发布,年度最后一个版本很讲武德
- IntelliJ IDEA 2020.2正式发布,诸多亮点总有几款能助你提效
- IntelliJ IDEA 2020.1正式发布,你要的Almost都在这!
- Spring Framework 5.3.0正式发布,在云原生路上继续发力
- Spring Boot 2.4.0正式发布,全新的配置文件加载机制(不向下兼容)
- Spring改变版本号命名规则:此举对非英语国家很友好
- JDK15正式发布,划时代的ZGC同时宣布转正
【程序人生】系列:
还有诸如【Spring配置类】【Spring-static】【Spring数据绑定】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原创专栏,关注BAT的乌托邦
回复专栏
二字即可全部获取,分享、成长,拒绝浅藏辄止。
有些专栏已完结,有些正在连载中,期待你的关注、共同进步
3. 搞定收工,PropertyEditor就到这的更多相关文章
- vue+mock.js+element-ui模拟数据搞定分页
效果如图: 前提是搭好vue前端框架,npm install mockjs引入mock.js 当前页全部代码如下,其他有关element-ui的引入未提到,仅作参考用 <!-- 用户管理 --& ...
- 【开源】简单4步搞定QQ登录,无需什么代码功底【无语言界限】
说17号发超简单的教程就17号,qq核审通过后就封装了这个,现在放出来~~ 这个是我封装的一个开源项目:https://github.com/dunitian/LoTQQLogin ————————— ...
- 对百度WebUploader开源上传控件的二次封装,精简前端代码(两句代码搞定上传)
前言 首先声明一下,我这个是对WebUploader开源上传控件的二次封装,底层还是WebUploader实现的,只是为了更简洁的使用他而已. 下面先介绍一下WebUploader 简介: WebUp ...
- 教你怎么半天搞定Docker
首先,不要把docker想的那么高大,它不就是先做个镜像,然后通过docker像虚拟机一样跑起来嘛...docker其实在真实业务场景中还是非常有局限性的.Dockerfile脚本也没那么好写,有些应 ...
- 用ORM的思想操作XML文档,一个对象就搞定不要太简单。滚蛋吧!XmlDocument、XmlNode、Xml***……
大家有没有这样的感受,一涉及XML文档操作就得百度一遍.是不是非!常!烦!.各种类型,各种方法,更别提为了找到一个节点多费劲.本来想写个XML操作的工具方法,写了两行一想既然XML文档是有规律的,如果 ...
- JS组件系列——又一款MVVM组件:Vue(一:30分钟搞定前端增删改查)
前言:关于Vue框架,好几个月之前就听说过,了解一项新技术之后,总是处于观望状态,一直在犹豫要不要系统学习下.正好最近有点空,就去官网了解了下,看上去还不错的一个组件,就抽空研究了下.最近园子里vue ...
- JS组件系列——BootstrapTable+KnockoutJS实现增删改查解决方案(三):两个Viewmodel搞定增删改查
前言:之前博主分享过knockoutJS和BootstrapTable的一些基础用法,都是写基础应用,根本谈不上封装,仅仅是避免了html控件的取值和赋值,远远没有将MVVM的精妙展现出来.最近项目打 ...
- 如何让两个div在同一行显示?一个float搞定
最近在学习div和css,遇到了一些问题也解决了很多以前以为很难搞定的问题.比如:如何让两个div显示在同一行呢?(不是用table表格,table对SE不太友好)其实,<div> 是一个 ...
- 3小时搞定一个简单的MIS系统案例Northwind,有视频、有源代码下载、有真相
一.瞎扯框架.架构 楼主自从1998年从C语言.MASM.Foxbase开始学计算机开始接触这个行当16年以来,2001年干第一份与程序.软件.然后是各种屌的东西开始,差不多干了13年了,这13年来, ...
随机推荐
- 精尽MyBatis源码分析 - SQL执行过程(二)之 StatementHandler
该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...
- Word中如何调整MathType公式的间距
作为一名理工科的学生,经常会面对一大堆公式,那么就要掌握在Word中编辑公式的技能,那么怎样才能在Word中编辑美观的公式呢?为了方便大家的使用,下面就详细介绍在Word中调整MathType公式间距 ...
- FL Studio录制面板知识讲解
FL Studio录制面板可以设置与录制有关的选项,它还有一个用来设置音符对齐的全局吸附选择器.刚接触水果这款音乐制作软件的同学通常不是很清楚这里的知识的,下面小编就给大家讲解一下. 1.首先,我们来 ...
- 解决Jenkins可安装界面是空白的小技巧
打开后这里面最底下有个[升级站点],把其中的链接改成http的就好了,http://updates.jenkins.io/update-center.json. 然后在服务列表中关闭jenkins,再 ...
- 【VUE】5.路由导航守卫
1. 功能需求 1. 当用户登陆成功后,把得到的token存到Session Storage 2. components -> Form.vue , 对预验证进行校验,如果验证不正确就跳出,如果 ...
- Mat使用详解
背景 笔记中躺了很久的文章,今天用到Mat时发现之前写的内容还算清晰,分享出来; 如下所举例使用的dump文件是针对之前使用的ignite库溢出时的dump文件:关于ignite的概念此处不再叙述,本 ...
- Java集合【1】--俯瞰java集合分类
目录 (一) java集合分类 (1) Iterable接口 1. 内部定义的方法 1.1 iterator方法 1.2 forEach方法 1.3 spliterator方法 2. Collecti ...
- OD断点操作
原文链接:https://www.cnblogs.com/qiyeboy/p/6815988.html 在做Windows平台软件逆向时,Ollydbg是极其常用的逆向工具,动态调试功能非常强大.在调 ...
- CentOS中安装Docker步骤
1.安装仓库所需要的软件包 yum install -y yum-utils device-mapper-persistent-data lvm2 2.设置yum加速源 yum-config-mana ...
- JZOJ2020年9月19日提高B组反思
CSP第一轮倒计时:22天 JZOJ2020年9月19日提高B组反思 今天比的不好,只有签到题过了 130,rank 20 T1 签到题 用二分直接切 AC 100 T2 觉得是依赖背包问题 但是我没 ...