1. 程式人生 > >SpringMVC型別轉換、資料繫結詳解[附帶原始碼分析]

SpringMVC型別轉換、資料繫結詳解[附帶原始碼分析]

目錄

前言

SpringMVC是目前主流的Web MVC框架之一。 

public String method(Integer num, Date birth) {
  ...
}

Http請求傳遞的資料都是字串String型別的,上面這個方法在Controller中定義,如果該方法對應的地址接收到到瀏覽器的請求的話,並且請求中含有num和birth引數,那麼num會被自動轉換成Integer物件;birth會被自動轉為Date物件(Date轉換需要配置屬性編輯器)。

本文將分析這一原理,解釋SpringMVC是如何實現資料型別的轉換。

屬性編輯器介紹

在講解核心內容之前,我們先來了解一下Java中定義的屬性編輯器。

sun設計屬性編輯器主要是為IDE服務的,讓IDE能夠以視覺化的方式設定JavaBean的屬性。

PropertyEditor是屬性編輯器的介面。

我們使用屬性編輯器一般都是將String物件轉換成我們需要的java物件而使用的。

有個方法setAsText很重要。 比如String物件"1"要使用屬性編輯器轉換成Integer物件,通過setAsText裡Integer.parseInt(text)得到Integer物件,然後將Integer物件儲存到屬性中。

它的基本實現類是PropertyEditorSupport,一般我們要編寫自定義的屬性編輯器只需要繼承這個類即可。

Spring中有很多自定義的屬性編輯器,都在spring-beans jar包下的org.springframework.beans.propertyeditors包裡。

CustomBooleanEditor繼承PropertyEditorSupport並重寫setAsText方法。

重要介面和類介紹

 剛剛分析了sun設計的屬性編輯器。 下面我們來看下Spring對這方面的設計。

1.PropertyEditorRegistry介面

  封裝方法來給JavaBean註冊對應的屬性編輯器。

2.PropertyEditorRegistrySupport:PropertyEditorRegistry介面的基礎實現類

  

  PropertyEditorRegistrySupport類有個createDefaultEditors方法,會建立預設的屬性編輯器。

  

  

3.TypeConverter介面

  型別轉換介面。 通過該介面,可以將value轉換為requiredType型別的物件。

  

4.TypeConverterSupport:TypeConverter基礎實現類,並繼承了PropertyEditorRegistrySupport  

  有個屬性typeConverterDelegate,型別為TypeConverterDelegate,TypeConverterSupport將型別轉換委託給typeConverterDelegate操作。

5.TypeConverterDelegate

  型別轉換委託類。具體的型別轉換操作由此類完成。

6.SimpleTypeConverter

TypeConverterSupport的子類,使用了PropertyEditorRegistrySupport(父類TypeConverterSupport的父類PropertyEditorRegistrySupport)中定義的預設屬性編輯器。

7.PropertyAccessor介面

  對類中屬性操作的介面。

8.BeanWrapper介面

  繼承ConfigurablePropertyAccessor(繼承PropertyAccessor、PropertyEditorRegistry、TypeConverter介面)介面的操作Spring中JavaBean的核心介面。

9.BeanWrapperImpl類

  BeanWrapper介面的預設實現類,TypeConverterSupport是它的父類,可以進行型別轉換,可以進行屬性設定。

10.DataBinder類

  實現PropertyEditorRegistry、TypeConverter的類。支援型別轉換,引數驗證,資料繫結等功能。

  有個屬性SimpleTypeConverter,用來進行型別轉換操作。

11.WebDataBinder

  DataBinder的子類,主要是針對Web請求的資料繫結。

部分類和介面測試

由於BeanWrapper支援型別轉換,屬性設定。以BeanWrapper介面為例,做幾個測試,讓讀者對它們有更清晰的認識:

以TestModel這個JavaBean為例,屬性:

  private int age;
  private Date birth;
  private String name;
  private boolean good;
  private long times;

測試方法1:

TestModel tm = new TestModel();
BeanWrapper bw = new BeanWrapperImpl(tm);
bw.setPropertyValue("good", "on");
//bw.setPropertyValue("good", "1");
//bw.setPropertyValue("good", "true");
//bw.setPropertyValue("good", "yes");
System.out.println(tm);

good是boolean屬性,使用BeanWrapperImpl設定屬性的時候,內部會使用型別轉換(父類TypeConverterSupport提供),將String型別轉換為boolean,CustomBooleanEditor對於String值是on,1,true,yes都會轉換為true,本文介紹PropertyEditorRegistrySupport的時候說明過,CustomBooleanEditor屬於預設的屬性編輯器。

測試方法2:

TestModel tm = new TestModel();
BeanWrapperImpl bw = new BeanWrapperImpl(false);
bw.setWrappedInstance(tm);
bw.setPropertyValue("good", "1");
System.out.println(tm);

不使用預設的屬性編輯器進行型別轉換。 很明顯,這段程式碼報錯了,沒有找到合適的屬性編輯,String型別不能作為boolean型別的值。

錯誤資訊:Failed to convert property value of type 'java.lang.String' to required type 'boolean' for property 'good'; 

