终于来到了基于注解的 Spring MVC 了。之前我们所讲到的 handler,需要根据 url 并通过 HandlerMapping 来映射出相应的 handler 并调用相应的方法以响应请求。实际上,ControllerClassNameHandlerMapping, MultiActionController 和选择恰当的 methodNameResolver(如 InternalPathMethodNameResolver) 就已经可以在很大程度上帮助我们省去不少的 XML 配置,谁让 ControllerClassNameHandlerMapping 极度的拥抱了 Convention Over Configuration 呢。
那为什么还要用基于注解的 Controller 呢?Spring MVC 在 Spring 2.5 发布中新添加了一种基于注解的 Controller 形式。借助于与 Spring 2.5 一同发布的容器内 <context:component-scan> 功能支持,基于注解的 Controller 几乎可以达到 XML 零配置,进而极大地提高我们的开发效率。
和其它 Controller 一样,基于注解的 Controller 同样有相应的 HandlerMapping,那就是 DefaultAnnotationHandlerMapping。同样,也有相应的 HandlerAdapter,那就是 AnnotationMethodHandlerAdapter。甚至,我们都可以不把 Controller 注册到容器里,那么肯定需要一种机制来帮助我们完成这点,这就是 <context:component-scan>。开发基于注解的 Controller,我们需要做以下准备工作:
● <context:compnent-scan>
- <context:component-scan base-package="org.zachary.spring3.anno.web" />
● HandlerMapping
- <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
- <description>
- 这点是必需的还是非必需的呢?
- 如果定义了 DefaultAnnotationHandlerMapping,它就可以将请求来的 url 和被注解了 @RequesMapping 的指进行匹配。当然,说这句话的前提是定义 DefaultAnnotationHandlerMapping 的优先级比定义了其它的 HandlerMapping 的优先级要高(如果定义了其它的话)。
- 如果没有定义 DefaultAnnotationHandlerMapping,并不代表不能映射到相应的 handler 上。因为如果你定义了其它的 HandlerMapping,请求过来的 url 和注解了的 @RequestMapping 里的值正好能匹配上,那么没有 DefaultAnnotationHandlerMapping,@Controller 一样可以如鱼得水的被捕获到。
- 当然,如果你要使用基于注解的 @Controller,最好还是老老实实地注册 DefaultAnnotationHandlerMapping。
- </description>
- </bean>
● HandlerAdaptor
- <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
- <description>
- 和上面的 HandlerMapping 一样,是必需的还是非必需的呢?
- Spring MVC 中,如果我们没有注册任何 HandlerAdaptor 到容器中,注意,我说的是任何。那么 DispatcherServlet 将启用后备的几个默认使用的 HandlerAdaptor 实现,包括:
- org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter
- org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter
- org.springframework.web.servlet.mvc.AnnotationMethodHandlerAdaptor
-
- 看见没,如果我们没有注册任何的 HandlerAdaptor,框架会准备 AnnotationMethodHandlerAdaptor 的。可是由于某些原因,我们需要为某些 HandlerAdaptoer 进行一些定制化,即在容器中注册了某个 HandlerAdaptor,那么很抱歉,框架只会启用你注册的那个,而框架本身准备的不会被启用。所以,你一旦为某个 HandlerMapping 进行了定制化,请别忘了把其它的 HandlerAdaptor 也注册进来,即便这些不需要定制化。否则的话,后果你是可以想象的。当然,除非你确保你真的只需要那一个你注册进容器的 HandlerAdaptor,否则,我再啰嗦一遍,别忘了把其它的 HandlerAdaptor 也注册进来。
- </description>
- </bean>
好了,有了以上几点准备工作,我们就可以开始基于注解的 Controller 之旅了。下面我们来一个一个注解的来讲解。
● @Controller
- @Controller
- public class MyController {
-
- }
● @RequestMapping
- @Controller
- @RequestMapping("/my")
- public class MyController {
-
-
- @RequestMapping(value="/somelist", method=RequestMethod.POST);
- public String getSomeList() {...}
-
-
- @RequestMapping(params="hello=world", method={RequestMethod.GET, RequestMethod.POST})
- public String helloworld() {...}
-
-
- @RequestMapping(params="hello=java", method={RequestMethod.GET, RequestMethod.POST})
- public String hellojava() {...}
-
-
- @RequestMapping(params="java", method={RequestMethod.GET})
- public String java() {...}
-
-
- @RequestMapping(params="cplusplus", method={RequestMethod.GET})
- public String cplusplus() {...}
-
-
- @RequestMapping(headers="hello=world", method={RequestMethod.POST})
- public String cplusplus() {...}
- }
● @RequestParam(将请求参数绑定到方法参数)
- @Controller
- @RequestMapping("/my")
- public class MyController {
-
-
- @RequestMapping("/test")
- public String test(int userId) { ... }
-
-
- @RequestMapping("/test2")
- public String test2(@RequestParam("userId") int id, int age, Date date, User user) { ... }
- }
● @PathVariable(将 url template 里的参数绑定到方法参数)
- @Controller
- @RequestMapping("/my")
- public class MyController {
-
-
- @RequestMapping("/user/{nickname}/{age}");
- public String getUserInfo(@PathVariable("nickname") String name, @PathVariable int age) {...}
- }
● @RequestBody(将请求正文绑定到方法参数)
- @Controller
- @RequestMapping("/my")
- public class MyController {
-
-
- @RequestMapping("/user/body");
- public String getBody(@RequestBody String body) {
-
-
- System.out.println(body);
- return null;
- }
- }
● @ResponseBody(将处理完请求后返回的对象绑定到响应正文)
- @Controller
- @RequestMapping("/my")
- public class MyController {
-
-
- @RequestMapping("/user")
- public @ResponseBody User getUser() {
- return new User(18, "Jack", "计算机");
- }
- }
● @ModelAttribute
- @Controller
- @RequestMapping("/my")
- public class MyController {
-
-
- @RequestMapping("/user")
- @ModelAttribute
- public User getUser() {
- return new User(18, "Jack", "计算机");
- }
-
-
- @RequestMapping("/user2")
- public String showUser(@ModelAttribute User user) {
- System.out.println(user);
- return null;
- }
- }
● @SessionAttributes
- @Controller
- @RequestMapping("/my")
- @SessionAttributes("the-attribute")
- public class MyController {
-
- @RequestMapping("/getUser")
- public String getUser(int userId, Model model) {
-
- User user = userService.getUserById(userId);
- model.addAtrribute("the-attribute", user);
- return "userinfo";
- }
-
-
- @RequestMapping("/updateUser")
- public String updateUser(@ModelAttribute("the-attribute") User user,
- BindingResult result, SessionStatus status) {
-
- if (result.hasErrors) {
- return "error";
- }
-
- userService.updateUser(user);
-
-
- status.setComplete();
- return "redirect:getUser?userId=" + user.getId();
- }
- }
Spring MVC 里的大部分的注解,这里基本上都讲到了。日后随着 Spring 的升级,我也会逐一补充新加的注解。其实,仅凭以上的注解,是可以构建一个足够强大的 RESTFul Webservices 的了。
这里,补充讲下被标注了 @RequestMapping 注解的请求方法的签名。使用 @RequestMapping 标注的 web 请求处理方法的签名比较灵活,我们几乎可以声明并使用任何类型的方法参数。不过,以下几种类型的方法参数将拥有更多语义,它们均来自框架内部(或者说 AnnotationMethodHandlerAdapter)所管理的对象引用:
- request/response/session
- org.springframework.web.context.request.WebRequest。当前处理方法中获得可用的 WebRequest 实例。
- java.util.Locale。通过相应的 LocalResolver 所返回的对应当前 web 请求的 Locale。
- java.io.InputStream/java.io.Reader。相当于 request.getInputStream() 或 request.getReader() 所获得的对象引用。
- java.io.OutputStream/java.io.Writer。相当于 response.getOutputStream() 或 response.getWriter() 所获得的对象引用。
- java.util.Map/org.springframework.ui.ModelMap。你现在可用对模型数据为所欲为了。
- org.springframework.validation.Errors/org.springframework.validation.BindingResult。用于对 Command 对象进行数据验证的 Errors 或者 BindingResult 对象。声明这两种类型的方法参数有一个限制,它们的声明必须紧跟着 Command 对象的定义。其它类型的方法参数是没有任何顺序限制的。
- org.springframework.web.bind.supportt.SessionStatus。SessionStatus 主要用于管理请求处理之后 Session 的状态,比如清除 Session 中的指定的数据。
基于注解的 Controller 的请求处理方法返回类型可以有如下 4 种形式(当然,前面提到的 @ResponseBody 和 @ModelAttribute 并没下面所描述的返回类型,具体参见上面对各自注解的讲解):
- org.springframework.web.servlet.ModelAndView。这个不用多说,视图信息和模型信息都能通过它返回。
- java.lang.String。该类型返回值代表逻辑视图名,模型数据需要以其它形式提供,比如为处理方法声明一个 ModelMap 类型的参数。注意,如果返回 null,并不代表向客户端输出空页面(定向思维惹的祸),这种情况下,框架会从请求路径中提取视图信息。如果返回 null 就是要表示方法内部已处理完请求,也不需要通知页面,就是想仅仅返回空白页面,唉,我还没有想出来咋整。。。反正 writer.write("") 这样写可以,还得声明一个 Writer 类型的方法参数。
- org.springframework.ui.ModelMap。ModelMap 类型返回值只包含了模型数据信息而没有视图信息,框架将根据请求的路径来提取视图信息。
- void。没有任何返回值,视图信息将从请求路径中提取,模型数据需要通过其它形式提供。
String 类型的返回值为 null, 还有返回类型为 ModelMap 和 void,从请求路径中如何提取视图信息呢?框架将截取请求路径中的最后一个 / 后面的内容,并去掉后缀名,剩下来的内容就是视图名。如请求路径为 /spring3/user/welcome,那么视图名是 welcome,/spring3/user/welcome.action 的视图名也是 welcome。
接下来来讲最后一个部分,请求参数到方法参数的绑定。这个在 @RequestParam 中已经讲过,不过,这里要讲的是绑定复杂的对象。在 @RequestParam 中,我们这样请求,date=2011-01-01 其实是绑定不到 Date 对象的。因为不同的 Locale 处理日期的字符串的表达方式不一样。总之,这部分涉及到字符串到对象的转换,这很像 PropertyEditor,对吧?Spring MVC 中,可以为某个 Controller 定制数据绑定,即在被标注了 @InitBinder 的方法里写绑定逻辑,方法名可以随意,如:
- @InitBinder
- public void initBinder(WebDataBinder binder) {
- binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
-
- final SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd");
-
- @Override
- public void setAsText(String text) throws IllegalArgumentException {
- try {
- Date date = sf.parse(text);
- setValue(date);
- } catch (ParseException e) {
- Date data = sf.parse(text);
- throw new IllegalArgumentException(e);
- }
- }
- })
- }
在 Controller 里使用 @InitBinder 标注的初始化方法只能对一个 Controller 对应的 WebBinder 做定制。如果想在整个应用中共享绑定规则,可以为 AnnotationMethodHandlerAdapter 指定一个自定义的 org.springframework.web.bind.support.WebBindingInitializer 实例,这样可以避免在每个 Controller 中都重复定义几乎相同逻辑的 @InitBinder 的初始化方法。
- public class MyBindingInitializer implements WebBindingInitializer {
-
- public void initBinder(WebBinder binder, WebRequest request) {
- binder.registerCustomEditor(SomeDataType.class, somePropertyEditor)
-
-
- }
- }
- <bean class=""org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter>
- <property name="webBindingInitializer">
- <bean class="...MyBindingInitializer" />
- </property>
- </bean>
结束该篇文章前,我们来看几个容易混淆的用于简化开发的配置: <mvc:annotation-driven />, <context:annotation-config/>, <context:component-scan />。
<mvc:annotation-driven /> 会做以下几件事:
- 向 spring 容器中注册 DefaultAnnotationHandlerMapping。
- 向 spring 容器中注册 AnnotationMethodHandlerAdapter。
- 配置一些 messageconverter。
- 解决了 @Controller 注解的使用前提配置,即 HandlerMapping 能够知道谁来处理请求。
<context:annotation-config /> 会做以下几件事:
- 向 spring 容器中注册 AutowiredAnnotationBeanPostProcessor。
- 向 spring 容器中注册 CommonAnnotationBeanPostProcessor。
- 向 spring 容器中注册 PersistenceAnnotationBeanPostProcessor。
- 向 spring 容器中注册 RequiredAnnotationBeanPostProcessor。
- 使用 <context:annotationconfig />之前,必须在 <beans> 元素中声明 context 命名空间 <context:component-scan />。<context:component-scan /> 对包进行扫描,实现注解驱动 Bean 定义。即,将 @Controller 标识的类的 bean 注册到容器中。
<context:component-scan/>,不但启用了对类包进行扫描以实施注解驱动 Bean 定义的功能,同时还启用了注解驱动自动注入的功能(即还隐式地在内部注册了 AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor)。因此当使用 <context:component-scan /> 后,除非需要使用PersistenceAnnotationBeanPostProcessor 和 RequiredAnnotationBeanPostProcessor 两个 Processor 的功能(例如 JPA 等),否则就可以将 <context:annotation-config /> 移除了。