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
中運用反射機制調用處理器的具體處理方法.
處理器的類型可以是任何對象類型, 這就使得, 來自第三方框架的處理器可以在無編碼的前提下集成進來, 也支持基於注解的、不需要實現任何接口的處理器類型.