Loading

SpringMVC Model&@ModelAttribute解析

为甚么需要Model

先忘掉前后端分离的基于API的开发方式。

在传统的MVC模式开发中,一个Controller的典型职责是:

  1. 通过模型层来获得一些数据
  2. 选择要渲染的视图,并将模型层获得的数据放到视图中

如果不使用SpringMVC,那我们必须要面对的一个问题就是如何将模型层的数据放到视图中。如果我们使用JSP技术,那么request对象的属性是一个放置模型层数据的好地方,而且我们在开发中也都是这么做的。那如果使用别的视图技术呢?如果你使用Velocity呢?如果你不是forward到一个JSP而是重定向到一个JSP呢?这时request对象的属性还可用吗?你可能需要以别的方式传递数据到视图中。

上面的问题不应该是Controller的职责,因为View层才最知道如何将数据绑定到页面中。所以,SpringMVC提供模型数据的统一表示,也就是一个MapDispatcherServlet将会在渲染视图时将这个Map传递到View中,View如何处理这个数据的绑定就是它自己的事了。

Spring中的模型使用ModelModelMapMap来表示,最后,它们都会以Map形式交付到View中进行渲染

所以Model存在的意义就是在要渲染到View中的数据和实际使用的View技术解耦

InternalResourceView处理模型数据的示例

比如在下面的InternalResourceView的渲染方法中,它就将Model数据保存到了request的Attribute中,所以也就有了Model数据会被保存在请求属性中这一说法,但实际这个说法是狭隘的,也许在别的视图技术中就不这样处理数据了呢!

img

所以,我们可以在JSP中使用request.getAttribute中获取模型数据,下面这个方法设置了一个模型属性customAttr,然后它会把我们带向modelTestPage2,这是一个JSP页面,使用InternalResourceView进行渲染:

@Controller
@RequestMapping("/model")
public class ModelTest {
    @GetMapping("/testModel")
    public String testModel(Model model) {
        model.addAttribute("customAttr", "customValue");
        return "modelTestPage2";
    }
}

JSP页面中会分别尝试使用el直接获取和使用requestScope.get来获取,实际上,它们都是在获取request域的属性,只不过前一种写法更像Model对象用了什么魔法将一个普通变量暴露到JSP中一样:

<p>Directly get use el: ${customAttr}</p>
<p>Using request attribute: ${requestScope.get("customAttr")}</p>

结果:

img

使用RedirectView时

RedirectView是把模型数据添加到URL参数中

img

所以,如果在Redirect时Model中有数据,那么它们就都会被拼接到URL上。

img

RedirectView可以通过设置exposeModelAttributes属性,不将Model中的数据放到URL参数中。而且,它只会将Model中的简单对象放到URL中(BeanUtils.isSimpleValueType返回true的),像Pojo这种对象它不会动的

@ModelAttribute

修饰方法参数

如果@ModelAttribute用在参数上,那么它将从Model中获取值并设置给属性:

@GetMapping("/testModelAttribute")
public String testModelAttribute(@ModelAttribute Person person) {
    System.out.println(person);
    return "modelTestPage2";
}

可是,Model从哪来?我们通过Controller使用Model,目的是将模型层的数据放进去然后传递给View层,所以Controller才是Model的填充者,现在它却要从Model中获取一个值,谁来给它填充?

把这个问题先放一放

修饰方法

@ModelAttribute也可以修饰方法,该方法的作用就是在为具体URL服务的方法被调用前,向Model中填充一些东西。

所以,上面的例子可以这样写:

// 在服务方法被调用前,向Model中填充一些内容
@ModelAttribute
public Person person() {
    return new Person("Yudoge", 21, 100.0, 100.0, "翻斗大街", new Date());
}

// 使用@ModelAttribute获取Model中的属性
@GetMapping("/testModelAttribute")
public String testModelAttribute(@ModelAttribute(name = "person") Person person) {
    System.out.println(person);
    return "modelTestPage2";
}

@ModelAttribute修饰的方法中可以使用和Handler方法一样的参数,并且它们都会正确的被注入。

需要注意的是,如果你通过@ModelAttribute方法返回的是简单属性,并且还redirect了,那么这个Model中的数据自然会被放到转发的url querystring中

应用场景?

假如我们的系统中用Person表示一个用户,当然,很难想象这是什么脑瘫想出来的idea。

现在,我们希望在Controller的每个方法中都通过请求头中的Token来获取当前登录的Person对象,那在每个请求方法中都要写上具体的逻辑,为了避免这种重复代码,这时就可以使用@ModelAttribute

// 获取当前登录的Person对象并添加到Model
@ModelAttribute
public Person getLoginPerson(WebRequest request) {
    String token = request.getHeader("token");
    String id = TokenUtils.dec(token);
    Person p = personRepository.findPersonById(id);
    return p;
}

