2. Spring早期類型轉換,基於PropertyEditor實現

青年時種下什麼,老年時就收穫什麼。關注公衆號【BAT的烏托邦】,有Spring技術棧、MyBatis、JVM、中間件等小而美的原創專欄供以免費學習。分享、成長,拒絕淺嘗輒止。本文已被 https://www.yourbatman.cn 收錄。

✍前言

你好,我是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實現的轉換器並未廢棄且還在發揮餘熱中,因此本文就針對其早期類型轉換實現做出專文講解。

版本約定

  • Spring Framework:5.3.1
  • Spring Boot:2.4.0

說明:版本均於2020-11發佈,且版本號均不帶有.RELEASE後綴,這和之前是不一樣的。具體原因請參考:Spring改變版本號命名規則:此舉對非英語國家很友好

✍正文

若你用當下的眼光去看Spring基於PropertyEditor的類型轉換實現,會發現這麼搞是存在一些設計缺陷的。當然並不能這麼去看,畢竟現在都2020年了,那會才哪跟哪呢。既然Spring裏的PropertyEditor現如今依然健在,那咱就會會它唄。

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上你可以輸入、編輯某個屬性然後經過它轉換成合適的類型。
Java GUI程序

此接口提供的方法挺多的,和本文類型轉換有關的最多隻有4個:

  1. void setValue(Object value):設置屬性值
  2. Object getValue():獲取屬性值
  3. String getAsText():輸出。把屬性值轉換成String輸出
  4. void setAsText(String text):輸入。將String轉換爲屬性值類型輸入

JDK對PropertyEditor接口提供了一個默認實現java.beans.PropertyEditorSupport,因此我們若需擴展此接口,僅需繼承此類,根據需要複寫getAsText/setAsText這兩個方法即可,Spring無一例外都是這麼做的。

PropertyEditor作爲一個JDK原生接口,內置了一些基本實現來服務於GUI程序,如:

  • BooleanEditor:將true/false字符串轉換爲Boolean類型
  • IntegerEditor:將字符串轉換爲Integer類型
    • 同類別的還有LongEditor、FloatEditor...

JDK內置的實現比較少(如上),功能簡陋,但對於服務GUI程序來說已經夠用,畢竟界面輸入的只可能是字符串,並且還均是基礎類型。但這對於複雜的Spring環境、以及富文本的web環境來說就不夠用了,所以Spring在此基礎上有所擴展,因此纔有了本文來討論。

注意:PropertyEditorSupport線程不安全

PropertyEditor實現的是雙向類型轉換:String和Object互轉。調用setValue()方法後,需要先“緩存”起來後續才能夠使用(輸出)。PropertyEditorSupport爲此提供了一個成員屬性來做:

PropertyEditorSupport:

    // 調用setValue()方法對此屬性賦值   getValue()方法取值
	private Object value;

這麼一來PropertyEditorSupport就是有狀態的了,因此是線程不安全的。在使用過程中需要特別注意,避免出現併發風險。

說明:Support類裏還看到屬性監聽器PropertyChangeListener,因它屬於GUI程序使用的組件,與我們無關,所以後續絲毫不會提及哦

Spring內置的所有擴展均是基於PropertyEditorSupport來實現的,因此也都是線程不安全的哦~

Spring爲何基於它擴展?

官方的javadoc都說得很清楚:PropertyEditor設計是爲GUI程序服務的,那麼Spring爲何看上它了呢?

試想一下:那會的Spring只能支持xml方式配置,而XML屬於文本類型配置,因此在給某個屬性設定值的時候,書寫上去的100%是個字符串,但是此屬性對應的類型卻不一定是字符串,可能是任意類型。你思考下,這種場景是不是跟GUI程序(AWT)一毛一樣:輸入字符串,對應任意類型。

爲了實現這種需求,在PropertyEditorSupport的基礎上只需要複寫setAsTextgetAsText這兩個方法就行,然後Spring就這麼幹了。我個人yy一下,當初Spring選擇這麼幹而沒自己另起爐竈的原因可能有這麼幾個:

  1. 本着不重複發明輪子的原則,有得用就直接用唄,況且是100%滿足要求的
  2. 示好Java EE技術。畢竟那會Spring地位還並不穩,有大腿就先榜上
  3. 2003年左右,Java GUI程序還並未退出歷史舞臺,Spring爲了通用性就選擇基於它擴展嘍
    1. 說明:那會的通用性可能和現在通用性含義上是不一樣的,需要稍作區別

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舉例說明。

