再看下面的变种例子(重要):
@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这个注解相对来说还是使用较为频繁,并且功能强大,也是最近讲的最为重要的一个注解,因此花的篇幅较多,希望对小伙伴们的实际工作中带来帮助,带来代码之美~