Spring核心——字符串到實體轉換

籠統的說一個系統主要是由3個部分組成的:

  1. 執行程序:主要負責處理業務邏輯,對接用戶操作。
  2. 內部數據:嵌套在源碼中的數據,用於指導程序運行。
  3. 外部數據:業務數據,外部配置數據。

內部數據本身就是程序的一部分,在Java中這些數據通常停留在類的靜態成員變量中。而外部數據往往與代碼無關,所以對於程序而言要“讀懂”它們需要進行一些前置處理。例如用戶在前端頁面提交的數據我們從RequestContext中獲取的數據類型都是字符串,而我們的業務需要將字符串轉換成數字、列表、對象等等,這就引入了我們接下來要介紹的內容——數據類型轉換。

JavaBean對於J2SE或者J2EE而言有著非常重要的意義,ORACLE為了統一各個組織對JavaBean的使用方式制定了詳盡的JavaBean規範,包括BeanInfo、PropertyEditor、PropertyEditorSupport等方面的內容。本文會涉及到JavaBean的一些規範,但是重點是介紹Spring的數據管理。

(可執行代碼請到本人gitee庫下載,本文的代碼在chkui.springcore.example.hybrid.beanmanipulation包)

Properties結構轉換為實體

標準資源文件*.properties是Java程序常用的數據存儲文件,Spring提供了BeanWrapper接口將*.properties文件中的數據轉換成一個標準的JavaBean對象。看下面的例子:

有一個實體類Person:

class Person {
private String name;
private int age;
private boolean license;
private Date birtday;
private Address address;
private Map otherInfo;
// Getter & Setter ......
}

然後可以通過BeanWrapper將Properties對象中的數據設置到對象中:

 private void simpleDataBind() {
BeanWrapper wrapper = new BeanWrapperImpl(new Person());

//使用 BeanWrapper::setPropertyValue 接口設置數據
wrapper.setPropertyValue("name", "niubility");
wrapper.setPropertyValue("age", 18);
wrapper.setPropertyValue("license", true);
print(wrapper.getWrappedInstance());
//使用 Properties對象設置數據,Properties實例可以來源於*.properties文件
Properties p = new Properties();
p.setProperty("name", "From Properties");
p.setProperty("age", "25");
p.setProperty("license", "false");
p.setProperty("otherInfo[birthday]", "2000-01-01");
wrapper.setPropertyValues(p);
print(wrapper.getWrappedInstance());
}

這樣,使用Spring的BeanWrapper接口,可以快速的將Properties數據結構轉換為一個JavaBean實體。

除了配置單個實體的數據,BeanWrapper還可以為嵌套結構的實體設置數據。現在增加一個實體Vehicle:

public class Vehicle {
private String name;
private String manufacturer;
private Person person; //Person對象
// Getter & Setter ......
}

在Vehicle中有一個Person類型的成員變量(person域),我們可以利用下面具備嵌套結構的語法來設置數據:

 private BeanManipulationApp nestedDataBind() {
// 數據嵌套轉換
BeanWrapper wrapper = new BeanWrapperImpl(new Vehicle(new Person()));
Properties p = new Properties();
p.setProperty("name", "Envision");
p.setProperty("manufacturer", "Buick");

//person.name表示設置person域的name變量數值
p.setProperty("person.name", "Alice");
p.setProperty("person.age", "25");
p.setProperty("person.license", "true");
p.setProperty("person.otherInfo[license code]", "123456789");
wrapper.setPropertyValues(p);
print(wrapper.getWrappedInstance());
return this;
}

在*.properties文件中,經常使用path.name=param的的語法來指定一個嵌套結構(比如LOG4J的配置文件),這裡也使用類似的方式來指定嵌套結構。person.name在程序執行時會調用Vehicle::getPerson::setName方法來設定數據。

除了設定單個數據BeanWrapper還提供了更豐富的方法來設置數據,以上面的Vehicle、person為例:

表達式 效果 p.setProperty("name", "Envision") name域的數據設置為"Envision" p.setProperty("person.name", "Alice") 將嵌套的person域下的name數據設置為"Alice" p.setProperty("list[1]", "Step2") list域是一個列表,將第二個數據設置為"Step2" p.setProperty("otherInfo[birthday]", "2000-01-01") otherInfo域是一個Map,將key=birthday、value="2000-01-01"的數據添加到Map中。 上面這4條規則可以組合使用,比如p.setProperty("person.otherInfo[license code]", "123456789")。