// 直接从model中拿到当前登录的Person对象
@GetMapping("/testModelAttribute")
public String testModelAttribute(@ModelAttribute Person person) {
    System.out.println(person);
    return "modelTestPage2";
}

回到修饰方法参数的情况

刚刚我们在说修饰方法参数时被打断了,现在我们回来。

我们通过将@ModelAttribute放在方法上,向每个Handler方法预填充Model,以让它们可以从Model中获取到值。那么如果没人给你填充,Model里没有你想要的对象咋办?

这种情况下,这个对象会被新建并且自动添加到Model中,你可以理解为这种情况下的@ModelAttribute注解是将一个对象快速添加到Model中而不需要显式操作Model对象的方法。

@GetMapping("/testModelAttribute")
public String testModelAttribute(@ModelAttribute(name = "person") Person person) {
    person.setName("Yudoge");
    person.setAddress("翻斗大街");
    person.setAge(10);
    return "modelTestPage3";
}

页面可以读取到值:

img

@ModelAttribute的数据绑定

@ModelAttribute标注的参数对象中的属性还可以被PathVariable中的属性覆盖,这被称为数据绑定(Databinding):

@GetMapping("/testModelAttribute/{weight}/{height}")
public String testModelAttribute(@ModelAttribute(name = "person") Person person) {
    person.setName("Yudoge");
    person.setAddress("翻斗大街");
    person.setAge(10);
    return "modelTestPage3";
}

页面中,weightheight被成功设置:

img

数据绑定过程可能失败,可以采用BindingResult来监测失败:

@GetMapping("/testModelAttribute/{weight}/{height}")
public String testModelAttribute(@ModelAttribute(name = "person") Person person, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        bindingResult.getAllErrors().forEach(System.out::println);
    }
    person.setName("Yudoge");
    person.setAddress("翻斗大街");
    person.setAge(10);
    return "modelTestPage3";
}

这时如果你传递错误格式的height

img

可以通过@ModelAttribute(binding=false)来取消数据绑定过程

@ModelAttribute注入的全部方式

实际上,@ModelAttribute放在方法参数上,SpringMVC并非只尝试从Model中获取,下面的是这种参数的全部获取方式:

  1. 尝试从Model中获取,也就是刚刚说的情况
  2. 如果类使用了@SessionAttributes注解,那么从Session获取
  3. 从通过Converter传递的URI的PathVariable中
  4. 调用默认的构造方法,也就是刚刚的Model中没有的情况
  5. 调用与Serlvet请求参数匹配的主构造函数创建

通过Converter的PathVarible注入

下面的代码通过@ModelAttribute尝试从PathVarible中获取Person对象:

@GetMapping("/testModelAttributeAndConverter/{person}")
public String testModelAttributeAndConverter(@ModelAttribute(name = "person") Person person) {
    return "modelTestPage3";
}

PathVarible是字符串,我们需要一个Converter将以某种格式编排的字符串转换成Person:

public class PersonConverter implements Converter<String, Person> {
    private final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
    @Override
    public Person convert(String source) {
        try {
            String[] sarr = source.split(":");
            Person person = new Person();
            person.setName(sarr[0]);
            person.setAge(Integer.parseInt(sarr[1]));
            person.setWeight(Double.parseDouble(sarr[2]));
            person.setHeight(Double.parseDouble(sarr[3]));
            person.setAddress(sarr[4]);
            person.setBirthday(format.parse(sarr[5]));
            return person;
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }
}

这里我们的编排格式是:

名字:年龄:体重:身高:地址:生日

它简单的使用split方法来把字符串分割成数组,如果在这过程中发生任何异常,比如数组下标越界,比如parse格式失败,都会遵循Converter接口的规范,重新抛出一个IllegalArgumentException表示转换失败。

注册该Converter:

@Configuration
@ComponentScan(basePackages = {"top.yudoge.controller"})
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {

    // ...

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new PersonConverter());
    }
}

结果:

img

ModelAttribute小总结

  1. @ModelAttribute放在方法上时,该方法会在所有Handler请求方法之前调用,用于向Model中预填充值
  2. @ModelAttribute放在方法参数上时,若Model中有该参数,那么获取到这个参数
  3. 否则,新建该对象,并将该对象放在Model中

缺省@ModelAttribute注解

官方文档中描述Controller方法参数的一节有这样一段:

If a method argument is not matched to any of the earlier values in this table and it is a simple type (as determined by BeanUtils#isSimpleProperty, it is a resolved as a @RequestParam. Otherwise, it is resolved as a @ModelAttribute.

如果一个方法参数没有匹配在这个表格中的前面的任何值,并且它是一个简单类型,那么它被@RequestParam注解来解析,否则,它被@ModelAttribute注解解析。

意思就是说,@RequestParam@ModelAttribute注解可以被省略掉,如果方法参数是一个简单类型就默认应用@RequestParam,否则就应用@ModelAttribute

posted @ 2022-07-24 15:11  yudoge  阅读(263)  评论(0编辑  收藏  举报