springmvc学习笔记(简介及使用)
工作之余, 回顾了一下springmvc的相关内容, 这次也为后面复习什么的做个标记, 也希望能与大家交流学习, 通过回帖留言等方式表达自己的观点或学习心得.
本文如若有误, 也敬请大家不吝赐教.
1 mvc框架通常要做哪些事情
- 将URL映射到Java类或Java方法
- 封装用户提交的数据
- 处理请求, 调用相关的业务处理, 并封装响应的数据
- 将要相应的数据进行渲染
2 springmvc优点和特点
- 与spring无缝集成(IoC, AOP)
- 约定优于配置
- 性能较struts2好
- 设计中的角色或职责划分明确
- Restful
- JUnit测试
- 异常处理
- 本地化, 国际化
- 数据验证, 类型转换等
- 拦截器
- 使用的人已经相当多, 使用的公司也相当多
- 简单, 便捷, 易学
3 springmvc处理请求流程
springmvc框架基于请求驱动, 所有设计都围绕一个*Servlet展开, 它负责将请求分发给各处理器(页面控制器, Controller). 下图中展示了springmvc处理请求的流程, 图中的Front Controller(前端控制器)正是springmvc的DispatcherServlet
; Controller称为处理器或应用控制器或页面控制器, 由它来处理具体的请求, 返回模型数据; View Template为具体视图, 用于展示数据, 响应请求.
具体处理请求步骤:
- 用户发送请求, 被前端控制器拦截, 前端控制器根据请求的信息选择相应的页面控制器, 并将请求委托给此页面控制器来处理.
- 页面控制器接收到请求后, 首先收集并绑定请求参数到一个命令对象(表单对象)中, 并进行验证转换等操作, 然后将命令对象(表单对象)委托给业务对象进行处理, 最后返回一个
ModelAndView
对象. - 前端控制器根据返回的视图名, 选择相应的视图进行渲染, 并将模型数据传入到视图中以便展示.
- 前端控制器将响应结果返回给用户.
至此, 整个请求流程结束. 当然还有一些细节的问题, 需要我们了解, 比如: 前端控制器如何选择页面控制器, 前端控制器如何根据页面控制器返回的视图名选择相应的视图进行渲染, 等等. 带着这些问题, 我们将springmvc处理请求的流程图转换为架构图讨论一下.
先上一段主要代码哈,
/**
* Process the actual dispatching to the handler.
* <p>The handler will be obtained by applying the servlet's HandlerMappings in order.
* The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
* to find the first that supports the handler class.
* <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers
* themselves to decide which methods are acceptable.
* @param request current HTTP request
* @param response current HTTP response
* @throws Exception in case of any kind of processing failure
*/
/**
* 处理实际的请求分发到处理器.
* 要获得具体的Handler, 需要先使用servlet的HandlerMappings.
* 要获得HandlerAdpter, 需要先在servlet加载的各HandlerAdapter中查找, 找到第一个支持此Handler的Adapter.
* 所有的HTTP方法都通过这个方法来处理的. 这个方法中, 由HandlerAdapter或Handler自己来决定哪些方法可以被调用.
* @param request current HTTP request
* @param response current HTTP response
* @throws Exception in case of any kind of processing failure
*/
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// 检查请求是否为multipart(文件上传)
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
// 图中2,3两步, 通过HandlerMappsing映射, 获取处理请求的Handler
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
// 图中步骤4, 将Handler包装成Adapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
// 前置拦截器
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
// 图中5,6,7步骤, 由HandlerAdapter调用真正的处理器处理请求, 并返回ModelAndView对象
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// 后置拦截器
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
// 图中8,9,10,11步骤, 处理Handler的处理结果, 这个结果可能是一个ModelAndView对象, 还可能是一个异常
// 第8,9步, 由viewResolver解析视图
// viewResolver.resolveViewName(viewName, locale)
// 第10, 11步, 传入Model, 并渲染视图
// view.render(mv.getModelInternal(), request, response);
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Error err) {
triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err);
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
// 完成时拦截器
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
- 步骤①,
DispatcherServlet
作为前端控制器, 统一的请求接收点, 控制全局的请求流程. 接收到用户请求, 自己不做处理, 而是将请求委托给其他的处理器进行处理. - 步骤②③,
DispatcherServlet
通过HandlerMapping
(处理映射器), 将请求映射为一个HandlerExecutionChain
对象, 其中包括了页面控制器和对其配置的拦截器. - 步骤④,
DispatcherServlet
通过获得的Handler(处理器, 页面控制器, Controller), 查找一个合适的HandlerAdapter
(处理器适配器), 通过这个HandlerAdapter
调用Handler实际处理请求的方法. - 步骤⑤, 提取请求中的模型数据, 调用Handler实际处理请求的方法. 在调用方法时, 填充参数过程中, spring会根据配置做一些工作, 如: 数据转换, 数据格式化, 数据验证等.
- 步骤⑥⑦, Handler执行完成后, 将返回一个
ModelAndView
对象给DispatherServlet
.ModelAndView
对象中包含逻辑视图名或逻辑视图名和模型. - 步骤⑧, 根据
ModelAndView
对象选择一个合适的ViewResolver(视图解析器). - 步骤⑨,
ViewResolver
将ModelAndView
中的逻辑视图名解释成View
对象. ViewResolver
也是接口, 同样采用了策略模式, 这样就很容易切换其他的视图类型. - 步骤⑩⑪, 渲染视图时, 将
Model
数据传入视图中, 这里的Model数据是一个Map, 容易与各种视图类型相结合. - 步骤⑫, 最后, 由
DispatcherServlet
将最终的响应结果返回给用户.
通过这些步骤, springmvc依赖几个对象共同完成了请求到响应的工作流程, 对于开发者来说, 这些对象是不可见的, 开发者只需要关心Handler处理器(页面控制器)中对请求的处理业务即可.
4 第一个示例 Hello World
4.1 创建一个web工程
此示例项目使用maven管理, pom.xml依赖包配置如下:
<!-- spring版本 -->
<org.springframework.version>4.2.3.RELEASE</org.springframework.version>
<!-- spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${org.springframework.version}</version>
<!-- 将commons-loggin换成了下面的logback -->
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- spring-mvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${org.springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${org.springframework.version}</version>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.6.1</version>
</dependency>
4.2 spring监听配置及springmvc前端控制器DispatcherServlet配置
在web.xml文件中, 加入spring监听以及DispatcherServlet
前端控制器相关配置, 如下:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 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-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<!-- 使此前端控制器拦截所有请求 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
对于DispatcherServlet
的配置,
- load-on-startup: 启动容器时初始化
DispatcherServlet
- url-pattern: 说明哪些请求会被
DispatcherServlet
所处理 - contextConfigLocation: springmvc配置文件, 默认文件为/WEB-INF/[servletName]-servlet.xml
4.3 applicationContext.xml、springmvc-servlet.xml、logback.xml
applicationContext.xml为根上下文配置文件, 暂未做bean的定义.
logback.xml为日志配置文件, 不做描述.
然后是springmvc的上下文配置springmvc-servlet.xml
<?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"
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.xsd">
<!-- 处理映射器 -->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"></bean>
<!-- 处理器适配器 -->
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"></bean>
<!-- 视图解释器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"></bean>
<!-- 定义一个bean, 即处理器(或控制器), 映射"/hello"请求 -->
<bean name="/hello" class="com.lizj.controller.HelloController"></bean>
</beans>
配置文件中, 配置了一个处理映射器BeanNameUrlHandlerMapping
, 这样可以Bean的name属性为url进行查找; 同时配置了处理器适配器SimpleControllerHandlerAdapter
, 以完成对处理器中实际处理方法的调用; 配置了视图解析器InternalResourceViewResolver
来解析视图, 将视图展示给用户.
最后定义了页面控制器HelloController
, 将其映射到了请求"/hello"上.
4.4 页面控制器HelloController
package com.lizj.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
/**
* HelloController作为一个页面控制器, 是一个实现Controller接口的页面控制器
* 它可以处理一个单一请求
*/
public class HelloController implements Controller {
/**
* 实现Controller接口中定义的方法
* 此方法返回一个ModelAndView对象,
* 此对象中, 包含着视图名和模型
*/
@Override
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
ModelAndView mv = new ModelAndView();
mv.addObject("message", "everyone");
mv.setViewName("/WEB-INF/jsp/hello.jsp");
return mv;
}
}
在spring2.5以前的版本, 要实现一个页面控制器中通过实现Controller
接口, 此接口中有一个必定要实现的方法:
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
方法参数为HttpServletRequest
和HttpServletResponse
, 请求处理完成后, 返回ModelAndView
对象. 但是它只能处理一个单一的请求. 后面的示例中, 会介绍利用注解来定义Controller.
4.5 hello.jsp
在路径/WEB-INF/jsp下新建hello.jsp文件, 其主要代码如下:
<body>
Hello ${requestScope.message} <br>
</body>
访问页面, 显示Hello everyone, 表示访问成功.
4.6 基于注解的Controller示例
spring2.5引入了注解, 以@Controller
和@RequestMapping
定义处理器, 通过这种方式实现的处理器既不需要继承某个父类, 也不需要实现某个接口.
修改HelloController.java类, 如下:
package com.lizj.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
/**
* 基于注解的控制器
* 并可以接收多个请求
* @Controller注解表示此类是一个控制器
*/
Controller
public class HelloController {
/** 计数 */
private int count = 0;
/**
* RequestMapping注解用来映射请求的URL和方法
* 此处映射请求"/hello"
*/
@RequestMapping(value="/hello")
public ModelAndView hello() {
ModelAndView mv = new ModelAndView();
mv.addObject("message", "everyone");
mv.setViewName("/WEB-INF/jsp/hello.jsp");
System.out.println("count: " + count++);
return mv;
}
/**
* 映射请求"/hi"
*/
@RequestMapping(value="/hi")
public ModelAndView hi(String param) {
ModelAndView mv = new ModelAndView();
mv.addObject("message", param);
mv.setViewName("/WEB-INF/jsp/hi.jsp");
return mv;
}
}
@Controller
: 标识类为处理器类(页面控制器).@RequestMapping
: 处理请求地址映射, 可映射到类, 也可映射到方法.
value="/hello"表示请求/hello由此方法处理
修改springmvc-servlet.xml配置文件
<?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"
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.xsd">
<!-- spring自动扫描包路径com.lizj.controller下的所有包和类 -->
<context:component-scan base-package="com.lizj.controller"></context:component-scan>
<!-- annotation处理映射器 -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"></bean>
<!-- annotation处理器适配器 -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"></bean>
<!-- 视图解释器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"></bean>
</beans>
由于使用了注解, 所以不再使用配置文件来定义bean. spring将自动扫描指定路径下的类, 查找基于注解的控制器类.
此配置文件中还定义了注解类型的处理映射器RequestMappingHandlerMapping
, 根据请求处理映射. 注解类型的控制器适配器RequestMappingHandlerAdapter
, 由它来完成@RequestMapping
注解方法的调用.
其实, 处理映射器和处理器适配器的使用可以更简便, 这里在是为了把springmvc所有组件都展示出来.
从两个示例中可以看出, 实现Controller
接口的页面控制器只能处理一个单一请求, 而@Controller
注解的页面控制器可以支持多个请求, 并且支持多样的方法名和方法签名, 更加灵活.
需要注意的是, 页面控制器是一个spring容器中的bean, 默认单例, 所以要注意在页面控制器类中使用成员变量的场景. 示例代码中的
count
成员变量就是为了说明这个问题.
4.7 关于...
4.7.1 上下文关系
在springmvc中, 每个DispatcherServlet
实例都会持有一个自己的上下文对象WebApplicationContext
, 而它又继承了根上下文(Root WebApplicationContext
), 从而继承了根上下文中定义的bean. 这些bean可以在DispatcherServlet
实例中被重载, 也可以在DispatcherServlet
实例定义新的bean.
当应用中只需要一个DispatcherServlet
时, 只配置一个根context对象也是可行的
此时, DispatcherServlet
初始化参数中, 可以配置一个空的contextConfigLocation来实现, 如下:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 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></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<!-- 使此前端控制器拦截所有请求 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
4.7.2 HandlerMapping(处理映射器)
HandlerMapping
是个接口, 那就是可扩展的. 它的作用就是根据请求信息, 获取具体处理这个请求的处理器. 这个过程需要两个步骤: 一是注册处理器到HandlerMapping
中, 二是根据请求信息从已注册的处理器中查找对应的处理器.
在HandlerMapping
接口的实现中, 都定义了请求与处理器之间的映射关系. 比如BeanNameUrlHandlerMapping
定义了URLs和bean的name属性之间的映射关系, 而RequestMappingHandlerMapping
则主要是根据注解@RequestMapping
维护映射关系的.
对于HandlerMapping
接口中定义的方法
public abstract HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
返回值的类型为HandlerExecutionChain
, 从命名上也可以看出, 它是一个执行链, 这与struts2的设计是一样的, 实际的Action被许多一层层包装着. 从HandlerExecutionChain
原码可以看出, 一堆拦截器和一个实际的处理器. 在调用真正的处理器之前, 会遍历所有拦截器, 调用其preHandle
方法, 然后再调用真正的处理器对象.
4.7.3 HandlerAdapter(处理器适配器)
springmvc是通过HandlerAdapter
调用实际的处理器以及处理方法的.
HandlerAdapter
从命名上可以看出, 是适配器模式的应用, 为的就是可以使用不同类型的处理器, 比如实现Controller
接口的处理器或者通过注解@Controller
声明的处理器. 换句话说, 每种类型的处理器, 都会对应一个与自身类型匹配的处理器适配器. 它是个接口, 允许扩展. DispatcherServlet
访问处理器都是通过这个接口, 所以DispatcherServlet
不能包含针对任一类型处理器的特有代码.
DispatcherServlet
会根据处理器的类型查找HandlerAdapter
, 具体是通过HandlerAdapter
的supports
方法, 找到匹配的HandlerAdapter
之后, 会使用自身定义的策略, 确定处理器的具体方法, 并处理传入参数, 然后执行自身的handle
方法, 而在handle
中运用反射机制调用处理器的具体处理方法.
处理器的类型可以是任何对象类型, 这就使得, 来自第三方框架的处理器可以在无编码的前提下集成进来, 也支持基于注解的、不需要实现任何接口的处理器类型.