SpringMVC回归MVC本质,简简单单的Restful式函数,没有任何基类之后,应该是传统Request-Response框架中最好用的了。
Tips
1.事务失效的惨案
Spring MVC最打击新人的事情,你必须保证spring-mvc.xml的context:component-scan只扫描Controller,而 applicationContext.xml里的不包含Controller. 否则你定义在applicationContext.xml里的事务就要失效了。方法如下:
spring-mvc.xml:
<context:component-scan base-package="com.mycompany.myproject" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan>
applicationContext.xml:
<context:component-scan base-package="org.springside.examples.quickstart"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan>
另外,定义在spring-mvc.xml里的东西,在applicationContext*.xml中是不可见的,想共享的东西最好放在applicationContext.xml那边。
而applicationContext*.xml里的一些BeanPostProccesor,也不会作用到spring-mvc.xml定义/扫描到的Bean上,如果有必要就在spring-mvc.xml里重新定义一次,像Shiro的AOP校验权限。
2.Struts2式的Preparable接口--表单仅包含对象领域部分属性
Struts2有一个很实用的Preparable二次绑定功能: 表单提交时,先绑定一个ID,使用这个ID从数据库里找出对象来,再把表单中的其他属性绑定到这个对象上,对于那些表单中的输入框数量比业务对象的实际属性数少的情况很实用。
其实Spring MVC也有相同的能力, 见quickstart中的UserAdminController.
先用@ModelAttribute标注如下函数。SpringMVC会在执行任何实际处理函数之前,执行该函数并将返回值存为Model Attribute "user"
@ModelAttribute("user") public User getUser(@RequestParam(value = "id", required = false) Long id) { if (id != null) { return accountService.getUser(id); } return null; }
再在save函数里,以@ModelAttribute标注表单处理函数的参数。SpringMVC就会按名称"user"取出前面的对象,然后才进行真正的Binding。
@RequestMapping(value = "update/{userId}", method = RequestMethod.POST) public String update(@Valid @ModelAttribute("user") User user) { accountService.updateUser(user); return "redirect:/admin/user"; }
注意1, 这里有个小坑爹的地方是,这个getUser()会在controller的所有函数前都执行,因此需要进行一下判断RequestParam中是否含id属性的判断,要不你就把update()方法独立到一个Controller中。
注意2, ModelAttribute如果已经占用了"user"这个名字,那些非update()函数的参数里就要躲开这个名字。
另外,你也可以选择不用这个功能,而是自己创建一个Form的DTO,然后用Dozer或手工把属性绑定到领域对象上。
3.Struts2式的FlashAttribute
为了防止用户刷新重复提交,save操作之后一般会redirect到另一个页面,同时带点操作成功的提示信息。因为是Redirect,Request里的attribute不会传递过去,如果放在session中,则需要在显示后及时清理,不然下面每一页都带着这个信息也不对。Spring在3.1才提供了这个能力。
public String save(@ModelAttribute("group") Group group, RedirectAttributes redirectAttributes) { accountManager.saveGroup(group); redirectAttributes.addFlashAttribute("message", "修改权限组成功"); return "redirect:/account/group/"; }
4.CheckBox/RadioButtons的绑定
在采用ORM的应用中,如果绑定子对象到页面上,以及在表单提交时如何把checkbox的内容重新绑回父对象是一个头痛的问题。
在showcase示例中,User-Role组合中的Role是一个对象而不是简单的枚举(对于简单的枚举,什么都不用做,直接用checkboxes的taglib就可以了。
<form:checkboxes path="permissionList" items="${allPermissions}" itemLabel="displayName" itemValue="value" />
注意,如果使用BootStrap,SpringMVC自带的checkboxes标签并不合用,详见Twitter Bootstrap章节。
而复杂对象时对象就没这么好彩了,详见showcase中的UserControler首先你需要设定不要自动绑定checkbox结果到对象中
@InitBinder protected void initBinder(WebDataBinder binder) { binder.setDisallowedFields("roleList"); }
然后在输入参数中多注入一个roleList, 自行处理:
@RequestMapping(value = "save/{userId}") public String update(@Valid @ModelAttribute("user") User user, @RequestParam(value = "roleList") List<Long> checkedRoleList) { user.getRoleList().clear(); for (Long roleId : checkedRoleList) { Role role = new Role(roleId); user.getRoleList().add(role); } accountService.saveUser(user); return "redirect:/account/user"; }
5.输出跨域Ajax所需的JsonP
网上说什么扩展JsonView什么的太复杂了,自己拿Jackson生成一个JsonP的字符串返回就好了。 更多JSONP信息见Ajax章节.
6.Spring MVC与Hibernate Validator的结合
见Validation章节,一般情况下使用JQuery Validation Plugin的客户端认证。为了防止恶意用户的攻击,可以再加上Spring MVC与Hibernate Validator的服务端认证。因为是用来防恶意攻击的,因此直接抛出异常,而不会返回输入页面且输出出错信息(如果Controller方法中有BindingResult的参数,就交由方法内部去处理,否则直接往外抛异常)。
- 在spring-mvc.xml中,加入hibernate validator的定义
- 在User.java的相关属性加入@NotBlank定义
- 在UserDetailController的save方法,加入@Valid定义和BindingResult参数。
7. WARNING: "Skipping URI variable ‘id‘ since the request contains a bind value
造成这个Warning的原因是url的path和表单中都有id 变量,这个时候,把url path改成别的名字就好了。
@RequestMapping(value = "save/{userId}")