背景需求如下,当更新一个对象时,某个字段比如密码不能被修改。
常见解决思路有二:
① new 一个对象,form表单中密码域为隐藏域,该种方法有风险。
② new 一个对象,在更新的时候再次从数据库查询密码从而进行更新,该方法比较麻烦。
SpringMVC的解决思路:使用@ModelAttribute !
JSP页面: <form action="springmvc/testModelAttribute" method="Post"> <input type="hidden" name="id" value="1"/> //注意此处隐藏域为id username: <input type="text" name="username" value="Tom"/> <br> email: <input type="text" name="email" value="tom@baidu.com"/> <br> age: <input type="text" name="age" value="18"/> <br> <input type="submit" value="Submit"/> </form>对比可知,age进行了更新 ,password延用了数据库查询的数据!
如下图所示,map中最终放的user并非数据库查询得到,而是经过了表单更新后的user !
① 执行 @ModelAttribute 注解修饰的方法(注意是方法,而不是参数): 从数据库中取出对象, 把对象放入到了 Map 中,键为: user。
即,在执行目标方法前,会先判断controller中有没有@ModelAttribute注解的方法(注意不是参数)。如果有,则先执行该方法。
② SpringMVC 从 Map 中取出 User 对象, 并把表单的请求参数赋给该 User 对象的对应属性
判断modelMap中有没有方法参数user对应的属性对象,即,modelMap中有没有{user:object}。如果有,取出使用;如果无,通过反射new 一个user对象出来。
③ SpringMVC 把上述对象传入目标方法的参数(最终匹配url的方法)
注意: 在 @ModelAttribute 修饰的方法中,放入到 Map 时的键需要和目标方法入参类型的第一个字母小写的字符串一致!(User user)map.put("user", user);如果不一致,需要@ModelAttribute()显示指定!
① 确定一个 key
若目标方法的 POJO 类型的参数没有使用 @ModelAttribute 作为修饰, 则 key 为 POJO 类名第一个字母的小写
若使用了 @ModelAttribute 来修饰, 则 key 为 @ModelAttribute 注解的 value 属性值。
② 确定key对应的value
2.1在 implicitModel 中查找 key 对应的对象, 若存在, 则作为入参传入
若在 @ModelAttribute 标记的方法中在 Map 中保存过, 且 key 和 ①确定的 key 一致, 则会获取到。
2.2若 implicitModel 中不存在 key 对应的对象, 则检查当前的 Handler 是否使用 @SessionAttributes 注解修饰,
若使用了该注解, 且 @SessionAttributes 注解的 value 属性值中包含了 key, 则会从 HttpSession 中来获取 key 所 对应的 value 值。
若value存在则直接传入到target(target是什么,继续往下看)中;若不存在则将抛出异常.
2.3 若 Handler 没有标识 @SessionAttributes 注解或 @SessionAttributes 注解的 value 值中不包含 key, 则 会通过反射来创建 POJO 类型的参数, 传入为目标方法的参数。
2.4 SpringMVC 会把 key 和 POJO 类型的对象保存到 implicitModel 中, 进而会保存到 request 中(先保存再传给目标方法参数,具体看源码分析流程).
该步结束后,request中会存在(key : POJO)对象
综合来讲有如下两个过程:
调用 @ModelAttribute 注解修饰的方法(注解在方法上),实际上把 @ModelAttribute 方法中 Map 中的数据放在了 implicitModel 中。
解析请求处理器的目标参数, 实际上该目标参数来自于 WebDataBinder 对象的 target 属性。
详细过程分析如下
① 创建 WebDataBinder 对象:
确定 objectName 属性: 若传入的 attrName 属性值为 "", 则 objectName 为类名第一个字母小写(如user);
注意: attrName. 若目标方法的 POJO 属性使用了 @ModelAttribute 来修饰, 则 attrName 值即为 @ModelAttribute 的 value 属性值 (也可以为abc)
确定 target 属性( 即从request或sessionAttribute中根据key获取的对象或者根据目标类型反射新建的空的对象)
在 implicitModel 中查找 attrName 对应的属性值;若存在, ok( map( attrName:user))(默认为请求域,可以理解为如果请求域中没有则去session域中寻找) · 若不存在: 则验证当前 Handler 是否使用了 @SessionAttributes 进行修饰, 若使用了, 则尝试从 Session 中 获取 attrName 所对应的属性值 ; 若 session 中没有对应的属性值, 则抛出了异常。 · 若 Handler 没有使用 @SessionAttributes 进行修饰, 或 @SessionAttributes 中没有 value 属性值指定的 key和 attrName 相匹配, 则通过反射创建了 POJO 对象 (此时 map(value:new POJO ))
② SpringMVC 把表单的请求参数赋给了 WebDataBinder 的 target对象对应的属性
如果target对象是从request或者session域中获取的,那么把表单参数再次赋值给该对象,将会改变对象的值!
③ SpringMVC 会把 WebDataBinder 的 attrName 和 target 给到 implicitModel, 进而传到 request 域对象中。
④ 把 WebDataBinder 的 target 作为参数传递给目标方法的入参。
综上,SpringMVC中,方法入参的绑定离不开WebDataBinder对象。其实,只从名字就可以得知,该对象的作用就是将web页面请求传过来的参数赋予后台方法。
① @ModelAttribute注释void返回值的方法
@Controller public class HelloWorldController { @ModelAttribute public void populateModel(@RequestParam String abc, Model model) { model.addAttribute("attributeName", abc); //返回model } @RequestMapping(value = "/helloWorld") public String helloWorld() { return "helloWorld"; //返回的视图名 } }这个例子,在获得请求/helloWorld 后,populateModel方法在helloWorld方法之前先被调用,它把请求参数(/helloWorld?abc=text)加入到一个名为attributeName的model属性中,在它执行后helloWorld被调用,返回视图名helloWorld和model(已由@ModelAttribute方法生产好了)。
② @ModelAttribute注释返回具体类的方法
@ModelAttribute public Account addAccount(@RequestParam String number) { return accountManager.findAccount(number); }这种情况,model属性的名称没有指定,它由返回类型隐含表示,如这个方法返回Account类型,那么这个model属性的名称是account(默认为对象类型的首字母小写)。
③ @ModelAttribute(value="")注释返回具体类的方法
@Controller public class HelloWorldController { @ModelAttribute("abc") public String addAccount(@RequestParam String abc) { return abc; } @RequestMapping(value = "/helloWorld") public String helloWorld() { return "helloWorld"; //返回的视图名 } }这个例子中使用@ModelAttribute注释的value属性,来指定model属性的名称。model属性对象就是方法的返回值(abc),它无须要特定的参数。 如果 @ModelAttribute(“abc”),然后方法里面使用了map.put(“user”,user)。那么,map额外会存在一个 key为abc,value为null的对象!其他方法如果绑定了 abc属性,那么将会获取到null!
④ @ModelAttribute和@RequestMapping同时注释一个方法
@Controller public class HelloWorldController { @RequestMapping(value = "/helloWorld.do") @ModelAttribute("attributeName") public String helloWorld() { return "hi"; } }这时这个方法的返回值并不是表示一个视图名称,而是model属性的值,视图名称由RequestToViewNameTranslator根据请求"/helloWorld.do"转换为逻辑视图helloWorld。 Model属性名称有@ModelAttribute(value=””)指定,相当于在request中封装了key=attributeName,value=hi。
⑤ @ModelAttribute注释一个方法的参数
@Controller public class HelloWorldController { @ModelAttribute("user") public User addAccount() { return new User("jz","123"); } @RequestMapping(value = "/helloWorld") public String helloWorld(@ModelAttribute("user") User user) { user.setUserName("jizhou"); return "helloWorld"; } }在这个例子里,@ModelAttribute(“user”) User user注释方法参数,参数user的值来源于addAccount()方法中的model属性。
因为@ModelAttribute 注解的方法总会在其他方法调用前执行。
此时如果方法体没有标注@SessionAttributes(“user”),那么scope为request,如果标注了,那么scope为session。如果controller上标注了@SessionAttributes(“user”),那么如果从model中获取不到,就会从session中尝试获取。如果仍旧获取不到,则会抛出异常!