標準實現示例

  • CustomBooleanEditor
@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,將在後面進行講述對比

  • CharsetEditor
@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中間的橫槓可要可不要,但建議加上使用標準寫法,另外字母也是不區分大小寫的。

  • CustomDateEditor
@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包下的實現稱作特殊實現(前者稱爲標準實現)。

  • MediaTypeEditor:位於org.springframework.http。依賴的核心API是MediaType.parseMediaType(text),可以把諸如text/html、application/json轉爲MediaType對象
    • 顯然它屬於spring-web包,使用在web環境下
  • FormatterPropertyEditorAdapter:位於org.springframework.format.support。將3.0新增的Formatter接口適配爲一個PropertyEditor:setAsText這種轉換操作委託給formatter.parse()去完成,反向委託給formatter.print()去完成。
    • 此適配器在DataBinder#addCustomFormatter()得到應用
  • PropertyValuesEditor:位於org.springframework.beans。將k-v字符串(Properties格式)轉換爲一個PropertyValues,從而方便放進Environment裏
  • ResourceEditor:位於org.springframework.core.io。此轉換器將String轉換爲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:協議,可謂非常好用。

  • ConvertingPropertyEditorAdapter:位於org.springframework.core.convert.support。將3.0新增的ConversionService轉換服務適配爲一個PropertyEditor,內部轉換動作都委託給前者去完成。
    • AbstractPropertyBindingResult#findEditor()爲屬性尋找合適PropertyEditor的時候,若ConversionService能支持就包裝爲ConvertingPropertyEditorAdapter供以使用,這是適配器模式的典型應用場景。

“誰”在使用ProertyEditor

PropertyEditor自動發現機制

PropertyEditor存在的缺陷

考慮到閱讀的舒適性,單篇文章不宜太長,因此涉及到PropertyEditor的這幾個問題,放在下篇文章單獨列出。這個幾個問題會明顯比本文更深入,歡迎保持持續關注,peace!

✍總結

本文主要介紹了三點內容:

  1. PropertyEditor是什麼?
  2. Spring爲何選擇基於PropertyEditor?
  3. Spring內建的那些PropertyEditor都有哪些,各自什麼作用?

PropertyEditor雖然已經很古老,不適合當下複雜環境。但不可否認它依舊有存在的價值,Spring內部也大量的仍在使用,因此不容忽視。下篇文章將深度探討Spring內部是如何使用PropertyEditor的,賦予了它哪些機制,以及最終爲何還是決定自己另起爐竈搞一套呢?歡迎對本系列保持持續關注~


附:Spring主要版本發佈時間和特性

  • 2002-02,開始開發孵化此項目,項目名叫:interface21,它便就是Spring的前身
  • 2004-03,1.0版發佈。進入迅速發展期
  • 2006-10,2.0版發佈。支持可擴展的xml配置功能、支持Java5、支持動態語言、支持更多擴展點
  • 2007-11,2.5版發佈。項目名從此改爲Spring Source,支持Java 6,支持註解配置(部分)
  • 2009-12,3.0版發佈。這是非常非常重要的一個版本,支持了模塊驅動、支持SpEL、支持Java Bean配置、支持嵌入式數據庫......
  • 2011和2012,這兩年發佈了非常多的3.x系列小版本,帶來了很多驚喜,同時也讓Spring更加紮實
  • 2013-12,4.0版發佈。這是有一次進步,提供了對Java 8的全面支持,支持Java EE 7、支持websocket、泛型依賴注入......
  • 2017-09,5.0版發佈。最低JDK版本要求是Java 8,同時支持Servlet 3.1。當然最爲重要的是引入了全新模塊:WebFlux

截止到當前,Spring Framework的最新版本是5.3.x版本,此版本是5.x的最後一個主要功能分支,下個版本將是6.x嘍,咱們拭目以待。


✔推薦閱讀:
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章