從原理層面掌握@ModelAttribute的使用(使用篇)【一起學Spring MVC】
每篇一句
每個人都應該想清楚這個問題:你是祖師爺賞飯吃的,還是靠老天爺賞飯吃的
前言
上篇文章 描繪了@ModelAttribute
的核心原理,這篇聚焦在場景使用上,演示@ModelAttribute
在不同場景下的使用,以及注意事項(當然有些關聯的原理也會涉及)。
為了進行Demo演示,首先得再次明確一下@ModelAttribute
的作用。
@ModelAttribute
的作用
雖然說你可能已經看過了核心原理篇,但還是可能會缺乏一些上層概念的總結。下面我以我的理解,總結一下 @ModelAttribute
這個註解的作用,主要分為如下三個方面:
- 繫結請求引數到命令物件(入參物件):放在控制器方法的入參上時,用於將多個請求引數繫結到一個命令物件,從而
簡化
- 暴露表單引用物件為模型資料:放在處理器的一般方法(非功能處理方法,也就是沒有
@RequestMapping
標註的方法)上時,是為表單準備要展示的表單引用資料物件:如註冊時需要選擇的所在城市等靜態資訊。它在執行功能處理方法(@RequestMapping
註解的方法)之前,自動新增到模型物件中,用於檢視頁面展示時使用; - 暴露
@RequestMapping
方法返回值為模型資料:放在功能處理方法的返回值上時,是暴露功能處理方法的返回值為模型資料,用於檢視頁面展示時使用。
下面針對這些使用場景,分別給出Demo
用例,供以大家在實際使用中參考。
@ConstructorProperties講解
因為在原理篇裡講過,自動建立模型物件的時候不僅僅可以使用空的建構函式,還可以使用java.beans.ConstructorProperties
這個註解,因此有必須先把它介紹一波:
官方解釋:建構函式上的註釋,顯示該建構函式的引數如何對應於構造物件的getter方法。
// @since 1.6 @Documented @Target(CONSTRUCTOR) // 只能使用在構造器上 @Retention(RUNTIME) public @interface ConstructorProperties { String[] value(); }
如下例子:
@Getter
@Setter
public class Person {
private String name;
private Integer age;
// 標註註解
@ConstructorProperties({"name", "age"})
public Person(String myName, Integer myAge) {
this.name = myName;
this.age = myAge;
}
}
這裡註解上的name
、age
的意思是對應著Person
這個JavaBean
的getName()
和getAge()
方法。
它表示:構造器的第一個引數可以用getName()
檢索,第二個引數可以用getAge()
檢索,由於方法/構造器的形參名在執行期就是不可見了,所以使用該註解可以達到這個效果。
此註解它的意義何在???
其實說實話,在現在去xml
,完全註解驅動的時代它的意義已經不大了。它使用得比較多的場景是之前像使用xml
配置Bean這樣:
<bean id="person" class="com.fsx.bean.Person">
<constructor-arg name="name" value="fsx"/>
<constructor-arg name="age" value="18"/>
</bean>
這樣<constructor-arg>
就不需要按照自然順序引數index(不靈活且容易出錯有木有)來了,可以按照屬性名來對應,靈活了很多。本來xml配置基本不用了,但恰好在@ModelAttribute
解析這塊讓它又換髮的新生,具體例子下面會給出的~
> java.beans
中還提供了一個註解java.beans.Transient
(1.7以後提供的):指定該屬性或欄位不是永久的。 它用於註釋實體類,對映超類或可嵌入類的屬性或欄位。(可以標註在屬性上和get方法上)
Demo Show
標註在非功能方法上
@Getter
@Setter
@ToString
public class Person {
private String name;
private Integer age;
public Person() {
}
public Person(String myName, int myAge) {
this.name = myName;
this.age = myAge;
}
}
@RestController
@RequestMapping
public class HelloController {
@ModelAttribute("myPersonAttr")
public Person personModelAttr() {
return new Person("非功能方法", 50);
}
@GetMapping("/testModelAttr")
public void testModelAttr(Person person, ModelMap modelMap) {
//System.out.println(modelMap.get("person")); // 若上面註解沒有指定value值,就是類名首字母小寫
System.out.println(modelMap.get("myPersonAttr"));
}
}
訪問:/testModelAttr?name=wo&age=10
。列印輸出:
Person(name=wo, age=10)
Person(name=非功能方法, age=50)
可以看到入參的Person
物件即使沒有標註@ModelAttribute
也是能夠正常被封裝進值的(並且還放進了ModelMap
裡)。
因為沒有註解也會使用空構造建立一個
Person
物件,再使用ServletRequestDataBinder.bind(ServletRequest request)
完成資料繫結(當然還可以@Valid校驗)
有如下細節需要注意:
1、Person
即使沒有空構造,藉助@ConstructorProperties
也能完成自動封裝
// Person只有如下一個建構函式
@ConstructorProperties({"name", "age"})
public Person(String myName, int myAge) {
this.name = myName;
this.age = myAge;
}
列印的結果完全同上。
2、即使上面@ConstructorProperties
的name寫成了myName
,結果依舊正常封裝。因為只要沒有校驗bindingResult == null
的時候,仍舊還會執行ServletRequestDataBinder.bind(ServletRequest request)
再封裝一次的。除非加了@Valid
校驗,那就只會使用@ConstructorProperties
封裝一次,不會二次bind了~(因為Spring認為你已經@Valid過了,那就不要在湊進去了)
3、即使上面構造器上沒有標註@ConstructorProperties
註解,也依舊是沒有問題的。原因:BeanUtils.instantiateClass(ctor, args)
建立物件時最多args是[null,null]
唄,也不會報錯嘛(so需要注意:如果你是入參是基本型別int那就報錯啦~~)
4、雖然說@ModelAttribute
寫不寫效果一樣。但是若寫成這樣@ModelAttribute("myPersonAttr") Person person
,也就是指定為上面一樣的value值,那列印的就是下面:
Person(name=wo, age=10)
Person(name=wo, age=10)
至於原因,就不用再解釋了(參考原理篇)。
==另外還需要知道的是:@ModelAttribute
標註在本方法上只會對本控制器有效。但若你使用在@ControllerAdvice
元件上,它將是全域性的。(當然可以指定basePackages
來限制它的作用範圍~)==
標註在功能方法(返回值)上
形如這樣:
@GetMapping("/testModelAttr")
public @ModelAttribute Person testModelAttr(Person person, ModelMap modelMap) {
...
}
這塊不用給具體的示例,因為比較簡單:把方法的返回值放入模型中。(注意void、null這些返回值是不會放進去的~)
標註在方法的入參上
該使用方式應該是我們使用得最多的方式了,雖然原理複雜,但對使用者來說還是很簡單的,略。
和@RequestAttribute
/@SessionAttribute
一起使用
參照博文:從原理層面掌握@RequestAttribute、@SessionAttribute的使用【一起學Spring MVC】。它倆合作使用是很順暢的,一般不會有什麼問題,也沒有什麼主意事項
和@SessionAttributes
一起使用
@ModelAttribute
它本質上來說:允許我們在呼叫目標方法前操縱模型資料。@SessionAttributes
它允許把Model
資料(符合條件的)同步一份到Session裡,方便多個請求之間傳遞數值。
下面通過一個使用案例來感受一把:
@RestController
@RequestMapping
@SessionAttributes(names = {"name", "age"}, types = Person.class)
public class HelloController {
@ModelAttribute
public Person personModelAttr() {
return new Person("非功能方法", 50);
}
@GetMapping("/testModelAttr")
public void testModelAttr(HttpSession httpSession, ModelMap modelMap) {
System.out.println(modelMap.get("person"));
System.out.println(httpSession.getAttribute("person"));
}
}
為了看到@SessionAttributes
的效果,我這裡直接使用瀏覽器連續訪問兩次(同一個session)看效果:
第一次訪問列印:
Person(name=非功能方法, age=50)
null
第二次訪問列印:
Person(name=非功能方法, age=50)
Person(name=非功能方法, age=50)
可以看到@ModelAttribute
結合@SessionAttributes
就生效了。至於具體原因,可以移步這裡輔助理解:從原理層面掌握@ModelAttribute的使用(核心原理篇)【一起學Spring MVC】
再看下面的變種例子(重要):
@RestController
@RequestMapping
@SessionAttributes(names = {"name", "age"}, types = Person.class)
public class HelloController {
@GetMapping("/testModelAttr")
public void testModelAttr(@ModelAttribute Person person, HttpSession httpSession, ModelMap modelMap) {
System.out.println(modelMap.get("person"));
System.out.println(httpSession.getAttribute("person"));
}
}
訪問:/testModelAttr?name=wo&age=10
。報錯了:
org.springframework.web.HttpSessionRequiredException: Expected session attribute 'person'
at org.springframework.web.method.annotation.ModelFactory.initModel(ModelFactory.java:117)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:869)
這個錯誤請務必重視:這是前面我特別強調的一個使用誤區,當你在@SessionAttributes
和@ModelAttribute
一起使用的時候,最容易犯的一個錯誤。
錯誤原因程式碼如下:
public void initModel(NativeWebRequest request, ModelAndViewContainer container, HandlerMethod handlerMethod) throws Exception {
Map<String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request);
container.mergeAttributes(sessionAttributes);
invokeModelAttributeMethods(request, container);
// 合併完sesson的屬性,並且執行完成@ModelAttribute的方法後,會繼續去檢測
// findSessionAttributeArguments:標註有@ModelAttribute的入參 並且isHandlerSessionAttribute()是SessionAttributts能夠處理的型別的話
// 那就必須給與賦值~~~~ 注意是必須
for (String name : findSessionAttributeArguments(handlerMethod)) {
// 如果model裡不存在這個屬性(那就去sessionAttr裡面找)
// 這就是所謂的其實@ModelAttribute它是會深入到session裡面去找的哦~~~不僅僅是request裡
if (!container.containsAttribute(name)) {
Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
// 倘若session裡都沒有找到,那就報錯嘍
// 注意:它並不會自己創建出一個新物件出來,然後自己填值,這就是區別。
// 至於Spring為什麼這麼設計 我覺得是值得思考一下子的
if (value == null) {
throw new HttpSessionRequiredException("Expected session attribute '" + name + "'", name);
}
container.addAttribute(name, value);
}
}
}
注意,這裡是initModel()
的時候就報錯了喲,還沒到resolveArgument()
呢。Spring這樣設計的意圖???我大膽猜測一下:控制器上標註了@SessionAttributes
註解,如果你入參上還使用了@ModelAttribute
,那麼你肯定是希望得到繫結的,若找不到肯定是你的程式失誤有問題,所以給你丟擲異常,顯示的告訴你要去排錯。
修改如下,本控制器上加上這個方法:
@ModelAttribute
public Person personModelAttr() {
return new Person("非功能方法", 50);
}
(請注意觀察下面的幾次訪問以及對應的列印結果)
訪問:/testModelAttr
Person(name=非功能方法, age=50)
null
再訪問:/testModelAttr
Person(name=非功能方法, age=50)
Person(name=非功能方法, age=50)
訪問:/testModelAttr?name=wo&age=10
Person(name=wo, age=10)
Person(name=wo, age=10)
注意:此時model
和session
裡面的值都變了哦,變成了最新的的請求連結上的引數值(並且每次都會使用請求引數的值)。
訪問:/testModelAttr?age=11111
Person(name=wo, age=11111)
Person(name=wo, age=11111)
可以看到是可以完成區域性屬性修改的
再次訪問:/testModelAttr
(無請求引數,相當於只執行非功能方法)
Person(name=fsx, age=18)
Person(name=fsx, age=18)
可以看到這個時候model
和session
裡的值已經不能再被非功能方法上的@ModelAttribute
所改變了,這是一個重要的結論。
它的根本原理在這裡:
public void initModel(NativeWebRequest request, ModelAndViewContainer container, HandlerMethod handlerMethod) throws Exception {
...
invokeModelAttributeMethods(request, container);
...
}
private void invokeModelAttributeMethods(NativeWebRequest request, ModelAndViewContainer container) throws Exception {
while (!this.modelMethods.isEmpty()) {
...
// 若model裡已經存在此key 直接continue了
if (container.containsAttribute(ann.name())) {
...
continue;
}
// 執行方法
Object returnValue = modelMethod.invokeForRequest(request, container);
// 注意:這裡只判斷了不為void,因此即使你的returnValue=null也是會進來的
if (!modelMethod.isVoid()){
...
// 也是隻有屬性不存在 才會生效哦~~~~
if (!container.containsAttribute(returnValueName)) {
container.addAttribute(returnValueName, returnValue);
}
}
}
}
因此最終對於@ModelAttribute
和@SessionAttributes
共同的使用的時候務必要注意的結論:已經新增進session
的資料,在沒用使用SessionStatus
清除過之前,@ModelAttribute
標註的非功能方法的返回值並不會被再次更新進session內
所以
@ModelAttribute
標註的非功能方法有點初始值的意思哈~,當然你可以手動SessionStatus
清楚後它又會生效了
總結
任何技術最終都會落到使用上,本文主要是介紹了@ModelAttribute
各種使用case的示例,同時也指出了它和@SessionAttributes
一起使用的坑。
@ModelAttribute
這個註解相對來說還是使用較為頻繁,並且功能強大,也是最近講的最為重要的一個註解,因此花的篇幅較多,希望對小夥伴們的實際工作中帶來幫助,帶來程式碼之美~
相關閱讀
從原理層面掌握HandlerMethod、InvocableHandlerMethod、ServletInvocableHandlerMethod的使用【一起學Spring MVC】
從原理層面掌握@SessionAttributes的使用【一起學Spring MVC】
從原理層面掌握@ModelAttribute的使用(核心原理篇)【一起學Spring MVC】
知識交流
==The last:如果覺得本文對你有幫助,不妨點個讚唄。當然分享到你的朋友圈讓更多小夥伴看到也是被作者本人許可的~
==
若對技術內容感興趣可以加入wx群交流:Java高工、架構師3群
。
若群二維碼失效,請加wx號:fsx641385712
(或者掃描下方wx二維碼)。並且備註:"java入群"
字樣,會手動邀請入群
若文章
格式混亂
或者圖片裂開
,請點選`:原文連結-原文連結-原文連結