SpringMVC学习笔记

SpringMVC 是 Spring 的一个模块,提供 web 层解决方案;和众多其他web框架一样,它基于MVC的设计理念,此外,它采用了松散耦合可插拔组件结构,比其他MVC框架更具扩展性和灵活性。
SpringMVC 通过一套 MVC 注解,让 POJO 成为处理请求的控制器,无需实现任何接口,同时,SpringMVC 还支持 REST 风格的 URL 请求。
此外,SpringMVC 在数据绑定、视图解析、本地化处理及静态资源处理上都有许多不俗的表现。
它在框架设计、扩展性、灵活性等方面全面超越了 Struts、WebWork 等 MVC 框架,从原来的追赶者一跃成为 MVC 的领跑者。
SpringMVC 框架围绕 DispatcherServlet 这个核心展开,DispatcherServlet 是 SpringMVC 框架的总导演、总策划,它负责截获请求并将其分派给相应的处理器处理。

框架流程

下面说说 SpringMVC 的执行流程,当然和传统的 MVC 模式是非常相似的:

SpringMVC.jpeg

介绍中提到过,DispatcherServlet 是前端控制器,相当于中央调度器,各个组件都和前端控制器进行交互,降低了各个组件之间耦合度;Handler 是后端控制器,当成模型(Model)。
那么一个请求的完整步骤可以概况为:

  1. 用户发起 request 请求,请求至 DispatcherServlet 前端控制器
  2. DispatcherServlet 前端控制器请求 HandlerMapping 处理器映射器查找 Handler
  3. HandlerMapping 处理器映射器,根据 url 及一些配置规则(xml配置、注解配置)查找 Handler,将 Handler 返回给 DispatcherServlet 前端控制器;
    这里其实会以 HandlerExecutionChain 对象的形式返回,我就称它为执行链了,它包含了 Handler 对象和拦截器列表
  4. DispatcherServlet 前端控制器根据返回的 Handler 调用合适的适配器执行 Handler,有了适配器通过适配器去扩展对不同 Handler 执行方式(比如:原始servlet开发,注解开发)
  5. 适配器执行 Handler(成功获得 HandlerAdapter 后,将开始执行拦截器的 preHandler(…) 方法 ),其中会经过一些消息转换器,例如进行数据绑定相关操作(数据转换、格式化、校验等)
  6. Handler 执行完成后返回 ModelAndView 给适配器
    ModelAndView 是 springmvc 的一个对象,对 Model (数据)和 view (仅仅是 View 的名字)进行封装。
  7. 适配器将 ModelAndView 返回给 DispatcherServlet
  8. DispatcherServlet 通过返回的 View 的名称,调用视图解析器进行视图解析,解析后生成真正的 view
    也就是说视图解析器根据逻辑视图名解析出真正的视图对象。
    View:springmvc 视图封装对象,提供了很多 view,比如:jsp、freemarker、pdf、excel。。。
  9. ViewResolver 视图解析器给前端控制器返回真正的 view 对象
  10. DispatcherServlet 调用 view 的渲染视图的方法(根据模型对象的数据),将模型数据填充到 request 域
  11. View 返回渲染后的视图
  12. DispatcherServlet 向用户响应结果(jsp页面、json数据等)

以上就是 springmvc 处理一个请求的步骤了,然后再贴一张详细一点的流程图吧:

Spring-MVC-详细运行流程图(通俗易懂).png

一个简单的栗子

然后就来看看具体的开发流程,首先定义 web.xml 文件,也就是定义 SpringMVC 的总导演 DispatcherServlet 这就是一个 servlet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<!-- springMVC前端控制器 -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 指定配置文件的位置 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<!-- load-on-startup 表示在web应用启动时,即加载该DispatcherServlet,而不是等到首次请求再中载 -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>*.action</url-pattern>
</servlet-mapping>

<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>

