每篇一句

每个人都应该想清楚这个问题:你是祖师爷赏饭吃的,还是靠老天爷赏饭吃的

前言

上篇文章 描绘了@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入群" 字样,会手动邀请入群

若文章格式混乱或者图片裂开,请点击`:原文链接-原文链接-原文链接

从原理层面掌握@ModelAttribute的使用(使用篇)【一起学Spring MVC】的更多相关文章

  1. 从原理层面掌握@ModelAttribute的使用(核心原理篇)【一起学Spring MVC】

    每篇一句 我们应该做一个:胸中有蓝图,脚底有计划的人 前言 Spring MVC提供的基于注释的编程模型,极大的简化了web应用的开发,我们都是受益者.比如我们在@RestController标注的C ...

  2. 从原理层面掌握@SessionAttribute的使用【一起学Spring MVC】

    每篇一句 不是你当上了火影大家就认可你,而是大家都认可你才能当上火影 前言 该注解顾名思义,作用是将Model中的属性同步到session会话当中,方便在下一次请求中使用(比如重定向场景~). 虽然说 ...

  3. 从原理层面掌握@RequestAttribute、@SessionAttribute的使用【一起学Spring MVC】

    每篇一句 改我们就改得:取其精华,去其糟粕.否则木有意义 前言 如果说知道@SessionAttributes这个注解的人已经很少了,那么不需要统计我就可以确定的说:知道@RequestAttribu ...

  4. Spring MVC内容协商实现原理及自定义配置【享学Spring MVC】

    每篇一句 在绝对力量面前,一切技巧都是浮云 前言 上文 介绍了Http内容协商的一些概念,以及Spring MVC内置的4种协商方式使用介绍.本文主要针对Spring MVC内容协商方式:从步骤.原理 ...

  5. RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】

    每篇一句 人圆月圆心圆,人和家和国和---中秋节快乐 前言 在阅读本篇之前,建议先阅读开山篇效果更佳.RestTemplate是Spring提供的用于访问Rest服务的客户端工具,它提供了多种便捷访问 ...

  6. 从原理层面掌握@InitBinder的使用【享学Spring MVC】

    每篇一句 大魔王张怡宁:女儿,这堆金牌你拿去玩吧,但我的银牌不能给你玩.你要想玩银牌就去找你王浩叔叔吧,他那银牌多 前言 为了讲述好Spring MVC最为复杂的数据绑定这块,我前面可谓是做足了功课, ...

  7. 从原理层面掌握HandlerMethod、InvocableHandlerMethod、ServletInvocableHandlerMethod的使用【一起学Spring MVC】

    每篇一句 想当火影的人没有近道可寻,当上火影的人同样无路可退 前言 HandlerMethod它作为Spring MVC的非公开API,可能绝大多数小伙伴都对它比较陌生,但我相信你对它又不是那么的生疏 ...

  8. Spring MVC 原理探秘 - 容器的创建过程

    1.简介 在上一篇文章中,我向大家介绍了 Spring MVC 是如何处理 HTTP 请求的.Spring MVC 可对外提供服务时,说明其已经处于了就绪状态.再次之前,Spring MVC 需要进行 ...

  9. Spring mvc的基本配置及工作原理

    1.spring mvc框架搭建 需求:在浏览器输入一个请求login.do,跳转到登录成功界面. 第一步,创建web项目,导入jar包 注意: 第二步,在web.xml中配置spring的核心监听器 ...

随机推荐

  1. 百度云下载神器 速盘SpeedPan v1.9.7

    速盘 – 不一样的度盘神器!SpeedPan 是一款由吾爱破解论坛会员"菩提叶"制作的度盘满速下载工具.这款百度网盘高速下载工具,免费小巧简单易用,采用了Aria2多线程下载,支持 ...

  2. SpringBoot快速入门01--环境搭建

    SpringBoot快速入门--环境搭建 1.创建web工程 1.1 创建新的工程. 1.2  选择maven工程,点击下一步. 1.3 填写groupid(maven的项目名称)和artifacti ...

  3. C#6.0 新增功能

    连载目录    [已更新最新开发文章,点击查看详细] C# 6.0 版本包含许多可提高开发人员工作效率的功能. 这些功能的总体效果是让你编写的代码更简洁.更具可读性. 该语法不像许多常见做法那样繁琐. ...

  4. 前端插件之Bootstrap Dual Listbox使用

    工欲善其事,必先利其器 对于很多非专业前端开发来说写页面是非常痛苦的,借助框架或插件往往能够达到事半功倍的效果,本系列文章会介绍我在运维系统开发过程中用到的那些顺手的前端插件,如果你是想写XX管理系统 ...

  5. python课堂整理16---内置函数

    1. abs :求绝对值 print(abs(-1)) 2. all()传入一个可迭代对象,对该对象进行bool值运算,若都为True 就返回True,有一个为假,就返回False print(all ...

  6. 基于 HTML5 Canvas 的可交互旋钮组件

    前言 此次的 Demo 效果如下: Demo 链接:https://hightopo.com/demo/comp-knob/ 整体思路 组件参数 绘制旋钮 绘制刻度 绘制指针 绘制标尺 绘制文本 1. ...

  7. Vim、Linux、Shell、Idea、Git命令总结

    目录 Vim 剪切 复制 shell idea centos Vim /名字 搜索关键词 :u 撤销上一次操作 v 进入可视模式(选中模式) 剪切 回到顶部 命令模式下的剪切 dd,删除一整行==剪切 ...

  8. spring读取xml配置文件(二)

    一.当spring解析完配置文件名的占位符后,就开始refresh容器 @Override public void refresh() throws BeansException, IllegalSt ...

  9. HPU暑期集训积分赛2

    A. 再战斐波那契 单点时限: 1.0 sec 内存限制: 512 MB 小z 学会了斐波那契和 gcd 后,老师又给他出了个难题,求第N个和第M个斐波那契数的最大公约数,这可难倒了小z ,不过在小z ...

  10. pheatmap绘制“热图”,你需要的都在这

    热图可以聚合大量的数据,并可以用一种渐进色来优雅地表现,可以很直观地展现数据的疏密程度或频率高低. 本文利用R语言 pheatmap 包从头开始绘制各种漂亮的热图.参数像积木,拼凑出你最喜欢的热图即可 ...