从原理层面掌握@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入群"
字样,会手动邀请入群**
若文章格式混乱
或者图片裂开
,请点击`:原文链接-原文链接-原文链接