关于配置文件也有必要说一下,首先这里使用了 init-param 标签来指定配置文件的位置,如果不配置,默认加载的是 WEB-INF/{servlet-name}-servlet.xml ,其中大括号中指的是变量名,还记的 servlet-name 在那设置的么?看第六行!也就是说最终默认找的配置文件是 springmvc-servlet.xml 。
然后就是 url-pattern 了,匹配规则,SpringMVC 支持:*.xxx//xxx/* ;不支持 :/*
是的,原来在 struts 中写的很爽的 /* 在这里是错误的;使用这种配置,最终要转发到一个 jsp 页面时,仍然会由 DispatcherServlet 解析 jsp 地址,不能根据 jsp 页面找到 handler,会报错。
不过 / 表示所有访问的地址都由 DispatcherServlet 解析,对于静态文件的解析需要配置不让 DispatcherServlet 解析,使用此种方法可以实现 RESTful 风格的 url。
按照套路接下来就应该是 springmvc 的配置文件了(先使用配置文件的方式),其实就是 spring 的配置文件,因为基本没差嘛:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd">

<!-- 配置 HandlerMapping(处理器映射器) -->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>

<!-- 处理器适配器,所有的处理器适配器都实现HandlerAdapter -->
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>

<!-- 配置Handler(自己写的) -->
<bean name="/hello.action" class="cn.itcast.ssm.controller.HelloController"/>

<!-- 定义视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
id="internalResourceViewResolver">
<!-- 前缀 -->
<property name="prefix" value="/WEB-INF/jsp/" />
<!-- 后缀 -->
<property name="suffix" value=".jsp" />
</bean>
</beans>

这些定义其实都没必要看文档,就看上面的那个流程图,定义的这些 bean 完全就是按照图的顺序定义的,因为图中都是接口所以定义的时候要写具体实现的 bean。
视图解析器的前缀后缀是什么意思呢?从图中得知,最终要根据一个 View 的名字(在 Handler 的返回值中)返回一个具体的视图对象,视图说白了就是 jsp、json 等这种,所以视图解析器会根据前缀和后缀最后拼出一个真正的视图,也就是找到具体你定义的 jsp 文件。
视图就不写了,就是简单的一个 JSP 文件,除了视图 Handler 也需要自己写,看下 Handler 应该怎么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//实现Controller接口的映射器
public class HelloController implements Controller{

@Override
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 返回ModelAndView
ModelAndView modelAndView = new ModelAndView();
// 相当于 request 的 setAttribute 方法
// 在jsp页面中就可以通过 EL 表达式获取数据
modelAndView.addObject("msg","HelloWorld!");
//指定视图
modelAndView.setViewName("hello");

return modelAndView;
}
}

这里一定要实现 Controller 接口,在适配器中会检查是否有实现这个接口,一般 Handler 会放在 controller 包下。
视图解析器会根据 setViewName 设置的名字和前缀、后缀进行拼接,最后就转到了我们定义的 jsp 视图上;
这样就可以通过访问 /hello.action 来调用 Handler 了。
从命名来说,一般如果是 addXXX 那么就可以添加多个,如果是 setXXX 那么就只能设置一个了。
另外,Controller(handler)的名字和视图的名字应该是对应的。

优化配置文件

我们在 springmvc 的配置文件中配置了一些 bean,但是有些其实是不用配也是可以的(有默认值),比如前两个:处理器映射器和处理器适配器,后面两个肯定不能省了,因为有我们自己配的信息

使用注解

在最开始的介绍中也说明了,springmvc 中的注解是很重要的,大多都是用注解吧,毕竟在默认的配置文件中就已经开启了注解,所以注解我们可以直接用。
目标就是达到零配置!虽然默认配置已经开启了注解,但是已经过时了(AnnotationMethodHandlerAdapter 和 DefaultAnnotationHandlerMapping),推荐使用新的类(RequestMappingHandlerAdapter 和 RequestMappingHandlerMapping):

1
2
3
4
5
6
7
8
9
10
<!-- spring 3.1之后由RequestMappingHandlerAdapter和RequestMappingHandlerMapping代替 -->  
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
<!-- 或者直接使用下面的注解驱动,上面的两个就不用写了 -->
<mvc:annotation-driven/>

<!-- 声明DispatcherServlet不要拦截下面声明的目录 -->
<mvc:resources location="/images/" mapping="/images/**"/>
<!-- 另一种放行方式 -->
<mvc:default-servlet-handler/>

至于注解驱动是如何将最新的映射器和适配器注入的可以去看 org.springframework.web.servlet.config.AnnotationDrivenBeanDefinitionParser 这个类,我就先不研究了….
简单说,驱动注解内部其实包含了这些内容:

1
2
3
4
5
6
7
8
9
10
11
<!-- HandlerMapping -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping "/>
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"></bean>
<!-- HandlerAdapter -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"></bean>
<bean class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter "></bean>
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"></bean>
<!-- HadnlerExceptionResolvers -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver "></bean>
<bean class="org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver"></bean>
<bean class="org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver"></bean>

@Controller

在 SpringMVC 中,控制器 Controller 负责处理由 DispatcherServlet 分发的请求,它把用户请求的数据 经过业务处理层处理之后封装成一个 Model ,然后再把该 Model 返回给对应的 View 进行展示。
在 SpringMVC 中提供了一个非常简便的定义 Controller 的方法,你无需继承特定的类或实现特定的接口,只需使用@Controller 标记一个类是 Controller
然后使用 @RequestMapping 和 @RequestParam 等一些注解用以定义 URL 请求和 Controller 方法之间的映射,这样的 Controller 就能被外界访问到。
Controller 不会直接依赖于 HttpServletRequest 和 HttpServletResponse 等 HttpServlet 对象,它们可以通过 Controller 的方法参数灵活的获取到。
也就是说,@Controller 用于标记在一个类上,使用它标记的类就是一个 SpringMVC Controller 对象,然后 Spring 怎样才能找到它呢,有两种方式,当然,推荐使用扫描啦:

1
2
3
4
<!--方式一-->
<bean class="com.host.app.web.controller.MyController"/>
<!--方式二-->
<context:component-scan base-package = "com.host.app.web" />

@RequestMapping

首先这是一个重点!其次,它非常 NB。
RequestMapping 是一个用来处理请求地址映射的注解,可用于类或方法上;访问的时候就是 类上 value + 方法上的 value 拼起来,并且可以不加斜线,会自动处理。
RequestMapping 注解有六个属性,可分为五种映射(或者说三种映射两种限制):

  1. 标准URL映射
    比如:@RequestMapping("hello") or @RequestMapping("/hello") or @RequestMapping(value="hello") ,它们都是一样的。

  2. Ant 风格的 URL 映射
    Ant 通配符有三种,- 匹配单个字符;* - 匹配 0 或者任意数量的字符;** - 匹配 0 或者更多的目录。
    例如:@RequestMapping("/test/*/hello")

  3. 占位符映射
    使用占位符用花括号括起来,可以使用多个占位符。例如:

    1
    2
    3
    4
    5
    6
    @RequestMapping(value="user/{id}")
    public ModelAndView show (@PathVariable("id") int pid) {
    ModelAndView modelAndView = new ModelAndView("hello");
    modelAndView.addObject("msg","HelloWorld!" + pid);
    return modelAndView;
    }

    这样访问 xxx/user/123.action 的时候,会自动把 123 传进 pid 这个参数,这样就又多了一种传值的方式。

  4. 限制请求方法映射
    也就是限制请求方式,只要添加一个属性就可以了,使用到的是 method 属性:
    @RequestMapping(value="hello",method = RequestMethod.GET)
    需要多个的话用花括号括起来就行了,之间用逗号分割。

  5. 限制参数映射
    也是通过属性指定,params 属性:指定 request 中必须包含某些参数值是,才让该方法处理。
    过个参数还是用花括号括起来,一般有下面的几种形式:
    "!user" :参数不能包含 user;
    "userID != 1" :参数必须包含 userID 但是不能为 1;
    {"user","age"} :参数必须包含 user 和 age;