測試方法3:

TestModel tm = new TestModel();
BeanWrapper bw = new BeanWrapperImpl(tm);
bw.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), true));
bw.setPropertyValue("birth", "1990-01-01");
System.out.println(tm);

預設屬性編輯器中並沒有日期型別的屬性編輯器,我們註冊一個Spring提供的CustomDateEditor屬性編輯器,對應Date物件,並且為空。有了CustomDateEditor,設定birth的時候會通過型別轉換自動轉化成Date物件。

關於其他屬性的設定,讀者自行測試吧。

原始碼分析

本文所使用的Spring版本是4.0.2

在分析RequestParamMethodArgumentResolver處理請求引數之前,我們簡單回顧一下SpringMVC是如何對http請求進行處理的。

HandlerAdapter會對每個請求例項化一個ServletInvocableHandlerMethod物件進行處理,我們僅看下WebDataBinderFactory的構造過程。

WebDataBinderFactory介面是一個建立WebDataBinder的工廠介面。

以如下方法為例:

public ModelAndView test(boolean b, ModelAndView view) {
  view.setViewName("test/test");
  if(b) {
      view.addObject("attr", "b is true");
  } else {
      view.addObject("attr", "b is false");
  }
  return view;
}

下面我們進入RequestParamMethodArgumentResolver看看是如何處理的。

RequestParamMethodArgumentResolver的resolveArgument方法是由它的父類AbstractNamedValueMethodArgumentResolver中定義的:

ServletRequestDataBinderFactory建立ExtendedServletRequestDataBinder。

ExtendedServletRequestDataBinder屬於DataBinder的子類。

我們在介紹重要介面的時候說過DataBinder進行型別轉換的時候內部會使用SimpleTypeConverter進行資料轉換。

下面看看測試:

 

CustomBooleanEditor處理ohmygod會丟擲IllegalArgumentException。 最終被截獲處理成http 400錯誤。

PS:以上例子boolean型別改成Boolean型別的話,不傳引數的話b就是null,我們解釋預設屬性編輯器的時候Boolean型別的引數是允許空的。但是boolean型別不傳引數的話,預設會是false,而不會丟擲異常。 原因就是resolveArgument方法中handleNullValue處理null值,spring進行了特殊的處理,如果引數型別是boolean的話,取false。 讀者可以試試。

再看看個例子:

public ModelAndView testObj(Employee e, ModelAndView view) {
  view.setViewName("test/test");
  view.addObject("attr", e.toString());
  return view;
}

該方法會被ServletModelAttributeMethodProcessorr這個HandlerMethodArgumentResolver處理。

ServletModelAttributeMethodProcessorr的resolveArgument方法是由它的父類ModelAttributeMethodProcessor中定義的:

 這裡WebDataBinder方法bind中會使用BeanWrapper構造物件,然後設定對應的屬性。BeanWrapper本文已介紹過。

編寫自定義的屬性編輯器

Spring提供的編輯器肯定不會滿足我們日常開發的功能,因此開發自定義的屬性編輯器也是很有必要的。

下面我們就寫1個自定義的屬性編輯器。

public class CustomDeptEditor extends PropertyEditorSupport {
  
  @Override
  public void setAsText(String text) throws IllegalArgumentException { 
    if(text.indexOf(",") > 0) {
        Dept dept = new Dept();
        String[] arr = text.split(",");
        dept.setId(Integer.parseInt(arr[0]));
        dept.setName(arr[1]);
        setValue(dept);
    } else {
        throw new IllegalArgumentException("dept param is error");
    }
  }
  
}

SpringMVC中使用自定義的屬性編輯器有3種方法:

1. Controller方法中新增@InitBinder註解的方法

@InitBinder
public void initBinder(WebDataBinder binder) { 
  binder.registerCustomEditor(Dept.class, new CustomDeptEditor());  
}

2. 實現WebBindingInitializer介面

public class MyWebBindingInitializer implements WebBindingInitializer {
  
  @Override
  public void initBinder(WebDataBinder binder, WebRequest request) { 
    binder.registerCustomEditor(Dept.class, new CustomDeptEditor());  
  }
  
}

之前分析原始碼的時候,HandlerAdapter構造WebDataBinderFactory的時候,會傳遞HandlerAdapter的屬性webBindingInitializer。

因此,我們在配置檔案中構造RequestMappingHandlerAdapter的時候傳入引數webBindingInitializer。

3. @ControllerAdvice註解

@ControllerAdvice
public class InitBinderControllerAdvice {
  
  @InitBinder
  public void initBinder(WebDataBinder binder) { 
    binder.registerCustomEditor(Dept.class, new CustomDeptEditor());  
  }
  
}

加上ControllerAdvice別忘記配置檔案component-scan需要掃描到這個類。

最終結果:

總結

分析了Spring的資料轉換功能,並解釋這個神奇的轉換功能是如何實現的,之後編寫了自定義的屬性編輯器。

大致講解了下Spring型別轉換中重要的類及介面。

文章難免會出現錯誤,希望讀者能夠指出。

參考資料