一文讀懂SpringMVC中的資料繫結

Wizey發表於2018-09-22

本文是對 SpringMVC 中資料繫結的總結。

1、SpringMVC 和 Struts2 的區別

Struts2 和 SpringMVC 都是 Web 開發中檢視層的框架,兩者都實現了資料的自動繫結,都不需要我們手動獲取引數然後關聯到對應的屬性上,下面就談談兩者的區別。

  • Spring MVC 是基於方法的,通過形參接收引數;Struts2 是基於類的,通過模型驅動封裝接收引數。
  • SpringMVC 將 url 和 controller 類中的方法對映,生成一個 Handler 物件來執行 method 方法;Struts2 根據配置檔案將 url 和 action 類中的方法對映,生成 action 物件來執行 method 方法。
  • SpringMVC 形參接收引數,一個方法獨享 request response 資料,使用單例開發;Struts2 成員變數接收引數,多個方法共享成員變數,必須使用多例開發。
  • SpringMVC 的入口是 Servlet,一個方法對於一個 request 上下文,通過註解將 request 資料注入方法形參;而 Struts2 的入口是 Filter,攔截每個請求,建立一個 Action,呼叫成員變數的 getter、setter 方法將 reque 資料注入成員變數,兩者實現機制不同。
  • SpringMVC 更加輕量級,Struts2 配置很多,SpringMVC 開發效率和效能都比 Struts2 高。
  • SpringMVC 方法返回的資料更加靈活,使用 AJAX 進行 JSON 互動很方便;Struts2 的標籤資料渲染慢,不如 JSTL 標籤效能高。

這兩個框架我都用過,這裡僅是個人看法,Struts2 的配置真的是寫死人,類的限制使得使用也不夠靈活,與一些前端框架的結合也不是很方便,個人是放棄 Struts2 框架了。

2、不同型別的資料繫結

在開發中前後臺互動的資料無非是下面幾種:

  • 基本型別(int、double、Integer、String 等)
  • 物件(類)型別(自定義的實體類)
  • 日期型別(java.util.Date)
  • 複雜型別(物件陣列、List、Set、Map 等)
  • 特殊文字型別(JSON、XML 等)

下面就總結一下這些資料在 SpringMVC 中如何繫結到方法形參中。

使用 Maven 來搭建專案,所有的程式碼都已上傳到 GitHub 上,有需要的小夥伴可以前往下載,也歡迎你 star 該倉庫哦!

在給方法加上 @ResponseBody 註解後,直接將處理好的資料輸出到響應流中,沒有了試圖解析過程,也就是返回的是 JSON 型別。SpringMVC 這裡使用了介面卡模式來處理資料轉換,當我們使用 Jackson 作為解析 JSON 工具,這裡注意一個大坑,Jackson 內預設的編碼為 ISO-8859-1(大坑),這就會導致在輸出中文時亂碼,這點可以通過瀏覽器的控制檯檢視,解決方法有以下幾種。

1、在每個方法上加上編碼設定@RequestMapping(value = "basetype3.do", produces = "application/json; charset=utf-8")

2、在 SpringMVC 配置檔案中修改 Jackson 的預設編碼為 UTF-8,注意要放在 <mvc:annotation-driven/> 前面,放在內部是不生效的。

3、更改 JSON 解析工具,推薦使用阿里的 fastjson,預設編碼就是 UTF-8,解析速度也比 Jackson 快。

方法二、三詳細的配置如下:

<!--特別注意:必須放在mvc:annotation-driven前面,放在內部是不會生效的-->
    <!--json裝換器使用 Jackson 預設編碼是 ISO-8859-1 需要重新設定編碼-->
    <!--<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">-->
        <!--<property name="messageConverters">-->
            <!--<list>-->
                <!--<bean class="org.springframework.http.converter.StringHttpMessageConverter">-->
                    <!--<property name="supportedMediaTypes">-->
                        <!--<list>-->
                            <!--<value>text/plain;charset=UTF-8</value>-->
                            <!--<value>text/html;charset=UTF-8</value>-->
                            <!--<value>applicaiton/json;charset=UTF-8</value>-->
                        <!--</list>-->
                    <!--</property>-->
                <!--</bean>-->
            <!--</list>-->
        <!--</property>-->
    <!--</bean>-->
    <!-- json裝換器使用 fastjson,預設就是 UTF-8 編碼,不需要再重新設定編碼-->
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
        <property name="messageConverters">
            <list>
                <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter"/>
            </list>
        </property>
    </bean>
    <!--配置註解驅動-->
    <mvc:annotation-driven/>