然后属性这样就已经说了三个了,value、method、params;其它是的:
headers: 指定 request 中必须包含某些指定的 header 值,才能让该方法处理请求;举个栗子:
@RequestMapping (value= "testHeaders" , headers={ "host=localhost" , "Accept" }) ,这样就表示只有当请求头包含 Accept 信息,且请求的 host 为 localhost 的时候才能正确的访问到此方法。
consumes: 指定处理请求的提交内容类型(Content-Type),例如 application/json 、 text/html;
produces: 指定返回的内容类型,仅当 request 请求头中的 (Accept) 类型中包含该指定类型才返回;

@PathVariable

用于将请求 URL 中的模板变量映射到功能处理方法的参数上,即取出 uri 模板中的变量作为参数。
上面已经使用过了,需要注意的是 value 参数不能省略!虽然省略了可以执行,但是当正常编译( IDE 一般为 debug 模式)时,方法的传入参数不会被记录,所以会报错。
许多注解都是这个机制,所以最好是写上 value ,万无一失,总之就是不要依赖变量名。
Eclipse 中的 Java Compiler 中的 add variable attributes to generated class files 默认是开启的。

@requestParam

主要用于在 SpringMVC 后台控制层获取参数,类似一种是 request.getParameter("name"),它有三个常用参数:
defaultValue = “0”、required = false、 value = “isApp”;
defaultValue 表示设置默认值,required 通过 boolean 设置是否是必须要传入的参数(默认为 true),value 值表示接受的传入的参数类型。
具体用在哪里可以参考上面的 PathVariable ,差不多的,这样就直接把请求参数放进方法参数里了,在方法里使用非常方便了,对于基本数据类型,记得一定要写,不要省略。
它可以直接对参数进行 POJO 实体注入、转换数组、List 等集合,并且不会出现空指针异常,非常的方便!

