2017年12月4日 星期一

@SessionAttributes、@ModelAttribute (Spring3.x MVC 三)

※@SessionAttribute

@Controller
@RequestMapping("/ooo/xxx/")
@SessionAttributes(value = {"xxx", "ooo"}, types = Date.class)
public class XxxAction {
    @RequestMapping(value = "session/*.mvc")
    public String session(Model model) {
        model.addAttribute("xxx", 5);
        model.addAttribute("ooo", "o");
        model.addAttribute("aaa", new Date(100,1,1));
        model.addAttribute("bbb", new Date(101,12,12));
        return "hello";
    }
}

※value 是 key 的名稱,type 表示某個類型的也會放入 session

※只能寫在 type 上,是針對整個 controller

※參數裡,Model、Map 都可以,其他沒試過

※測試

---------- index.jsp ----------
<a href="ooo/xxx/session/attr.mvc">session</a>


---------- hello.jsp ----------
request xxx:${requestScope.xxx}<br />
request ooo:${requestScope.ooo}<br />
request aaa:${requestScope.aaa}<br />
request bbb:${requestScope.bbb}<br />
<br />
session xxx:${sessionScope.xxx}<br />
session ooo:${sessionScope.ooo}<br />
session aaa:${sessionScope.aaa}<br />
session bbb:${sessionScope.bbb}<br />




※常見錯誤

@SessionAttributes("book")
public class XxxAction {
    @RequestMapping(value = "session/*.mvc")
    public String session(Model model, Book book) {
        model.addAttribute("xxx", 5);
        // model.addAttribute("book", "b");
        return "hello";
    }
}

※不管有沒有註解那一行,都會出「org.springframework.web.HttpSessionRequiredException: Session attribute 'book' required - not found in session」的錯

※因為參數 Book,有一個隱藏的 @ModelAttribute("book"),只要將裡面的值和 @SessionAttributes 的屬性值不一樣即可

※另外一個解決辦法就是使用下面要介紹的 @ModelAttribute,Map 裡放 book 也可以解決



※@ModelAttribute

※這個 annotation 是針對所有的 controller,每次都會先調用,所以要考慮清楚再寫,調用完後,才執行前端的方法

※只有一個屬性 value,是 String,而不是 String[],所以是 1 對 1 的關係,可以在 controller 裡寫很多的 @ModelAttribute,都會在開始前被調用

※可以寫在方法上和參數裡
方法上的 value:沒寫為 void
參數裡的 value:沒寫為參數型態的駝峰命名

参數上:req 的參數依名稱注入到指定物件裡,而且還會將這個物件自動加入 Model
方法上:@RequestMapping 方法前執行,如有返回值,會自動將返回值加入到 Model


public class Book {
    private Integer id;
    private String name;
    private Integer price;
    
    // setter/getter...
    
    public Book() {}
    
