zoukankan      html  css  js  c++  java
  • 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类型转换】系列:

    【Jackson】系列:

    【数据校验Bean Validation】系列:

    【新特性】系列:

    【程序人生】系列:

    还有诸如【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的乌托邦
    每日文章推荐 每日文章推荐

    BAT的乌托邦

  • 相关阅读:
    arthas常用命令记录
    idea 的 http-client
    springboot 接口层参数校验 自定义参数校验
    Spring AOP 实现——使用annotation、pointcut、aspect
    Redis 热点名词
    SpringCloud注册发现配置
    【设计模式】-行为型-11-解释器模式
    【设计模式】-行为型-10-备忘录模式
    【设计模式】-创建型-9-访问者模式
    Kubernetes运行原理
  • 原文地址:https://www.cnblogs.com/yourbatman/p/14149463.html
Copyright © 2011-2022 走看看