@CookieValue

可以把 Request header 中关于 cookie 的值绑定到方法的参数上,例如获取 cookie 中的 JSESSIONID 的值:
public void displayHeaderInfo(@CookieValue("JSESSIONID") String cookie) {}
和上面两个注解也没啥差别,都是获取数据的。

@RequestHeader

和 CookieValue 等注解类似,就是将 HttpServletRequest 头信息到 Controller 的方法参数上,比如 @RequestHeader("host") 就是获取 host 头信息;
不同的是,在 RequestHeader 中是不区分大小写的,但在 @PathVariable 、 @RequestParam 和 @CookieValue 中都是大小写敏感的。

@ResponseBody

作用:该注解用于将 Controller 的方法返回的对象,通过适当的 HttpMessageConverter 转换为指定格式后,写入到 Response 对象的 body 数据区。
使用时机:返回的数据不是 html 标签的页面,而是其他某种格式的数据时(如 json、xml等)使用;默认为 JSON;
补充:为什么默认是 JSON ,在注解驱动中已经定义了,如果项目中有 Jackson 的依赖,那么会自动注册其转换器做转换输出。

@RequestBody

和上面长的很像…..我都看错了,它们是一对。
该注解常用来处理请求的 Content-Type 不是application/x-www-form-urlencoded编码的内容,例如 application/json, application/xml 等.
它是通过使用 HandlerAdapter 配置的 HttpMessageConverters 来解析 post data body,然后绑定到相应的 bean 上的。
因为配置有 FormHttpMessageConverter,所以也可以用来处理 application/x-www-form-urlencoded 的内容,处理完的结果放在一个 MultiValueMap<String, String> 里,这种情况在某些特殊需求下使用,详情查看FormHttpMessageConverter API.

@ResponseStatus

业务异常可以使用 @ResponseStatus 来注解。当异常被抛出时,ResponseStatusExceptionResolver 会设置相应的响应状态码。DispatcherServlet 会默认注册一个 ResponseStatusExceptionResolver 以供使用。
它有两个属性,value 属性是 http 状态码,比如 404,500 等。reason 是错误信息:
@ResponseStatus(value=HttpStatus.FORBIDDEN,reason="用户不匹配")
通常它是修饰类的,然后在抛出异常的时候就会起作用了:

1
2
3
4
5
6
7
8
9
@ResponseStatus(value=HttpStatus.FORBIDDEN,reason="用户不匹配")
public class UserNotMatchException extends RuntimeException{}

@RequestMapping("/testResponseStatus")
public String testResponseStatus(int i){
if(i==0)
throw new UserNotMatchException();
return "hello";
}

这样,用户看到的异常界面正是我们自己定义的异常,而不再是一大堆用户看不懂的代码,但如果用这个注解来修饰方法,那么无论它执行方法过程中有没有异常产生,用户都会得到异常的界面。而目标方法正常执行

@ModelAttribute和@SessionAttributes

该 Controller 的所有方法在调用前,先执行此 @ModelAttribute 方法;
可以把这个 @ModelAttribute 特性,应用在 BaseController 当中,所有的 Controller 继承 BaseController,即可实现在调用 Controller 时,先执行 @ModelAttribute 方法。
@SessionAttributes 即将值放到 session 作用域中,写在 class 上面有效。


