1
Spring MVC
Calvin Xiao edited this page 2013-11-14 19:08:40 -08:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

##OverView 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

	@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.输出跨域Ajax所需的JsonP

网上说什么扩展JsonView什么的太复杂了,自己拿Jackson生成一个JsonP的字符串返回就好了。 更多JSONP信息见Ajax章节。


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的结合

见Validation章节,一般情况下使用JQuery Validation Plugin的客户端认证。为了防止恶意用户的攻击,可以再加上Spring MVC与Hibernate Validator的服务端认证。因为是用来防恶意攻击的,因此直接抛出异常,而不会返回输入页面且输出出错信息(如果Controller方法中有BindingResult的参数,就交由方法内部去处理,否则直接往外抛异常)。

  1. 在spring-mvc.xml中,加入hibernate validator的定义
  2. 在User.java的相关属性加入@NotBlank定义
  3. 在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类

##资料

  • Book: <Pro Spring MVC with Web Flow> 2012