複製程式碼

說明:專案名為 springmvc,每個方法上面是測試地址哦。

2.1 基本型別

在傳參時方法中的形參名稱預設要和 url 中的引數名稱保持一致,也可以在方法中加 @RequestParam 註解修改 url 中的引數名稱。

基本型別中的基本資料型別(int,double)設定為引數是不能為空,否則將會報錯,而基本資料型別的包裝型別是可以為 null,也即是沒有傳入時預設值為 null,這裡也要注意上面提到的中文亂碼哦。

// http://localhost:8080/springmvc/basetype1.do?id=1
// http://localhost:8080/springmvc/basetype1.do?id= 不帶引數報錯
@RequestMapping(value = "basetype1.do")
@ResponseBody
public String baseType1(int id) {
    return "id=" + id;
}

// http://localhost:8080/springmvc/basetype2.do?id=1
// http://localhost:8080/springmvc/basetype2.do?id= 不帶引數不報錯,引數預設為null
@RequestMapping(value = "basetype2.do")
@ResponseBody
public String baseType2(Integer id) {
    return "id=" + id;
}
// http://localhost:8080/springmvc/basetype3.do?name='湯姆' 注意中文亂碼問題
// http://localhost:8080/springmvc/basetype3.do?name='tom'
@RequestMapping(value = "basetype3.do")
@ResponseBody
public String baseType3(String name) {
    return "name=" + name;
}

// http://localhost:8080/springmvc/basetype4.do?xid=1
@RequestMapping(value = "basetype4.do")
@ResponseBody
public String baseType4(@RequestParam(value = "xid") Integer id) {
    return "id=" + id;
}
複製程式碼

2.2 物件型別

實體類說明:

  • User 類中只有兩個屬性,一個是 String 型別的 name,一個是 Integer 型別的 age。
  • Order 類中也只有兩個屬性,一個是 String 型別的 id,一個是 User 型別的 user。
  • People 類中的屬性和 User 類中的完全一樣。

類中生成屬性的 getter 和 setter 方法以及 toString 方法。

在傳物件型別的屬性時,url 中引數名稱為物件的屬性名稱,不加物件名。

如果一個類中的屬性是另一個類,在傳參時,url 中引數名稱為屬性物件名稱加屬性,如下面的第二個方法。

當傳入的物件型別引數相同時,如果不加以區分,會給同名的屬性都賦值,如下面的第三個方法,這裡的資料繫結就需要我們自定義,@InitBinder("物件名"),在自定義的方法(方法名任意)中設定屬性預設的字首值,這樣就可以區分不同物件的屬性了。

// http://localhost:8080/springmvc/objecttype1.do?name='tom'&age=1
// http://localhost:8080/springmvc/objecttype1.do?name='tom'&age=
@RequestMapping(value = "objecttype1.do")
@ResponseBody
public String objecttype1(User user) {
    return "user=" + user;
}

// http://localhost:8080/springmvc/objecttype2.do?id='123'&user.name='tom'&user.age=1
// http://localhost:8080/springmvc/objecttype2.do?id='123'&user.name='tom'&user.age=
// http://localhost:8080/springmvc/objecttype2.do?id='123'
@RequestMapping(value = "objecttype2.do")
@ResponseBody
public String objecttype2(Order order) {
    return "order=" + order;
}

// http://localhost:8080/springmvc/objecttype3.do?people.name=Tom&user.name=Lucy
// http://localhost:8080/springmvc/objecttype3.do?people.name=Tom
// http://localhost:8080/springmvc/objecttype3.do?name=Tom
@RequestMapping(value = "objecttype3.do")
@ResponseBody
public String objecttype3(People people, User user) {
    return "people=" + people + ",user=" + user;
}

@InitBinder("people")
public void initPeople(WebDataBinder binder) {
    binder.setFieldDefaultPrefix("people.");
}

@InitBinder("user")
public void initUser(WebDataBinder binder) {
    binder.setFieldDefaultPrefix("user.");
}
複製程式碼

2.3 日期型別

大多數情況下,SpringMVC 的資料繫結以及可以滿足我們的使用了,但是對於一些特殊資料型別,如 java.util.Date 型別。字串轉 Date 型別,需要我們自定義轉換器(Converter)或格式化(Formatter)來進行資料繫結。

