3. 搞定收工,PropertyEditor就到这(上)

简介: 3. 搞定收工,PropertyEditor就到这(上)

✍前言


你好,我是YourBatman。


上篇文章介绍了PropertyEditor在类型转换里的作用,以及举例说明了Spring内置实现的PropertyEditor们,它们各司其职完成 String <-> 各种类型 的互转。


在知晓了这些基础知识后,本文将更进一步,为你介绍Spring是如何注册、管理这些转换器,以及如何自定义转换器去实现私有转换协议。


版本约定


  • Spring Framework:5.3.1
  • Spring Boot:2.4.0


✍正文


稍微熟悉点Spring Framework的小伙伴就知道,Spring特别擅长API设计、模块化设计。后缀模式是它常用的一种管理手段,比如xxxRegistry注册中心在Spring内部就有非常多:


image.png


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对使用者无感的话(你不可能会用到它),增/减也是有可能的


此接口的继承树如下:


image.png


值得注意的是:虽然此接口看似实现者众多,但其实其它所有的实现关于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来维护不同来源的编辑器,作为查找的 “数据源”


image.png


这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;
}


这段逻辑不难理解,此流程用一张图可描绘如下:



image.png


因为遍历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



结论


  • 类型精确匹配优先级最高
  • 若没精确匹配到结果且本类型的父类型已注册上去,则最终也会匹配成功


image.png


customEditorCache的作用可总结为一句话:帮助customEditors属性装载对已匹配上的子类型的编辑器,从而避免了每次全部遍历,有效的提升了匹配效率。


值得注意的是,每次调用API向customEditors添加新元素时,customEditorCache就会被清空,因此因尽量避免在运行期注册编辑器,以避免缓存失效而降低性能

相关文章
|
消息中间件 Java Kafka
掌握Kafka事务,看这篇就够了
先赞后看,南哥助你Java进阶一大半Kafka事务实际上引入了原子多分区写入的概念,播客画了以下流程图,展示了事务在分区级别如何工作。我是南哥,一个Java学习与进阶的领路人,相信对你通关面试、拿下Offer进入心心念念的公司有所帮助。
598 3
掌握Kafka事务,看这篇就够了
柔性事务与刚性事务的区别
柔性事务遵循BASE理论,强调系统在面对高并发和分布式环境时的基本可用性和最终一致性。与之相对,刚性事务严格遵守ACID原则,确保操作的原子性、一致性、隔离性和持久性,适用于对数据完整性和准确性要求极高的场景。
386 5
|
自然语言处理 Linux C++
make和Cmake都有什么区别?(内附使用详解)
make: 是一个构建工具,它的任务是读取 Makefile 文件,并基于这些文件中的指令执行具体的构建操作。Makefile 文件包含了如何构建项目的规则,make 负责解析这些规则并执行必要的命令来编译和链接源代码,生成可执行文件或库。 CMake: 是一个构建系统生成器。它并不直接进行编译或链接,而是根据项目中 CMakeLists.txt 文件的内容生成一个或多个构建系统的描述文件(如 Makefile 或 Visual Studio 解决方案)。CMake 提供了一种更高级、更抽象的方式来描述构建过程,这使得它能够跨平台地生成各种构建系统。
1954 1
|
存储 JSON 定位技术
基于or-tools解决物流调度问题(二)
基于or-tools解决物流调度问题(二)
499 6
|
运维 前端开发 Java
全网最全!彻底弄透Java处理GMT/UTC日期时间(上)
全网最全!彻底弄透Java处理GMT/UTC日期时间(上)
全网最全!彻底弄透Java处理GMT/UTC日期时间(上)
|
测试技术
软件设计原则-里氏替换原则讲解以及代码示例
里氏替换原则(Liskov Substitution Principle,LSP)是面向对象设计中的一条重要原则,它由Barbara Liskov在1987年提出。 里氏替换原则的核心思想是:父类的对象可以被子类的对象替换,而程序的行为不会发生变化。也就是说,如果一个类型A是另一个类型B的子类型,那么在任何使用B的地方都可以使用A,而不会引起错误或异常。
1479 0
|
XML JavaScript Java
全网最全!彻底弄透Java处理GMT/UTC日期时间(中)
全网最全!彻底弄透Java处理GMT/UTC日期时间(中)
全网最全!彻底弄透Java处理GMT/UTC日期时间(中)
|
存储 IDE Java
玩转IDEA项目结构Project Structure,打Jar包、模块/依赖管理全搞定(上)
玩转IDEA项目结构Project Structure,打Jar包、模块/依赖管理全搞定(上)
玩转IDEA项目结构Project Structure,打Jar包、模块/依赖管理全搞定(上)
|
NoSQL 网络协议 Linux
Redis-6.2.6 Linux 离线安装教程,让你一路畅通无阻,5分钟轻松完成安装。
Redis-6.2.6 Linux 离线安装教程,让你一路畅通无阻,5分钟轻松完成安装。
Redis-6.2.6 Linux 离线安装教程,让你一路畅通无阻,5分钟轻松完成安装。
如何使用PowerShell批量删除注册表项
如何使用PowerShell批量删除注册表项呢?
646 0