Skip to content

Spring MVC

Calvin Xiao edited this page Nov 15, 2013 · 1 revision

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