下面的方法一使用繫結資料時會按照使用者設定的格式初始化,但這種方法只對單個方法生效,我們可以自定義型別轉換類,轉換類需要實現 Converter 或者 Formatter 介面,具體的程式碼如下。

實現 Converter 介面需要指定介面的兩個泛型,前者為要轉換的型別,後者為轉換後的型別,並且需要實現介面中的 convert() 方法,方法中的引數為要轉換的型別,返回值為轉換後的型別。

實現 Formatter 介面只需要指定介面的一個泛型,即轉換後的型別,但是要實現介面中的 parse() 方法和 print() 方法,前一個方法是將要轉換的型別轉換為我們指定的型別,後一個方法是規定如何輸出轉換後的型別。

// DateConverter
public class DateConverter implements Converter<String, Date> {
    // 定義日期格式
    private String dataPattern = "yyyy-MM-dd HH:mm:ss";

    @Override
    public Date convert(String s) {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dataPattern);
        try {
            return simpleDateFormat.parse(s);
        } catch (ParseException e) {
            throw new IllegalArgumentException("無效的日期格式,請使用" + dataPattern + "格式的日期");
        }
    }
}
複製程式碼
// DateFormatter 類
public class DateFormatter implements Formatter<Date> {
    // 定義日期格式
    private String dataPattern = "yyyy-MM-dd HH:mm:ss";

    @Override
    public Date parse(String s, Locale locale) throws ParseException {
        return new SimpleDateFormat(dataPattern).parse(s);
    }

    @Override
    public String print(Date date, Locale locale) {
        return new SimpleDateFormat().format(date);
    }
}
複製程式碼

寫完自定義轉換類後,還需要在 SprinMVC 的配置檔案中配置,這樣對所有的方法都生效,具體配置如下:

<!--配置自定義的日期型別轉換器-->
    <mvc:annotation-driven conversion-service="dataConverterService"/>
    <!--使用 Convert 介面-->
    <bean id="dataConverterService" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <bean class="com.wenshixin.convert.DateConverter"/>
            </set>
        </property>
    </bean>
    <!--使用 Formatter 介面-->
    <!--<bean id="dataConverterService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">-->
    <!--<property name="formatters">-->
    <!--<set>-->
    <!--<bean class="com.wenshixin.convert.DateFormatter"/>-->
    <!--</set>-->
    <!--</property>-->
    <!--</bean>-->
複製程式碼
// http://localhost:8080/springmvc/datetype1.do?date=2018-09-19
@RequestMapping(value = "datetype1.do")
@ResponseBody
public String datetype1(Date date1) {
    return date1.toString();
}
@InitBinder("date1")
public void initDate(WebDataBinder binder) {
    binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), true));
}

// http://localhost:8080/springmvc/datetype2.do?date2=2018-09-10 22:50:10
@RequestMapping(value = "datetype2.do")
@ResponseBody
public String datetype2(Date date2) {
    return date2.toString();
}
複製程式碼

2.4 複雜型別

複雜型別包括陣列和集合型別,像 List、Set、Map。

陣列型別用於傳入多個引數名稱相同的值,如接收頁面上的核取方塊引數時。

SpringMVC 對於複雜型別的支援並不是很好,因為對於複雜型別,我們更多都是使用 JSON、XML等資料格式來傳參。對於 List、Set、Map 這些型別,還需要單獨設定一個包裝類,屬性設定為對應的集合型別,方法的引數為包裝型別,比較繁瑣。SpringMVC 對複雜型別的資料繫結的功能,基本上就是雞肋。

類說明:

  • UserList 為 User 對應的 List 集合包裝類,只有一個屬性,private List<User> users;
  • UserList 為 User 對應的 Set 集合包裝類,只有一個屬性,private Set<User> users = new HashSet<>();,並且需要在該類的建構函式中初始化 Set 集合的大小,不能動態改變 Set 集合大小,在傳值時,物件的個數不能超過這個大小。
  • UserList 為 User 對應的 Map 集合包裝類,只有一個屬性,private Map<String, User> users;
// http://localhost:8080/springmvc/complextype1.do?ids=1&ids=2
@RequestMapping(value = "complextype1.do")
@ResponseBody
public String objecttype1(String[] ids) {
    System.out.println(ids.length);
    StringBuilder stringBuilder = new StringBuilder();
    for(String id : ids) {
        stringBuilder.append(id + " ");
    }
    return stringBuilder.toString();
}

