乐趣区

从原理层面掌握ModelAttribute的使用使用篇一起学Spring-MVC

每篇一句

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

前言

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

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

退出移动版