SpringMVC 支持使用 @ModelAttribute 和 @SessionAttributes 在不同的模型(model)和控制器之间共享数据。 @ModelAttribute 主要有两种使用方式,一种是标注在方法上,一种是标注在 Controller 方法参数上。
当 @ModelAttribute 标记在方法上的时候,该方法将在处理器方法执行之前执行,然后把返回的对象存放在 session 或模型属性中,属性名称可以使用 @ModelAttribute("attributeName") 在标记方法的时候指定,若未指定,则使用返回类型的类名称(首字母小写)作为属性名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Controller
@RequestMapping ( "/myTest" )
public class MyController {

@ModelAttribute ( "hello" )
public String getModel() {
System. out .println( "-------------Hello---------" );
return "world" ;
}

@ModelAttribute ( "intValue" )
public int getInteger() {
System. out .println( "-------------intValue---------------" );
return 10;
}

@RequestMapping ( "sayHello" )
public void sayHello( @ModelAttribute ( "hello" ) String hello, @ModelAttribute ( "intValue" ) int num, @ModelAttribute ( "user2" ) User user, Writer writer, HttpSession session) throws IOException {
writer.write( "Hello " + hello + " , Hello " + user.getUsername() + num);
writer.write( "\r" );
Enumeration enume = session.getAttributeNames();
while (enume.hasMoreElements())
writer.write(enume.nextElement() + "\r" );
}

@ModelAttribute ( "user2" )
public User getUser(){
System. out .println( "---------getUser-------------" );
return new User(3, "user2" );
}
}

当我们请求 /myTest/sayHello.do 的时候使用 @ModelAttribute 标记的方法会先执行,然后把它们返回的对象存放到模型中。最终访问到 sayHello 方法的时候,使用 @ModelAttribute 标记的方法参数都能被正确的注入值.
这时,数据都是放在模型中,而不是存放在 session 属性中,如果想要也同时放进 Session 中,那么需要加 @SessionAttributes 注解:

1
2
3
4
@Controller
@RequestMapping ("/myTest")
@SessionAttributes (value={ "intValue" , "stringValue" }, types={User. class })
public class MyController {...}

在上面代码中我们指定了属性为 intValue 或 stringValue 或者类型为 User 的都会放到 Session 中;但是当访问时,Session 中并没有值,第二次访问才有,也就是说:等处理器方法执行完成后 Spring 才会把模型中对应的属性添加到 session 中,所以第二次访问才能拿到值。

绑定Servlet内置对象

使用注解后确实简单多了,但有一个问题,怎么才能获得 Servlet 中的内置对象呢,比如 request、response、session 之类的,工具类?接口?
完全不需要,需要什么定义什么就 OK 了,直接定义在 handler 中的方法参数中即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping(value = "test/show")
public ModelAndView show (HttpServletRequest request,HttpServletResponse response,HttpSession session){
ModelAndView mv = new ModelAndView();
mv.setViewName("show");

StringBuilder sb = new StringBuilder();
sb.append("request = " + request);
sb.append("<br/>response = " + response);
sb.append("<br/>session = " + session);

mv.addObject("msg", sb.toString());
return mv;
}

似不似非常简单方便,这种做法确实非常棒!

POJO对象绑定

对于“实体”对象,也是非常的简单,和上面一样,直接在方法参数里写就行了,会自动把请求中的参数注入到这个 POJO 中去;
例如,方法为:public ModelAndView show (User user){} ;请求的时候直接 id=1&name=abc 这样就可以了。
然后,还有一个对于我们来说最烦人的问题,中文乱码问题…..,如何解决后面再补充


如果提交的是集合,就是这样:<input name="users[0].name" /> ;这样提交的不是一个集合或者数组嘛,但是接收不能使用 public ModelAndView show (List<user> users){} 的形式来接收,从上面的简单栗子也可以看得出,填充对象的时候应该是去参数对象中去找相应的 setter 方法进行注入(除非使用 @requestParam 注解,适用于基本数据类型,大概是这样),所以,你需要将 List 进行对象包装后才可以使用
虽然这种使用形式并不会经常用到。

参考

http://www.cnblogs.com/leskang/p/5445698.html

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~