關於在Java如何使用Properties有很多討論(比如這篇stackoverflow的問答),BeanWrapper不僅僅是針對資源文件,他還衍生擴展了數據類型轉換等等功能。

PropertyEditor轉換數據

在JavaBean規範中定義了java.beans.PropertyEditor,他的作用簡單的說就是將字符串轉換為任意對象結構。

PropertyEditor最早是用來支持java.awt中的可視化接口編輯數據的(詳情見Oracle關於IDE數據定製化的介紹)。但是在Spring或其他應用場景中更多的僅僅是用來做字符串到特定數據格式的轉換(畢竟java.awt應用不多),所以PropertyEditor提供的BeanWrapper::paintValue之類的支持awt的方法不用太去關心他,主要聚焦在BeanWrapper::setAsText方法上。

BeanWrapper繼承了PropertyEditorRegistry接口用於註冊PropertyEditor。BeanWrapperImpl已經預設了很多有價值的PropertyEditor,比如上面的例子的代碼p.setProperty("age", "25");,age域是一個數字整型,而Properties中的數據都是字符串,在設置數據時會自動啟用CustomNumberEditor將字符串轉換為數字。

Spring已經提供的PropertyEditor可以看這裡的清單。需要注意的是,這些PropertyEditor並不是每一個都默認啟用,比如CustomDateEditor必須由開發者提供DateFormat才能使用,所以需要像下面這樣將其添加註冊到BeanWrapper中:

private void propertyEditor() {
BeanWrapper wrapper = new BeanWrapperImpl(new Person());
// 設定日期轉換格式
DateFormat df = new java.text.SimpleDateFormat("yyyy-MM-dd");

// 將Editor與DateFormat進行幫頂,使用指定的格式
CustomDateEditor dateEditor = new CustomDateEditor(df, false);

// 註冊dateEditor,將其與Date類進行綁定
wrapper.registerCustomEditor(Date.class, dateEditor);
// CustomNumberEditor執行轉換
wrapper.setPropertyValue("age", "18");
// CustomBooleanEditor執行轉換
wrapper.setPropertyValue("license", "false");
// dateEditor執行轉換
wrapper.setPropertyValue("birtday", "1999-01-30");
print(wrapper.getWrappedInstance());
}

添加之後,設定setPropertyValue("birtday", "1999-01-30")時會自動使用指定的DateFormat轉換日期。

自定義PropertyEditor

除了預設的各種PropertyEditor,我們還可以開發自定義的PropertyEditor。Person中有一個類型為Address的成員變量:

public class Address {
private String province; //省
private String city; //市
private String district; //區
// Getter & Setter
}

我們為Address實體添加一個PropertyEditor,將特定格式的字符串轉換為Address結構:

public class AddressEditor extends PropertyEditorSupport {
private String[] SPLIT_FLAG = { ",", "-", ";", ":" };
public void setAsText(String text) {
int pos = -1;
Address address = new Address();
for (String flag : SPLIT_FLAG) {
pos = text.indexOf(flag);
if (-1 < pos) {
String[] split = text.split(flag);
address.setProvince(split[0]);
address.setCity(split[1]);
address.setDistrict(split[2]);
break;
}
}
if (-1 == pos) {
throw new IllegalArgumentException("地址格式錯誤");
}
setValue(address);//設定Address實例
}
}

通過AddressEditor::setAsText方法,可以將輸入的字符串最紅轉換為一個Address實例。通常情況下開發一個Editor轉換器不會直接去實現PropertyEditor接口,而是繼承PropertyEditorSupport。

然後我們使用AddressEditor來將字符串轉換為Address對象:

private BeanManipulationApp propertyEditor() {
//使用預設轉換工具和自定義轉換工具
BeanWrapper wrapper = new BeanWrapperImpl(new Person());
// 創建AddressEditor實例
AddressEditor addressEditor = new AddressEditor();

// 註冊addressEditor,將其與Address類進行綁定
wrapper.registerCustomEditor(Address.class, addressEditor);
// 設置值自動進行轉化
wrapper.setPropertyValue("address", "廣東-廣州-白雲");
print(wrapper.getWrappedInstance());
}

