http://www.iteye.com/topic/1123628
在Spring配置文件裏,我們往往通過字面值爲Bean各種類型的屬性提供設置值:不管是double類型還是int類型,在配置文件中都對應字符串類型的字面值。BeanWrapper填充Bean屬性時如何將這個字面值轉換爲對應的double或int等內部類型呢?我們可以隱約地感覺到一定有一個轉換器在其中起作用,這個轉換器就是屬性編輯器。
“屬性編輯器”這個名字可能會讓人誤以爲是一個帶用戶界面的輸入器,其實屬性編輯器不一定非得有用戶界面,任何實現java.beans.PropertyEditor接口的類都是屬性編輯器。屬性編輯器的主要功能就是將外部的設置值轉換爲JVM內部的對應類型,所以屬性編輯器其實就是一個類型轉換器。
PropertyEditor是JavaBean規範定義的接口,JavaBean規範中還有其他一些PropertyEditor配置的接口。爲了徹底理解屬性編輯器,必須對JavaBean中有關屬性編輯器的規範進行學習,相信這些知識對學習和掌握Spring中的屬性編輯器會大有幫助。
JavaBean的編輯器
Sun所制定的JavaBean規範,很大程度上是爲IDE準備的——它讓IDE能夠以可視化的方式設置JavaBean的屬性。如果在IDE中開發一個可視化應用程序,我們需要通過屬性設置的方式對組成應用的各種組件進行定製,IDE通過屬性編輯器讓開發人員使用可視化的方式設置組件的屬性。
一般的IDE都支持JavaBean規範所定義的屬性編輯器,當組件開發商發佈一個組件時,它往往將組件對應的屬性編輯器捆綁發行,這樣開發者就可以在IDE環境下方便地利用屬性編輯器對組件進行定製工作。
JavaBean規範通過java.beans.PropertyEditor定義了設置JavaBean屬性的方法,通過BeanInfo描述了JavaBean哪些屬性是可定製的,此外還描述了可定製屬性與PropertyEditor的對應關係。
BeanInfo與JavaBean之間的對應關係,通過兩者之間規範的命名確立:對應JavaBean的BeanInfo採用如下的命名規範:<Bean>BeanInfo。如ChartBean對應的BeanInfo爲ChartBeanBeanInfo;Car對應的BeanInfo爲CarBeanInfo。當JavaBean連同其屬性編輯器相同的組件註冊到IDE中後,當在開發界面中對JavaBean進行定製時,IDE就會根據JavaBean規範找到對應的BeanInfo,再根據BeanInfo中的描述信息找到JavaBean屬性描述(是否開放、使用哪個屬性編輯器),進而爲JavaBean生成特定開發編輯界面。
JavaBean規範提供了一個管理默認屬性編輯器的管理器:PropertyEditorManager,該管理器內保存着一些常見類型的屬性編輯器,如果某個JavaBean的常見類型屬性沒有通過BeanInfo顯式指定屬性編輯器,IDE將自動使用PropertyEditorManager中註冊的對應默認屬性編輯器。
由於JavaBean對應的屬性編輯器等IDE環境相關的資源和組件需要動態加載,所以在純Java的IDE中開發基於組件的應用時,總會感覺IDE反應很遲鈍,不像Delphi、C++Builder一樣靈敏快捷。但在Eclipse開發環境中,設計包括可視化組件的應用時卻很快捷,原因是Eclipse沒有使用Java的標準用戶界面組件庫,當然也就沒有按照JavaBean的規範開發設計GUI組件了。
PropertyEditor
PropertyEditor是屬性編輯器的接口,它規定了將外部設置值轉換爲內部JavaBean屬性值的轉換接口方法。PropertyEditor主要的接口方法說明如下:
- Object getValue():返回屬性的當前值。基本類型被封裝成對應的封裝類實例;
- void setValue(Object newValue):設置屬性的值,基本類型以封裝類傳入;
- String getAsText():將屬性對象用一個字符串表示,以便外部的屬性編輯器能以可視化的方式顯示。缺省返回null,表示該屬性不能以字符串表示;
- void setAsText(String text):用一個字符串去更新屬性的內部值,這個字符串一般從外部屬性編輯器傳入;
- String[] getTags():返回表示有效屬性值的字符串數組(如boolean屬性對應的有效Tag爲true和false),以便屬性編輯器能以下拉框的方式顯示出來。缺省返回null,表示屬性沒有匹配的字符值有限集合;
- String getJavaInitializationString():爲屬性提供一個表示初始值的字符串,屬性編輯器以此值作爲屬性的默認值。
可以看出PropertyEditor接口方法是內部屬性值和外部設置值的溝通橋樑。此外,我們可以很容易地發現該接口的很多方法是專爲IDE中的可視化屬性編輯器提供的:如getTags()、getJavaInitializationString()以及另外一些我們未此介紹的接口方法。
Java爲PropertyEditor提供了一個方便類:PropertyEditorSupport,該類實現了PropertyEditor接口並提供默認實現,一般情況下,用戶可以通過擴展這個方便類設計自己的屬性編輯器。
BeanInfo
BeanInfo主要描述了JavaBean哪些屬性可以編輯以及對應的屬性編輯器,每一個屬性對應一個屬性描述器PropertyDescriptor。PropertyDescriptor的構造函數有兩個入參:
PropertyDescriptor(String propertyName, Class beanClass) ,其中propertyName爲屬性名;而beanClass爲JavaBean對應的Class。
此外PropertyDescriptor還有一個setPropertyEditorClass(Class propertyEditorClass)方法,爲JavaBean屬性指定編輯器。BeanInfo接口最重要的方法就是:PropertyDescriptor[] getPropertyDescriptors() ,該方法返回JavaBean的屬性描述器數組。
BeanInfo接口有一個常用的實現類:SimpleBeanInfo,一般情況下,可以通過擴展SimpleBeanInfo實現自己的功能。
一個實例
在本節中,我們來看一個具體屬性編輯器的實例,該實例根據《Core Java Ⅱ》上的一個例子改編而成。
ChartBean是一個可定製圖表組件,允許通過屬性的設置定製圖表的樣式以得到滿足各種不同使用場合要求的圖表。我們忽略ChartBean的其他屬性,僅關注其中的兩個屬性:
代碼清單5-2 CharBean
- public class ChartBean extends JPanel{
- private int titlePosition = CENTER;
- private boolean inverse;
- //省略get/setter方法
- }
public class ChartBean extends JPanel{
private int titlePosition = CENTER;
private boolean inverse;
//省略get/setter方法
}
下面,我們爲titlePosition屬性提供一個屬性編輯器。我們不去直接實現PropertyEditor,而是通過擴展PropertyEditorSupport這個方便類來定義我們的屬性編輯器:
代碼清單5-3 TitlePositionEditor
- import java.beans.;
- public class TitlePositionEditor extends PropertyEditorSupport{
- private String[] options = { “Left”, “Center”, “Right” };
- //①代表可選屬性值的字符串標識數組
- public String[] getTags() { return options; }
- //②代表屬性初始值的字符串
- public String getJavaInitializationString() { return “” + getValue(); }
- //③將內部屬性值轉換爲對應的字符串表示形式,供屬性編輯器顯示之用
- public String getAsText(){
- int value = (Integer) getValue();
- return options[value];
- }
- //④將外部設置的字符串轉換爲內部屬性的值
- public void setAsText(String s){
- for (int i = 0; i < options.length; i++){
- if (options[i].equals(s)){
- setValue(i);
- return;
- }
- }
- }
- }
import java.beans.;
public class TitlePositionEditor extends PropertyEditorSupport{
private String[] options = { “Left”, “Center”, “Right” };
//①代表可選屬性值的字符串標識數組
public String[] getTags() { return options; }
//②代表屬性初始值的字符串
public String getJavaInitializationString() { return "" + getValue(); }
//③將內部屬性值轉換爲對應的字符串表示形式,供屬性編輯器顯示之用
public String getAsText(){
int value = (Integer) getValue();
return options[value];
}
//④將外部設置的字符串轉換爲內部屬性的值
public void setAsText(String s){
for (int i = 0; i < options.length; i++){
if (options[i].equals(s)){
setValue(i);
return;
}
}
}
}
①處通過getTags()方法返回一個字符串數組,因此在IDE中該屬性對應的編輯器將自動提供一個下拉框,下拉框中包含3個可選項:“Left”、“Center”、“Right”。而③和④處的兩個方法分別完成屬性值到字符串的雙向轉換功能。CharBean的inverse屬性也有一個相似的編輯器InverseEditor,我們忽略不講。
下面編寫ChartBean對應的BeanInfo,根據JavaBean的命名規範,這個BeanInfo應該命名爲ChartBeanBeanInfo,它負責將屬性編輯器和ChartBean的屬性掛鉤起來:
代碼清單5-4 ChartBeanBeanInfo
- import java.beans.;
- public class ChartBeanBeanInfo extends SimpleBeanInfo{
- public PropertyDescriptor[] getPropertyDescriptors() {
- try{
- //①將TitlePositionEditor綁定到ChartBean的titlePosition屬性中
- PropertyDescriptor titlePositionDescriptor
- = new PropertyDescriptor(“titlePosition”, ChartBean.class);
- titlePositionDescriptor.setPropertyEditorClass(TitlePositionEditor.class);
- //②將InverseEditor綁定到ChartBean的inverse屬性中
- PropertyDescriptor inverseDescriptor
- = new PropertyDescriptor(“inverse”, ChartBean.class);
- inverseDescriptor.setPropertyEditorClass(InverseEditor.class);
- return new PropertyDescriptor[]{titlePositionDescriptor, inverseDescriptor};
- }
- catch (IntrospectionException e){
- e.printStackTrace();
- return null;
- }
- }
- }
import java.beans.;
public class ChartBeanBeanInfo extends SimpleBeanInfo{
public PropertyDescriptor[] getPropertyDescriptors() {
try{
//①將TitlePositionEditor綁定到ChartBean的titlePosition屬性中
PropertyDescriptor titlePositionDescriptor
= new PropertyDescriptor(“titlePosition”, ChartBean.class);
titlePositionDescriptor.setPropertyEditorClass(TitlePositionEditor.class);
//②將InverseEditor綁定到ChartBean的inverse屬性中
PropertyDescriptor inverseDescriptor
= new PropertyDescriptor(“inverse”, ChartBean.class);
inverseDescriptor.setPropertyEditorClass(InverseEditor.class);
return new PropertyDescriptor[]{titlePositionDescriptor, inverseDescriptor};
}
catch (IntrospectionException e){
e.printStackTrace();
return null;
}
}
}
在ChartBeanBeanInfo中,我們分別爲ChartBean和titlePosition和inverse屬性指定對應的屬性編輯器。將ChartBean連同屬性編輯器以及ChartBeanBeanInfo打成JAR包,使用IDE組件擴展管理功能註冊到IDE中。這樣,我們就可以像使用TextField、Checkbox等這些組對ChartBean進行可視化的開發設計工作了。下面是ChartBean在NetBeans IDE中的屬性編輯器效果圖,如圖5-5所示。
ChartBean可設置的屬性都列在屬性查看器中,當單擊titlePosition屬性時,下拉框中列出了我們提供的3個選項。
Spring默認屬性編輯器
Spring的屬性編輯器和傳統的用於IDE開發時的屬性編輯器不同,它們沒有UI界面,僅負責將配置文件中的文本配置值轉換爲Bean屬性的對應值,所以Spring的屬性編輯器並非傳統意義上的JavaBean屬性編輯器。
Spring爲常見的屬性類型提供了默認的屬性編輯器。從圖5-4中,我們可以看出BeanWrapperImpl類擴展了PropertyEditorRegistrySupport類,Spring在PropertyEditor RegistrySupport中爲常見屬性類型提供了默認的屬性編輯器,這些“常見的類型”共32個,可分爲3大類,總結如下:
表5-1 Spring提供的默認屬性編輯器
類 別 | 說 明 | |
基礎數據類型 | 分爲幾個小類: 1)基本數據類型,如:boolean、byte、short、int等; 2)基本數據類型封裝類,如:Long、Character、Integer等; 3)兩個基本數據類型的數組,char[]和byte[]; 4)大數類,BigDecimal和BigInteger | |
集合類 | 爲5種類型的集合類Collection、Set、SortedSet、List和SortedMap提供了編輯器 | |
資源類 | 用於訪問外部資源的8個常見類Class、Class[]、File、InputStream、Locale、Properties、Resource[]和URL |
PropertyEditorRegistrySupport中有兩個用於保存屬性編輯器的Map類型變量:
- defaultEditors:用於保存默認屬性類型的編輯器,元素的鍵爲屬性類型,值爲對應的屬性編輯器實例;
- customEditors:用於保存用戶自定義的屬性編輯器,元素的鍵值和defaultEditors相同。
PropertyEditorRegistrySupport通過類似以下的代碼定義默認屬性編輯器:
- this.defaultEditors.put(char.class, new CharacterEditor(false));
- this.defaultEditors.put(Character.class, new CharacterEditor(true));
- this.defaultEditors.put(Locale.class, new LocaleEditor());
- this.defaultEditors.put(Properties.class, new PropertiesEditor());
this.defaultEditors.put(char.class, new CharacterEditor(false));
this.defaultEditors.put(Character.class, new CharacterEditor(true));
this.defaultEditors.put(Locale.class, new LocaleEditor());
this.defaultEditors.put(Properties.class, new PropertiesEditor());
這些默認的屬性編輯器解決常見屬性類型的註冊問題,如果用戶的應用包括一些特殊類型的屬性,且希望在配置文件中以字面值提供配置值,那麼就需要編寫自定義屬性編輯器並註冊到Spring容器中。這樣,Spring才能將配置文件中的屬性配置值轉換爲對應的屬性類型值。
自定義屬性編輯器
Spring大部分默認屬性編輯器都直接擴展於java.beans.PropertyEditorSupport類,用戶也可以通過擴展PropertyEditorSupport實現自己的屬性編輯器。比起用於IDE環境的屬性編輯器來說,Spring環境下使用的屬性編輯器的功能非常單一:僅需要將配置文件中字面值轉換爲屬性類型的對象即可,並不需要提供UI界面,因此僅需要簡單覆蓋PropertyEditorSupport的setAsText()方法就可以了。
一個實例
我們繼續使用第4章中Boss和Car的例子,假設我們現在希望在配置Boss時,不通過引用Bean的方式注入Boss的car屬性,而希望直接通過字符串字面值提供配置。爲了方便閱讀,這裏再次列出Boss和Car類的簡要代碼:
代碼清單5-5 Car
- package com.baobaotao.editor;
- public class Car {
- private int maxSpeed;
- public String brand;
- private double price;
- //省略get/setter
- }
package com.baobaotao.editor;
public class Car {
private int maxSpeed;
public String brand;
private double price;
//省略get/setter
}
代碼清單5-6 Boss
- package com.baobaotao.editor;
- public class Boss {
- private String name;
- private Car car = new Car();
- //省略get/setter
- }
package com.baobaotao.editor;
public class Boss {
private String name;
private Car car = new Car();
//省略get/setter
}
Boss有兩個屬性:name和car,分別對應String類型和Car類型。Spring擁有String類型的默認屬性編輯器,因此對於String類型的屬性我們不用操心。但Car類型是我們自定義的類型,要配置Boss的car屬性,有兩種方案:
- 1)在配置文件中爲car專門配置一個<bean>,然後在boss的<bean>中通過ref引用car Bean,這正是我們上一章中所用的方法;
- 2)爲Car類型提供一個自定義的屬性編輯器,這樣,我們就通過字面值爲Boss的car屬性提供配置值。
第一種方案是常用的方法,但是在有些情況下,這種方式需要將屬性對象一步步肢解爲最終可以用基本類型表示的Bean,使配置文件變得不夠清晰,直接爲屬性類提供一個對應的自定義屬性編輯器可能會是更好的替代方案。
現在,我們來爲Car編寫一個自定義的屬性編輯器,其代碼如下所示:
代碼清單5-7 CustomCarEditor
- package com.baobaotao.editor;
- import java.beans.PropertyEditorSupport;
- public class CustomCarEditor extends PropertyEditorSupport {
- //①將字面值轉換爲屬性類型對象
- public void setAsText(String text){
- if(text == null || text.indexOf(“,”) == -1){
- throw new IllegalArgumentException(“設置的字符串格式不正確”);
- }
- String[] infos = text.split(”,”);
- Car car = new Car();
- car.setBrand(infos[0]);
- car.setMaxSpeed(Integer.parseInt(infos[1]));
- car.setPrice(Double.parseDouble(infos[2]));
- //②調用父類的setValue()方法設置轉換後的屬性對象
- setValue(car);
- }
- }
package com.baobaotao.editor;
import java.beans.PropertyEditorSupport;
public class CustomCarEditor extends PropertyEditorSupport {
//①將字面值轉換爲屬性類型對象
public void setAsText(String text){
if(text == null || text.indexOf(",") == -1){
throw new IllegalArgumentException("設置的字符串格式不正確");
}
String[] infos = text.split(",");
Car car = new Car();
car.setBrand(infos[0]);
car.setMaxSpeed(Integer.parseInt(infos[1]));
car.setPrice(Double.parseDouble(infos[2]));
//②調用父類的setValue()方法設置轉換後的屬性對象
setValue(car);
}
}
CustomCarEditor很簡單,它僅覆蓋PropertyEditorSupport便利類的setAsText(String text)方法,該方法負責將配置文件以字符串提供的字面值轉換爲Car對象。字面值採用逗號分隔的格式同時爲brand、maxSpeed和price屬性值提供設置值,setAsText()方法解析這個字面值並生成對應的Car對象。由於我們並不需要將Boss內部的car屬性反顯到屬性編輯器中,因此不需要覆蓋getAsText()方法。
註冊自定義的屬性編輯器
在IDE環境下,自定義屬性編輯器在使用之前必須通過擴展組件功能進行註冊,在Spring環境中也需要通過一定的方法註冊自定義的屬性編輯器。
如果使用BeanFactory,用戶需要手工調用registerCustomEditor(Class requiredType, PropertyEditor propertyEditor)方法註冊自定義屬性編輯器;如果使用ApplicationContext,則只需要在配置文件通過CustomEditorConfigurer註冊就可以了。CustomEditorConfigurer實現BeanFactoryPostProcessor接口,因此是一個Bean工廠後處理器。我們知道Bean工廠後處理器在Spring容器加載配置文件並生成BeanDefinition半成品後就會被自動執行。因此CustomEditorConfigurer有容器啓動時有機會注入自定義的屬性編輯器。下面的配置片斷定義了一個CustomEditorConfigurer:
- <!–①配置自動註冊屬性編輯器的CustomEditorConfigurer –>
- <bean class=“org.springframework.beans.factory.config.CustomEditorConfigurer”>
- <property name=“customEditors”>
- <map>
- <!–②-1屬性編輯器對應的屬性類型–>
- <entry key=“com.baobaotao.editor.Car”>
- <!–②-2對應的屬性編輯器Bean –>
- <bean class=“com.baobaotao.editor.CustomCarEditor” />
- </entry>
- </map>
- </property>
- </bean>
- <bean id=“boss” class=“com.baobaotao.editor.Boss”>
- <property name=“name” value=“John”/>
- <!–③該屬性將使用②處的屬性編輯器完成屬性填充操作–>
- <property name=“car” value=“紅旗CA72,200,20000.00”/>
- </bean>
<!–①配置自動註冊屬性編輯器的CustomEditorConfigurer –>
<bean class=”org.springframework.beans.factory.config.CustomEditorConfigurer”>
<property name=”customEditors”>
<map>
<!–②-1屬性編輯器對應的屬性類型–>
<entry key=”com.baobaotao.editor.Car”>
<!--②-2對應的屬性編輯器Bean -->
<bean class="com.baobaotao.editor.CustomCarEditor" />
</entry>
</map>
</property>
</bean>
<bean id=”boss” class=”com.baobaotao.editor.Boss”>
<property name=”name” value=”John”/>
<!–③該屬性將使用②處的屬性編輯器完成屬性填充操作–>
<property name=”car” value=”紅旗CA72,200,20000.00”/>
</bean>
在①處,我們定義了用於註冊自定義屬性編輯器的CustomEditorConfigurer,Spring容器將通過反射機制自動調用這個Bean。CustomEditorConfigurer通過一個Map屬性定義需要自動註冊的自定義屬性編輯器。在②處,我們爲Car類型指定了對應屬性編輯器CustomCarEditor,注意鍵是屬性類型,而值是對應的屬性編輯器Bean,而不是屬性編輯器的類名。
最精彩的部分當然是③處的配置,我們原來通過一個<bean>元素標籤配置好car Bean,然後在boss的<bean>中通過ref引用car Bean,但是現在我們直接通過value爲car屬性提供配置。BeanWrapper在設置boss的car屬性時,它將檢索自定義屬性編輯器的註冊表,當發現Car屬性類型擁有對應的屬性編輯器CustomCarEditor時,它就會利用CustomCarEditor將“紅旗CA72,200,20000.00”轉換爲Car對象。
如com.baobaotao.domain.UserEditor會自動成爲com.baobaotao.domain.User對應的PropertyEditor。Spring也支持這個規範,也即如果採用這種規約命令PropertyEditor,就無須顯式在CustomEditorConfigurer中註冊了,Spring將自動查找並註冊這個PropertyEditor。
另:Spring 3.0除支持PropertyEditor外,還在覈心包中引入了自建的ConversionService,它提供了更爲強大的類型轉換的能力,可以完成任意類型之間的轉換,還可以在轉換過程中參考目標對象所在宿主類的上下文信息。Spring的類型轉換同時支持PropertyEdito和ConversionService。
Java原生的PropertyEditor存在以下不足:
1 只能用於字符串和Java對象的轉換,不適用於任意兩個Java類型之間的轉換;
2 對源對象及目標對象所在的上下文信息(如註解、所在宿主類的結構等)不敏感,在類型轉換時不能利用這些上下文信息實施高級轉換邏輯。
有鑑於此,Spring 3.0在覈心模型中添加了一個通用的類型轉換模塊,類型轉換模塊位於org.springframework.core.convert包中。Spring希望用這個類型轉換體系替換Java標準的PropertyEditor。但由於歷史原因,Spring將同時支持兩者,在Bean配置、Spring MVC處理方法入參綁定中使用它們。