6

3. 搞定收工,PropertyEditor就到这

 3 years ago
source link: https://blog.csdn.net/f641385712/article/details/110514831
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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哥♥

BAT的乌托邦


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK