從原理層面掌握@ModelAttribute的使用(使用篇)【一起學Spring MVC】

来源:https://www.cnblogs.com/fangshixiang/archive/2019/08/15/11361061.html
-Advertisement-
Play Games

一個可以沉迷於技術的程式猿,wx加入加入技術群:fsx641385712 ...


每篇一句

每個人都應該想清楚這個問題:你是祖師爺賞飯吃的,還是靠老天爺賞飯吃的

前言

上篇文章 描繪了@ModelAttribute的核心原理,這篇聚焦在場景使用上,演示@ModelAttribute在不同場景下的使用,以及註意事項(當然有些關聯的原理也會涉及)。

為了進行Demo演示,首先得再次明確一下@ModelAttribute的作用。

@ModelAttribute的作用

雖然說你可能已經看過了核心原理篇,但還是可能會缺乏一些上層概念的總結。下麵我以我的理解,總結一下 @ModelAttribute這個註解的作用,主要分為如下三個方面:

  1. 綁定請求參數到命令對象(入參對象):放在控制器方法的入參上時,用於將多個請求參數綁定到一個命令對象,從而簡化綁定流程,而且自動暴露為模型數據用於視圖頁面展示時使用;
  2. 暴露表單引用對象為模型數據:放在處理器的一般方法(非功能處理方法,也就是沒有@RequestMapping標註的方法)上時,是為表單準備要展示的表單引用數據對象:如註冊時需要選擇的所在城市等靜態信息。它在執行功能處理方法(@RequestMapping 註解的方法)之前,自動添加到模型對象中,用於視圖頁面展示時使用;
  3. 暴露@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;
    }
}

這裡註解上的nameage的意思是對應著Person這個JavaBeangetName()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)

註意:此時modelsession裡面的值都變了哦,變成了最新的的請求鏈接上的參數值(並且每次都會使用請求參數的值)。

訪問:/testModelAttr?age=11111

Person(name=wo, age=11111)
Person(name=wo, age=11111)

可以看到是可以完成局部屬性修改的

再次訪問:/testModelAttr(無請求參數,相當於只執行非功能方法)

Person(name=fsx, age=18)
Person(name=fsx, age=18)

可以看到這個時候modelsession里的值已經不能再被非功能方法上的@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入群" 字樣,會手動邀請入群

若文章格式混亂或者圖片裂開,請點擊`:原文鏈接-原文鏈接-原文鏈接


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 前面一篇文章寫了 "《SimpleDateFormat 如何安全的使用?》" , 裡面介紹了 SimpleDateFormat 如何處理日期/時間,以及如何保證線程安全,及其介紹了在 Java 8 中的處理時間/日期預設就線程安全的 DateTimeFormatter 類。那麼 Java 8 ...
  • 以 Spring MVC 啟動 Servlet 為例,其應用上下文為 ServletWebServerApplicationContext,繼承了 GenericWebApplicationContext 的大部分方法,主要重寫了 postProcessBeanFactory()、refresh() ...
  • 一個簡單的Restful Crud實驗 預設首頁的訪問設置: 項目結構: Bean: package com.project.javasystem.Bean; public class Department { private Integer id; private String departmen ...
  • 1.日誌文件列表 比如:/data1/logs/2019/08/15/ 10.1.1.1.log.gz 10.1.1.2.log.gz 2.統計日誌中的某關鍵字shell腳本 zcat *.gz|grep 關鍵字 |grep -oP "deviceid=[^=]+"|uniq|sort -u > / ...
  • 本文整理自知乎上的同名討論帖:《為什麼有些大公司技術弱爆了?》,版權歸原作者所有,原文地址: www.zhihu.com/question/32039226 有網友提問: 今年年初,到一家互聯網公司實習,該公司是國內行業龍頭。不過技術和管理方面,卻弱爆了。那裡的程式員,每天都在看郵件,查問題工單。這 ...
  • Java虛擬機是如何載入Java類的? 這個問題也就是面試常問到的Java類載入機制。在年初面試百戰之後,菜鳥喜鵲也是能把這流程倒背如流啊!但是,也只是字面上的背誦,根本就是像上學時背書考試一樣。 tonight ! 我們把它映射到實戰里,看看如何用代碼說明這個流程。 ready! go! 在這之前 ...
  • 本文共:3495字,預估閱讀時間:9分鐘 前言 上到職場幹將下到職場萌新,都會接觸到包裝簡歷這個詞語。當你簡歷投到心儀的公司,公司內負責求職的工作人員是如何甄別簡歷的包裝程度的?Coody老師根據自己的經驗寫下了這篇文章,誰都不是天才,包裝無可厚非,切勿對號入座! 正文 在互聯網極速膨脹的社會背景下 ...
  • 一個可以沉迷於技術的程式猿,wx加入加入技術群:fsx641385712 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...