按照JavaBean規範,PropertyEditor和對應的JavaBean可以使用命名規則來表示綁定關係,而無需顯式的調用註冊方法。

綁定的規則是:有一個JavaBean命名為Tyre,在相同的包下(package)有一個實現了PropertyEditor接口並且命名為TyreEditor的類,那麼框架認為TyreEditor就是Tyre的Editor,無需調用BeanWrapper::registerCustomEditor方法來聲明Tyre和TyreEditor的綁定關係,詳情請看源碼中chkui.springcore.example.hybrid.beanmanipulation.bean.Tyre的使用。

IoC與數據轉換整合

對於Spring的ApplicationContext而言,BeanWrapper、PropertyEditor都是相對比較底層的功能,在使用Spring Ioc容器的時候可以直接將這些功能嵌入到Bean初始化中或MVC的requestContext的數據轉換中。

從框架使用者的角度來看,Spring的XML配置數據或者通過MVC接口傳遞數據都是字符串,因此PropertyEditor在處理這些數據時有極大的用武之地。IoC容器使用後置處理器CustomEditorConfigurer來管理Bean初始化相關的PropertyEditor。通過CustomEditorConfigurer可以使用所有預設的Editor,還可以增加自定義的Editor,下面是使用@Configurable啟用CustomEditorConfigurer的例子:

@Configurable
@ImportResource("classpath:hybrid/beanmanipulation/config.xml")
public class BeanManipulationConfig {
@Bean
CustomEditorConfigurer customEditorConfigurer() {
// 構建CustomEditorConfigurer
CustomEditorConfigurer configurer = new CustomEditorConfigurer();

Map, Class extends PropertyEditor>> customEditors = new HashMap<>();

// 添加AddressEditor和Address的綁定
customEditors.put(Address.class, AddressEditor.class);

// 添加綁定列表
configurer.setCustomEditors(customEditors);

// 通過PropertyEditorRegistrar註冊PropertyEditor
configurer.setPropertyEditorRegistrars(new PropertyEditorRegistrar[] { new DateFormatRegistrar() });
return configurer;
}
}

CustomEditorConfigurer::setCustomEditorsCustomEditorConfigurer::setPropertyEditorRegistrars都可以向容器中添加PropertyEditor,最主要區別在於:

  1. 前者是直接申明一對綁定關係的類對象(Class>),例如customEditors.put(Address.class, AddressEditor.class); 這行代碼並沒有實例化AddressEditor,而是將實例化交給後置處理器。
  2. 而後者是提供一個實例化的PropertyEditor,比前者更能實現更復雜的功能。比如下面的DateFormatRegistrar代碼,由於需要組裝DateFormat和CustomDateEditor,所以使用PropertyEditorRegistrar來實現這個過程更加合理,後置處理器會在某個時候調用這個註冊方法。
public class DateFormatRegistrar implements PropertyEditorRegistrar {
@Override
public void registerCustomEditors(PropertyEditorRegistry registry) {
DateFormat df = new java.text.SimpleDateFormat("yyyy-MM-dd");
CustomDateEditor editor = new CustomDateEditor(df, false);
registry.registerCustomEditor(Date.class, editor);
}
}

配置好CustomEditorConfigurer之後,就可以直接在配置Bean的時候直接使用預定的格式了,比如:




















此外,在Spring MVC中,可以SimpleFormController::initBinder方法將外部傳入的數據和某個Bean進行綁定:

public final class MyController extends SimpleFormController {
// 通過任何方式獲取PropertyEditorRegistrar
@Autowired
private MyPropertyEditorRegistrar editorRegistrar;
protected void initBinder(HttpServletRequest request,
ServletRequestDataBinder binder) throws Exception {
// 將Editor與當前Controller進行綁定
this.editorRegistrar.registerCustomEditors(binder);
}
}

Spring MVC並不屬於Sring核心功能範疇,這裡就不展開了,需要了解的話看看SimpleFormController的JavaDoc文檔即可。

© 著作權歸作者所有


分享到:


相關文章: