Spring MVC
OverView
SpringMVC回归MVC本质,简简单单的Restful式函数,没有任何基类之后,应该是传统Request-Response框架中最好用的了。
Tips
1.事务失效的惨案
Spring MVC最打击新人的事情,你必须保证spring-mvc.xml的\只扫描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
@ModelAttribute
public void getUser(@RequestParam(value = "id", required = false) Long id) {
if (id != null) {
model.addAttribute("user", accountService.getUser(id));
}
return null;
}
再在update函数里,以@ModelAttribute标注表单处理函数的参数。SpringMVC就会按名称"user"取出前面的对象,然后才进行真正的Binding。
@RequestMapping(value = "update", method = RequestMethod.POST)
public String update(@Valid @ModelAttribute("user") User user,
RedirectAttributes redirectAttributes) {
accountService.updateUser(user);
redirectAttributes.addFlashAttribute("message", "更新用户" + user.getLoginName() + "成功");
return "redirect:/admin/user";
}
这里有个小坑爹的地方是,这个getUser()会在controller的所有函数前都执行,因此需要进行一下判断RequestParam中是否含id属性的判断,要不你就把update()方法独立到一个Controller中。
另外,你也可以选择不用这个功能,而是自己创建一个Form的DTO,然后用Dozer或手工把属性绑定到领域对象上。
3.Struts2式的FlashAttribute
为了防止用户刷新重复提交,save操作之后一般会redirect到另一个页面,同时带点操作成功的提示信息。因为是Redirect,Request里的attribute不会传递过去,如果放在session中,则需要在显示后及时清理,不然下面每一页都带着这个信息也不对。留意上面UserAdminController例子里那句redirectAttributes.addFlashAttribute()
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.输出跨域 https://www.xnip.cn/doc/pwycpnyn 所需的JsonP
网上说什么扩展JsonView什么的太复杂了,自己拿Jackson生成一个JsonP的字符串返回就好了。 更多JSONP信息见https://www.xnip.cn/shouce/596/44920.html章节。
6.方法直接返回字符串时,中文字符乱码
因为方法定义直接返回字符串时(Html或Json内容),调用的是StringHttpMessageConverter,而此Converter默认编码是ISO-85591,需要重新设为UTF-8。
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<constructor-arg value="UTF-8" />
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
7.Spring MVC与Hibernate Validator的结合
见https://www.xnip.cn/shouce/596/44951.html章节,一般情况下使用JQuery https://www.xnip.cn/doc/vjvqmcrm Plugin的客户端认证。为了防止恶意用户的攻击,可以再加上Spring MVC与Hibernate Validator的服务端认证。因为是用来防恶意攻击的,因此直接抛出异常,而不会返回输入页面且输出出错信息(如果Controller方法中有BindingResult的参数,就交由方法内部去处理,否则直接往外抛异常)。
- 在spring-mvc.xml中,加入hibernate validator的定义
- 在User.java的相关属性加入@NotBlank定义
- 在UserDetailController的save方法,加入@Valid定义和BindingResult参数。
8. 异常处理
原来Spring MVC的异常定义比较土,详见Exception handling for rest with spring3.2 :
- 按照如下列表,将Spring MVC的标准异常转换为4xx或5xx的http返回码,对异常本身不做处理如记日志,也不会把具体错误信息返回给客户端。
- 非Spring MVC的异常,可以用\@ResponseStatus(value = HttpStatus.NOT_FOUND) 标注返回码,但问题依然是无处理无错误信息。
- 可以将Controller抛出的异常转到特定View, 保持SiteMesh的装饰效果:
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <property name="exceptionMappings"> <props> <prop key="java.lang.Throwable">error/500</prop> </props> </property> </bean>
- 可以在每个Controller内加上一个异常处理方法,并用\@ExceptionHandler标注。但此法要每个Controller写,太分散太累了。
@ExceptionHandler({ CustomException1.class, CustomException2.class }) public void handleException() { }
- 可以写一个自定义的HandlerExceptionResolver ,替代原来的,将所有的活都包过去。但这个又太集中了,整个War里只有一个Resolver,而且API是基于ModelAndView的,不适合做Restful的输出。
Spring MVC 3.2,终于补上了这块短板,合成了两种写法的优点,可以用@ControllerAdvice定义多个公共的ExceptionHandler类,每个Handler类可以用@ExceptionHandler(MyException1.class, MyException2.class)标注handler方法,只处理自己关心的异常,而且API变成了Restful友好的ResponseEntity。Quickstart中的RestExceptionHandler代码如下:
@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = { ConstraintViolationException.class })
public final ResponseEntity<?> handleException(ConstraintViolationException ex, WebRequest request) {
return new ResponseEntity(BeanValidators.extractPropertyAndMessage(ex.getConstraintViolations()),
HttpStatus.BAD_REQUEST);
}
}
9. WARNING: "Skipping URI variable 'id' since the request contains a bind value
造成这个Warning的原因是requestMapping中的url path如 update/{id}和表单中都有id 变量,这个时候,把url path改成别的名字就好了。
@RequestMapping(value = "save/{userId}")
10. MediaType
无论Spring还是Jax-RS自带的MediaType类,都没有附加UTF-8的定义,Google Guava的MediaType类的类型又不是字符串,不能直接用于annotation,所以在springside-core里重新封装了一个MediaTypes类