SpringMVC Model&@ModelAttribute解析
为甚么需要Model
先忘掉前后端分离的基于API的开发方式。
在传统的MVC模式开发中,一个Controller
的典型职责是:
- 通过模型层来获得一些数据
- 选择要渲染的视图,并将模型层获得的数据放到视图中
如果不使用SpringMVC,那我们必须要面对的一个问题就是如何将模型层的数据放到视图中。如果我们使用JSP技术,那么request
对象的属性是一个放置模型层数据的好地方,而且我们在开发中也都是这么做的。那如果使用别的视图技术呢?如果你使用Velocity
呢?如果你不是forward到一个JSP而是重定向到一个JSP呢?这时request
对象的属性还可用吗?你可能需要以别的方式传递数据到视图中。
上面的问题不应该是Controller
的职责,因为View
层才最知道如何将数据绑定到页面中。所以,SpringMVC提供模型数据的统一表示,也就是一个Map
,DispatcherServlet
将会在渲染视图时将这个Map
传递到View
中,View
如何处理这个数据的绑定就是它自己的事了。
Spring中的模型使用Model
、ModelMap
、Map
来表示,最后,它们都会以Map
形式交付到View
中进行渲染
所以Model存在的意义就是在要渲染到View中的数据和实际使用的View技术解耦
InternalResourceView处理模型数据的示例
比如在下面的InternalResourceView
的渲染方法中,它就将Model数据保存到了request
的Attribute中,所以也就有了Model数据会被保存在请求属性中这一说法,但实际这个说法是狭隘的,也许在别的视图技术中就不这样处理数据了呢!
所以,我们可以在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>
结果:
使用RedirectView时
RedirectView是把模型数据添加到URL参数中
所以,如果在Redirect时Model中有数据,那么它们就都会被拼接到URL上。
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";
}
页面可以读取到值:
@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";
}
页面中,weight
和height
被成功设置:
数据绑定过程可能失败,可以采用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
:
可以通过@ModelAttribute(binding=false)
来取消数据绑定过程
@ModelAttribute
注入的全部方式
实际上,@ModelAttribute
放在方法参数上,SpringMVC并非只尝试从Model中获取,下面的是这种参数的全部获取方式:
- 尝试从Model中获取,也就是刚刚说的情况
- 如果类使用了
@SessionAttributes
注解,那么从Session获取 - 从通过
Converter
传递的URI的PathVariable中 - 调用默认的构造方法,也就是刚刚的Model中没有的情况
- 调用与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());
}
}
结果:
ModelAttribute小总结
- 当
@ModelAttribute
放在方法上时,该方法会在所有Handler请求方法之前调用,用于向Model中预填充值 - 当
@ModelAttribute
放在方法参数上时,若Model中有该参数,那么获取到这个参数 - 否则,新建该对象,并将该对象放在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
。