乐趣区

关于spring: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 editorscustom editors的治理,最终次要为 BeanWrapperImplDataBinder服务。

一般来说,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) 匹配的时候也是依照优先级程序执行匹配的:

  1. 若指定了 propertyPath(不为 null),就先去 customEditorsForPath 里找。否则就去 customEditors 里找
  2. 若没有指定 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 是为了实现 自定义类型 <-> 字符串 的主动转换,它个别须要有如下步骤:

  1. 为自定义类型写好一个 xxxPropertyEditor(实现 PropertyEditor 接口)
  2. 将写好的编辑器注册到注册核心 PropertyEditorRegistry

显然步骤 1 属个性化行为无奈代替,但步骤 2 属于规范行为,重复劳动是能够标准化的。主动发现机制就是用来解决此问题,对自定义的编辑器制订了如下规范:

  1. 实现了 PropertyEditor 接口,具备 空结构器
  2. 与自定义类型 同包(在同一个 package 内),名称必须为:targetType.getName() + "Editor"

这样你就无需再手动注册到注册核心了(当然手动注册了也不碍事),Spring 可能主动发现它,这在有大量自定义类型编辑器的须要的时候将很有用。

阐明:此段外围逻辑在 BeanUtils#findEditorByConvention() 里,有趣味者可看看

值得注意的是:此机制属 Spring 遵循 Java Bean 标准而独自提供,在独自应用 PropertyEditorRegistry 时并未开启,而是在应用 Spring 产品级能力 TypeConverter 时有提供,这在后文将有体现,欢送放弃关注。

✍总结

本文在理解 PropertyEditor 根底反对之上,次要介绍了其注册核心 PropertyEditorRegistry 的应用。PropertyEditorRegistrySupport 作为其“惟一”实现,负责管理 PropertyEditor,包含通用解决和专用解决。最初介绍了 PropertyEditor 的主动发现机制,其实在理论生产中我并 倡议应用主动机制,因为对于可能产生扭转的因素,显示指定优于隐式约定

对于 Spring 类型转换 PropertyEditor 相干内容就介绍到这了,尽管它很“古老”但并没有退出历史舞台,在排查问题,甚至日常扩大开发中还常常会碰到,因而 强烈建议你把握 。上面起将介绍 Spring 类型转换的另外一个重点:新时代的类型转换服务ConversionService 及其周边。


✔✔✔举荐浏览✔✔✔

【Spring 类型转换】系列:

  • 1. 揭秘 Spring 类型转换 – 框架设计的基石
  • 2. Spring 晚期类型转换,基于 PropertyEditor 实现
  • 3. 搞定出工,PropertyEditor 就到这

【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 的乌托邦 回复 专栏 二字即可全副获取,分享、成长,回绝浅藏辄止。

有些专栏 已完结 ,有些正在 连载中,期待你的关注、共同进步


♥关注 A 哥♥

Author A 哥(YourBatman)
集体站点 www.yourbatman.cn
E-mail yourbatman@qq.com
微 信 fsx1056342982
沉闷平台
公众号 BAT 的乌托邦(ID:BAT-utopia)
常识星球 BAT 的乌托邦
每日文章举荐 每日文章举荐

退出移动版