你好,我是YourBatman。
Spring早在1.0(2004年发布,2003年孵化中)的时候,就有了类型转换功能模块。此模块存在的必要性不必多说,相信每个同学都可理解。最初,Spring做类型转换器是基于Java标准的java.beans.PropertyEditor
这个API去扩展实现的,直到Spring 3.0后才得以出现更好替代方案(Spring 3.0发布于2009 年12月)。
❝提示:文章末尾附有Spring主要版本的发布时间和以及主要特性,感兴趣者可文末查看
❞
虽说Spring自3.0就提出了更为灵活、优秀的类型转换接口/服务,但是早期基于PropertyEditor
实现的转换器并未废弃且还在发挥余热中,因此本文就针对其早期类型转换实现做出专文讲解。
❝说明:版本均于2020-11发布,且版本号均不带有
❞.RELEASE
后缀,这和之前是不一样的。具体原因请参考:Spring改变版本号命名规则:此举对非英语国家很友好
若你用当下的眼光去看Spring基于PropertyEditor
的类型转换实现,会发现这么搞是存在一些设计缺陷的。当然并不能这么去看,毕竟现在都2020年了,那会才哪跟哪呢。既然Spring里的PropertyEditor
现如今依然健在,那咱就会会它呗。
PropertyEditor
位于java.beans包中,要知道这个包里面的类都是设计为Java GUI程序(AWT)服务的,所以你看官方javadoc对PropertyEditor
的介绍也无出其右:
A PropertyEditor class provides support for GUIs that want to allow users to edit a property value of a given type.
为GUI程序提供支持,允许你对给定的value进行编辑,作用类似于一个转换器:GUI上你可以输入、编辑某个属性然后经过它转换成合适的类型。
此接口提供的方法挺多的,和本文类型转换有关的最多只有4个:
void setValue(Object value)
:设置属性值Object getValue()
:获取属性值String getAsText()
:输出。把属性值转换成String输出void setAsText(String text)
:输入。将String转换为属性值类型输入JDK对PropertyEditor接口提供了一个默认实现java.beans.PropertyEditorSupport
,因此我们若需扩展此接口,仅需继承此类,根据需要复写getAsText/setAsText
这两个方法即可,Spring无一例外都是这么做的。
PropertyEditor作为一个JDK原生接口,内置了一些基本实现来服务于GUI程序,如:
BooleanEditor
:将true/false字符串转换为Boolean类型IntegerEditor
:将字符串转换为Integer类型JDK内置的实现比较少(如上),功能简陋,但对于服务GUI程序来说已经够用,毕竟界面输入的只可能是字符串,并且还均是基础类型。但这对于复杂的Spring环境、以及富文本的web环境来说就不够用了,所以Spring在此基础上有所扩展,因此才有了本文来讨论。
PropertyEditor
实现的是双向类型转换:String和Object互转。调用setValue()
方法后,需要先“缓存”起来后续才能够使用(输出)。PropertyEditorSupport
为此提供了一个成员属性来做:
PropertyEditorSupport:
// 调用setValue()方法对此属性赋值 getValue()方法取值
private Object value;
这么一来PropertyEditorSupport
就是有状态的了,因此是线程不安全的。在使用过程中需要特别注意,避免出现并发风险。
❝说明:Support类里还看到属性监听器
❞PropertyChangeListener
,因它属于GUI程序使用的组件,与我们无关,所以后续丝毫不会提及哦
Spring内置的「所有扩展」均是基于PropertyEditorSupport来实现的,因此也「都是」线程不安全的哦~
官方的javadoc都说得很清楚:PropertyEditor设计是为GUI程序服务的,那么Spring为何看上它了呢?
试想一下:那会的Spring只能支持xml方式配置,而XML属于文本类型配置,因此在给某个属性设定值的时候,书写上去的**100%**是个字符串,但是此属性对应的类型却不一定是字符串,可能是任意类型。你思考下,这种场景是不是跟GUI程序(AWT)一毛一样:输入字符串,对应任意类型。
为了实现这种需求,在PropertyEditorSupport
的基础上只需要复写setAsText
和getAsText
这两个方法就行,然后Spring就这么干了。我个人yy
一下,当初Spring选择这么干而没自己另起炉灶的原因可能有这么几个:
Spring为了扩展自身功能,提高配置灵活性,扩展出了非常非常多的PropertyEditor
实现,共计40余个,部分截图如下:
PropertyEditor | 功能 | 举例 |
---|---|---|
ZoneIdEditor | 转为java.time.ZoneId | Asia/Shanghai |
URLEditor | 转为URL,支持传统方式file:,http: ,也支持Spring风格:classpath:,context上下文相对路径 等等 | http://www.baidu.com |
StringTrimmerEditor | trim()字符串,也可删除指定字符char | 任意字符串 |
StringArrayPropertyEditor | 转为字符串数组 | A,B,C |
PropertiesEditor | 转为Properties | name = YourBatman |
PatternEditor | 转为Pattern | (\D*)(\d+)(.*) |
PathEditor | 转为java.nio.file.Path。支持传统URL和Spring风格的url | classpath:xxx |
ClassEditor | 转为Class | 全类名 |
CustomBooleanEditor | 转为Boolean | 见示例 |
CharsetEditor | 转为Charset | 见示例 |
CustomDateEditor | 转为java.util.Date | 见示例 |
Spring把实现基本(大多数)都放在org.springframework.beans.propertyeditors
包下,接下来我挑选几个代表性API举例说明。
@Test
public void test1() {
PropertyEditor editor = new CustomBooleanEditor(true);
// 这些都是true,不区分大小写
editor.setAsText("trUe");
System.out.println(editor.getAsText());
editor.setAsText("on");
System.out.println(editor.getAsText());
editor.setAsText("yes");
System.out.println(editor.getAsText());
editor.setAsText("1");
System.out.println(editor.getAsText());
// 这些都是false(注意:null并不会输出为false,而是输出空串)
editor.setAsText(null);
System.out.println(editor.getAsText());
editor.setAsText("fAlse");
System.out.println(editor.getAsText());
editor.setAsText("off");
System.out.println(editor.getAsText());
editor.setAsText("no");
System.out.println(editor.getAsText());
editor.setAsText("0");
System.out.println(editor.getAsText());
// 报错
editor.setAsText("2");
System.out.println(editor.getAsText());
}
关注点:对于Spring来说,传入的「true、on、yes、1」等都会被“翻译”成true(字母不区分大小写),大大提高兼容性。
❝现在知道为啥你用Postman传个1,用Bool值也能正常封装进去了吧,就是它的功劳。此效果等同于转换器
❞StringToBooleanConverter
,将在后面进行讲述对比
@Test
public void test2() {
// 虽然都行,但建议你规范书写:UTF-8
PropertyEditor editor = new CharsetEditor();
editor.setAsText("UtF-8");
System.out.println(editor.getAsText()); // UTF-8
editor.setAsText("utF8");
System.out.println(editor.getAsText()); // UTF-8
}
关注点:utf-8中间的横杠可要可不要,但建议加上使用标准写法,另外字母也是不区分大小写的。
@Test
public void test3() {
PropertyEditor editor = new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"),true);
editor.setAsText("2020-11-30 09:10:10");
System.out.println(editor.getAsText()); // 2020-11-30 09:10:10
// null输出空串
editor.setAsText(null);
System.out.println(editor.getAsText());
// 报错
editor.setAsText("2020-11-30");
System.out.println(editor.getAsText());
}
关注点:这个时间/日期转换器很不好用,构造时就必须指定一个SimpleDateFormat
格式化器。在实际应用中,Spring并没有使用到它,而是用后面会说到的替代方案。
把没有放在org.springframework.beans.propertyeditors
包下的实现称作特殊实现(前者称为标准实现)。
MediaType.parseMediaType(text)
,可以把诸如text/html、application/json转为MediaType对象formatter.parse()
去完成,反向委托给formatter.print()
去完成。DataBinder#addCustomFormatter()
得到应用Resource
资源,特别实用。作为基础设施,广泛被用到Spring的很多地方FileEditor、InputSourceEditor、InputStreamEditor、URLEditor
等等与资源相关的转换器,均依赖它而实现@Test
public void test4() {
// 支持标准URL如file:C:/myfile.txt,也支持classpath:myfile.txt
// 同时还支持占位符形式
PropertyEditor editor = new ResourceEditor(new DefaultResourceLoader(), new StandardEnvironment(), true);
// file:形式本处略
// editor.setAsText("file:...");
// System.out.println(editor.getAsText());
// classpath形式(注意:若文件不存在不会报错,而是输出null)
editor.setAsText("classpath:app.properties");
System.out.println(editor.getAsText()); // 输出带盘符的全路径
System.setProperty("myFile.name", "app.properties");
editor.setAsText("classpath:${myFile.name}");
System.out.println(editor.getAsText()); // 结果同上
}
关注点:Spring扩展出来的Resource不仅自持常规file:
资源协议,还支持平时使用最多的classpath:
协议,可谓非常好用。
ConversionService
转换服务适配为一个PropertyEditor
,内部转换动作都委托给前者去完成。AbstractPropertyBindingResult#findEditor()
为属性寻找合适PropertyEditor的时候,若ConversionService能支持就包装为ConvertingPropertyEditorAdapter供以使用,这是适配器模式的典型应用场景。考虑到阅读的舒适性,单篇文章不宜太长,因此涉及到PropertyEditor
的这几个问题,放在下篇文章单独列出。这几个问题明显比本文更深入,欢迎保持持续关注,peace!
本文主要介绍了三点内容:
PropertyEditor虽然已经很古老,不适合当下复杂环境。但不可否认它依旧有存在的价值,Spring内部也大量的仍在使用,因此「不容忽视」。下篇文章将深度探讨Spring内部是如何使用PropertyEditor的,赋予了它哪些机制,以及最终为何还是决定自己另起炉灶搞一套呢?欢迎对本系列保持持续关注~
interface21
,它便就是Spring的前身截止到当前,Spring Framework的最新版本是5.3.x
版本,此版本是5.x的最后一个主要功能分支,下个版本将是6.x喽,咱们拭目以待。
关注A哥,开启专栏式学习
扫码关注后,回复“专栏”进入更多Spring专栏学习
右侧为私人微信(加好友备注:Java入群)
个人站点(本文已收录):https://www.yourbatman.cn