// http://localhost:8080/springmvc/complextype2.do?users%5B0%5D.name=Tom&users%5B1%5D.name=Lucy 注意特殊字元[]的轉義,不然會報錯
// http://localhost:8080/springmvc/complextype2.do?users%5B0%5D.name=Tom&users%5B1%5D.name=Lucy&users%5B6%5D.name=Mary 注意特殊字元[]的轉義,不然會報錯
@RequestMapping(value = "complextype2.do")
@ResponseBody
public String objecttype2(UserList userList) {
    return userList.toString();
}

// http://localhost:8080/springmvc/complextype2.do?users%5B0%5D.name=Tom&users%5B1%5D.name=Lucy&users%5B2%5D.name=Mary 注意特殊字元[]的轉義,不然會報錯
@RequestMapping(value = "complextype3.do")
@ResponseBody
public String objecttype3(UserSet userSet) {
    System.out.println(userSet.getUsers().size());
    return userSet.toString();
}

// http://localhost:8080/springmvc/complextype4.do?users%5B%270%27%5D.name=Tom&users%5B%271%27%5D.name=Lucy&users%5B%272%27%5D.name=Mary
@RequestMapping(value = "complextype4.do")
@ResponseBody
public String objecttype4(UserMap userMap) {
    System.out.println(userMap.getUsers().size());
    return userMap.toString();
}
複製程式碼

2.5 特殊型別

SpringMVC 更適合現今前後端分離的資料傳輸,對於現在流行的格式化資料型別 JSON,支援很好,只需要 @RequestBody(傳參)和 @ResponseBody(輸出)兩個註解,使用起來很方便。

對於編寫 API,SpringMVC 無疑是比 Struts2 的有優勢。

// json 格式
/*
  {
    "name":"Tom",
    "age":1
  }
*/
@RequestMapping(value = "jsontype.do")
@ResponseBody
public User jsontype(@RequestBody User user) {
    System.out.println(user);
    return user;
}

// xml 格式
/*
  <?xml version="1.0" encoding="UTF-8" ?>
  <user>
    <name>Jim</name>
    <age>16</age>
  </user>
*/
@RequestMapping(value = "xmltype.do")
@ResponseBody
public User xmltype(@RequestBody User user) {
    System.out.println(user);
    return user;
}

複製程式碼

2.6 RESTful 風格

RESTful 風格的 API 已經受到業界的肯定,在當今的分散式架構中更是如魚得水。很多 Web 框架也都支援 RESTful 風格的 API編寫,當然也包括 SpringMVC ,這裡簡單介紹一下 RESTful 風格。

RESTful 是 Resource Representional State Transfer 的縮寫,RE 是前面兩個單詞的簡寫,第一個單詞經常被省略,而這個單詞其實才是 RESTful 的核心思想,中文翻譯為 資源表現層狀態轉換

RESTful 的作者也是 HTTP 協議的設計者,他將 HTTP 中的 URI 的思想引入到 API 程式設計中,每一個資源都有一個存放的位置,對資源的操作(請求)就是資源在表現層的轉態轉換,如常見的 GET、POST,還有不常用 PUT、DELETE 等。

RESTful 風格有更加簡短的資源地址,和一般的 API 地址直接對資源進行操作,如 add、select 不同,RESTful 風格的主體是資源,對資源的操作體現在請求方式上,如 DELETE。不同的請求方式對應不同的操作,如同一個地址,如果是 GET 方式,就直接返回頁面,如果是 POST 方式,就是提交頁面上的資料,這樣地址也更少,使得訪問也更加安全。

下面的程式碼展示了 RESTful 風格的 API 如何使用,API 的測試,用瀏覽器並不方便,可以使用 Postman 等網路工具。

@RequestMapping(value = "/user/{name}", method = RequestMethod.GET)
@ResponseBody
public String findUserByGET(@PathVariable("name") String name) {
    return "GET name=" + name;
}

@RequestMapping(value = "/user/{name}", method = RequestMethod.POST)
@ResponseBody
public String findUserByPOST(@PathVariable("name") String name) {
    return "POST name=" + name;
}
複製程式碼

歡迎關注下方的微信公眾號哦,裡面有各種學習資料免費分享哦!

程式設計心路

相關文章