    public Book(Integer id, String name, Integer price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
}




※controller

@ModelAttribute
public void getBook(@RequestParam(value="id", required=true) Integer id, Integer price, Model model) {
    model.addAttribute("book", new Book(id, "bruce", 1111)); // 模擬 DB
}
    
@RequestMapping("book")
public String pojo(Book book) {
    System.out.println(book.getId());
    System.out.println(book.getPrice());
    System.out.println(book.getName());
    return "hello";
}

※注意 model 的 key 必需是類名的駝峰命名法,如 ComicBook,那就要取名為 comicBook

※如果想自己取名,如 model.addAttribute("uuu", ...); 那 pojo 的參數也要宣告 @ModelAttribute("uuu"),也就是說,pojo 不寫 @ModelAttribute("uuu"),預設是@ModelAttribute("參數名的駝峰命名"),兩個對應就可以,沒對應不會報錯,只是替換沒成功

※測試

---------- index.jsp ----------
<a href="ooo/xxx/book.mvc?id=99&price=77">modelAttr</a>


---------- hello.jsp ----------
${requestScope.book.id}<br />
${requestScope.book.price}<br />
${requestScope.book.name}<br />

※此例是模擬前端傳 id 到 DB 取出對應的值,但我只想改 price,name 並沒有給,所以是 null,但我不給的意思是想要和 DB 一樣,所以就寫了@ModelAttribute

DB 取出來的 price 是 1111,然後再到 pojo 方法,此時會將前端傳過來的值覆蓋 DB 的值


※@ModelAttribute 的屬性

只有一個屬性 value,它的值可以用在前端

@ModelAttribute("abc")
public Book getBook() {
    return new Book(88, "bruce", 100);
}
    
@RequestMapping("book")
public String pojo(Book book) {
    return "hello";
}

※必須要 return 才行

※測試

---------- index.jsp ----------
<a href="ooo/xxx/book.mvc">modelAttr</a>


---------- hello.jsp ----------
${abc.id}<br />
${abc.price}<br />
${abc.name}<br />

好文章



※原理

在 Map 的地方下斷點,debug 調試後,HandlerMethodInvoker.java 有二個較重要的方法如下:

一、invokeHandlerMethod

有兩個迴圈,這個方法主要在處理 annotation

第一個迴圈:
如果有 @SessionAttributes 的 value 和 types 就將它們放入 ExtendedModelMap 裡,所以至少要執行過一次,瀏覽器才有

第二個迴圈:
@ModelAttribute 可以寫在方法上和參數裡,這個方法裡都是方法上的,有兩個重要變數

⑴ attrName:@ModelAttribute 的 value 屬性,沒有為 void,最後成為 ExtendedModelMap 的 key

⑵ attrValue:回傳的內容,沒回傳為 null,最後成為 ExtendedModelMap 的 value

1.resolveHandlerArguments 取得的是前端傳過來的資料,但 @ModelAttribute 裡的參數要有,不然是空,看方法二
2.如果 attrName 在 ExtendedModelMap 找到就下一個迴圈了,不會再繼續執行 3 之後的程式碼
3.回 controller 執行此迴圈的 @ModelAttribute 方法得到 attrValue
4.如果 @ModelAttribute 沒寫 value,那值就是 void
5.如果 ExtendedModelMap 沒有 attrName,就塞入
迴圈結束

再呼叫一次第二個迴圈裡的 1 方法,但是是針對 @RequestMapping 的,肯定只有一個,所以寫在迴圈外,回傳的是前端覆蓋 @ModelAttribute 方法裡的值


二、resolveHandlerArguments

迴圈裡還有迴圈,這個方法主要在處理方法的參數的 annotation

外層迴圈:方法有幾個參數就跑幾次,整個方法除了 return 外,都是外層迴圈

內層迴圈:每一個參數有幾個 annotation 就跑幾次,只有以下 8 種 annotation 才會處理
1.RequestParam
2.RequestHeader
3.RequestBody
4.CookieValue
5.PathVariable
6.ModelAttribute
7.Value
8.Valid 開頭的,spring3 只有 Validated
內層迴圈結束

以下還是在外層迴圈內,內層迴圈外
1~6 的 annotation,每一個參數最多只能給一個,否則會報「Handler parameter annotations are exclusive choices - do not specify more than one such annotation on the same parameter: 」 + 方法名

如果參數沒有 1~6 的 annotation,還有三個判斷
第一個我沒看懂
第二個是判斷有 @Valid 的 value 屬性就將參數塞到回傳的變數 args 裡
如果不是1、2 就是這一個,又分成 6 個判斷
⑴ 參數型態是 Model 或 Map :是的話再判斷是不是 ExtendedModelMap
是就將 ExtendedModelMap 塞到回傳的變數 args 裡
不是就拋「Argument [參數型態] is of type Model or Map but is not assignable from the actual model. You may need to switch newer MVC infrastructure classes to use this argument.」
⑵是 SessionStatus 或其父類,將變數 sessionStatus 塞到 args 裡
⑶是 HttpEntity 或其父類,呼叫 resolveHttpEntityRequest 後回傳給 args
⑷是 Errors 或其父類,拋「Errors/BindingResult argument declared without preceding model attribute. Check your handler method signature!」
⑸基本類型和Wrapper、enum、CharSequence、Number、Date、URI、URL、Locale、Class,只要是其中一個,就將變數 paramName 置為空
⑹都不是變數attrName為空

最後的部分還有 6 個判斷,前 5 個都是針對 annotation 的 value 屬性,有就呼叫相對應的方法後,回傳到 args
第 6 個是 @ModelAttribute 沒寫在參數裡或者寫了 value 值才會進去,
主要就是取得 WebDataBinder 後,有個 getTarget 方法,塞到 args 裡

沒有留言:

張貼留言