1.Spring背景
1.1.Spring四大原則:
- 使用POJO進行輕量級和最侵入式開發;
- 通過依賴注入和基于借口編程實現松耦合;
- 通過AOP和默認習慣進行聲明式編程;
- 使用AOP和模板(template)減少模塊化代碼
Spring所有功能的設計和實現都是基于這四大原則的。
1.2.依賴注入
容器負責創建對象和維護對象之間的依賴關系,而不是通過對象本身負責自己的創建和解決自己的依賴。依賴注入的主要目的是解耦,體現一種“組合”的理念,如果你希望你的類具備某項功能的時候,是繼承自具有此項功能的父類好,還是組合另一個具有這個功能的類好,答案不言而喻。
Spring IoC容器負責創建Bean,并將功能類Bean注入到你需要的Bean中。Spring提供xml配置、注解、Java配置、groovy配置實現Bean的創建和注入。
聲明Bean的注解:
@Component組件:沒有明確的角色
@Service:業務邏輯層使用
@Repository:數據訪問層(DAO)使用
@Controller:展現層使用
注入Bean的注解
@Autowire:Spring提供的注解
@Inject:JSR-330標準提供的注解
@Resource:JSR-250標準提供的注解
一般情況下通用
1.3.Java配置
Spring4.x推薦的配置方式,也是Spring Boot推薦的配置方式,可以完全替代xml配置。
@Configuration:Java配置配置方式,聲明當前類是一個配置類,與使用xml配置文件效果一樣
@ComponentScan 自動掃描報名下所有使用@Service @Component @Repository @Controller的類并注冊為Bean
@Bean注解在方法上,聲明當前方法的返回值為一個Bean
2.MVC模型
MVC模型是一種架構型的模式,本身不引入新功能,只是幫助我們將開發的結構組織的更加合理,使展示與模型分離、流程控制邏輯、業務邏輯調用與展示邏輯分離。
Web端的開發發展流程如下圖
這里只講述服務到工作者:Front Controller + Application Controller + Page Controller + Context
運行流程如下:
我們回顧了整個web開發架構的發展歷程,可能不同的web層框架在細節處理方面不同,但的目的是一樣的:
1、 干凈的web表現層:
- 模型和視圖的分離;
- 控制器中的控制邏輯與功能處理分離(收集并封裝參數到模型對象、業務對象調用);
- 控制器中的視圖選擇與具體視圖技術分離。
2、 輕薄的web表現層:
- 做的事情越少越好,薄薄的,不應該包含無關代碼;
- 只負責收集并組織參數到模型對象,啟動業務對象的調用;
- 控制器只返回邏輯視圖名并由相應的應用控制器來選擇具體使用的視圖策略;
- 盡量少使用框架特定API,保證容易測試。
3.Spring MVC模型
Spring Web MVC是一種基于Java的實現了Web MVC設計模式的請求驅動類型的輕量級Web框架,即使用了MVC架構模式的思想,將web層進行職責解耦,基于請求驅動指的就是使用請求-響應模型,框架的目的就是幫助我們簡化開發
Spring Web MVC也是服務到工作者模式的實現,但進行可優化。
- 前端控制器是DispatcherServlet;
- 應用控制器其實拆為處理器映射器(Handler Mapping)進行處理器管理和視圖解析器****(View Resolver)進行視圖管理;
- 頁面控制器/動作/處理器為Controller****接口(僅包含ModelAndView handleRequest(request, response) 方法)的實現(也可以是任何的POJO類);
- 支持本地化(Locale)解析、主題(Theme)解析及文件上傳等;
- 提供了非常靈活的數據驗證、格式化和數據綁定機制;
- 提供了強大的約定大于配置(慣例優先原則)的契約式編程支持。
3.1.Spring Web MVC能做什么
- 讓我們能非常簡單的設計出干凈的Web層和薄薄的Web層;
- 進行更簡潔的Web層的開發;
- 天生與Spring框架集成(如IoC容器、AOP等);
- 提供強大的約定大于配置的契約式編程支持;
- 能簡單的進行Web層的單元測試;
- 支持靈活的URL到頁面控制器的映射;
- 非常容易與其他視圖技術集成,如Velocity、FreeMarker等等,因為模型數據不放在特定的API里,而是放在一個Model里(Map數據結構實現,因此很容易被其他框架使用);
- 非常靈活的數據驗證、格式化和數據綁定機制,能使用任何對象進行數據綁定,不必實現特定框架的API;
- 提供一套強大的JSP標簽庫,簡化JSP開發;
- 支持靈活的本地化、主題等解析;
- 更加簡單的異常處理;
- 對靜態資源的支持;
- 支持Restful風格。
3.2.Spring Web MVC處理請求的流程
具體執行步驟如下:
1、 首先用戶發送請求—>前端控制器,前端控制器根據請求信息(如URL)來決定選擇哪一個頁面控制器進行處理并把請求委托給它,即以前的控制器的控制邏輯部分;圖中的1、2步驟;
2、 頁面控制器接收到請求后,進行功能處理,首先需要收集和綁定請求參數到一個對象,這個對象在Spring Web MVC中叫命令對象,并進行驗證,然后將命令對象委托給業務對象進行處理;處理完畢后返回一個ModelAndView(模型數據和邏輯視圖名);圖2-1中的3、4、5步驟;
3、 前端控制器收回控制權,然后根據返回的邏輯視圖名,選擇相應的視圖進行渲染,并把模型數據傳入以便視圖渲染;圖2-1中的步驟6、7;
4、 前端控制器再次收回控制權,將響應返回給用戶,圖2-1中的步驟8;至此整個結束
3.3.疑惑(后面解答)
1、 請求如何給前端控制器?
2、 前端控制器如何根據請求信息選擇頁面控制器進行功能處理?
3、 如何支持多種頁面控制器呢?
4、 如何頁面控制器如何使用業務對象?
5、 頁面控制器如何返回模型數據?
6、 前端控制器如何根據頁面控制器返回的邏輯視圖名選擇具體的視圖進行渲染?
7、 不同的視圖技術如何使用相應的模型數據?
4.Spring核心框架
核心架構的具體流程步驟如下:
1、 用戶發送請求 -> DispatcherServlet,前端控制器收到請求后自己不進行處理,而是委托給其他的解析器進行處理,作為統一訪問點,進行全局的流程控制;
2、 DispatcherServle -> HandlerMapping:HandlerMapping將會把請求映射為HandlerExecutionChain對象(包含一個Handler處理器(頁面控制器)對象、多個HandlerInterceptor攔截器)對象,通過這種策略模式,很容易添加新的映射策略;
3、 DispatcherServlet -> HandlerAdapter:HandlerAdapter將會把處理器包裝為適配器,從而支持多種類型的處理器,即適配器設計模式的應用,從而很容易支持很多類型的處理器;
4、 HandlerAdapter -> 處理器:HandlerAdapter將會根據適配的結果調用真正的處理器的功能處理方法,完成功能處理;并返回一個ModelAndView對象(包含模型數據、邏輯視圖名);
5、 ModelAndView -> ViewResolver:ViewResolver將把邏輯視圖名解析為具體的View,通過這種策略模式,很容易更換其他視圖技術;
6、 渲染View :View會根據傳進來的Model模型數據進行渲染,此處的Model實際是一個Map數據結構,因此很容易支持其他視圖技術;
7、 DispatcherServlet****響應:返回控制權給DispatcherServlet,由DispatcherServlet返回響應給用戶,到此一個流程結束。
此處我們只是講了核心流程,沒有考慮攔截器、本地解析、文件上傳解析等。
現在可以回答上述提出的問題:
1、 請求如何給前端控制器?
答:這個應該在web.xml中進行部署描述
2、 前端控制器如何根據請求信息選擇頁面控制器進行功能處理?
答:我們需要配置HandlerMapping進行映射
3、 如何支持多種頁面控制器呢?
答:配置HandlerAdapter從而支持多種類型的頁面控制器
4、 頁面控制器如何使用業務對象?
答:利用Spring IoC容器的依賴注入功能
5、 頁面控制器如何返回模型數據?
答:使用ModelAndView返回
6、 前端控制器如何根據頁面控制器返回的邏輯視圖名選擇具體的視圖進行渲染?
答: 使用ViewResolver進行解析
7、 不同的視圖技術如何使用相應的模型數據?
答:因為Model是一個Map數據結構,很容易支持其他視圖技術
org.springframework.web.servlet.DispatcherServlet # doDispatch :
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 {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.(通過HandlerMapping映射獲取)
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
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. 適配器執行處理器
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", 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);
}
}
}
}
通過閱讀源碼,可以看出具體的核心開發步驟:
1、 DispatcherServlet在web.xml中的部署描述,從而攔截請求到Spring Web MVC
2、 HandlerMapping的配置,從而將請求映射到處理器
3、 HandlerAdapter的配置,從而支持多種類型的處理器
4、 ViewResolver的配置,從而將邏輯視圖名解析為具體視圖技術
5、 處理器(頁面控制器)的配置,從而進行功能處理
5.注解式開發
Spring2.5引入注解式處理器支持,通過@Controller 和 @RequestMapping注解定義我們的處理器類。并且提供了一組強大的注解:
需要通過處理器映射DefaultAnnotationHandlerMapping和處理器適配器AnnotationMethodHandlerAdapter來開啟支持@Controller 和 @RequestMapping注解的處理器。
- @Controller:用于標識是處理器類;
- @RequestMapping:請求到處理器功能方法的映射規則;
- @RequestParam:請求參數到處理器功能處理方法的方法參數上的綁定;
- @ModelAttribute:請求參數到命令對象的綁定;
- @SessionAttributes:用于聲明session級別存儲的屬性,放置在處理器類上,通常列出模型屬性(如@ModelAttribute)對應的名稱,則這些屬性會透明的保存到session中;
- @InitBinder:自定義數據綁定注冊支持,用于將請求參數轉換到命令對象屬性的對應類型;
Spring3.0引入RESTful架構風格支持(通過@PathVariable注解和一些其他特性支持),且又引入了更多的注解支持:
- @CookieValue:cookie數據到處理器功能處理方法的方法參數上的綁定;
- @RequestHeader:請求頭(header)數據到處理器功能處理方法的方法參數上的綁定;
- @RequestBody:請求的body體的綁定(通過HttpMessageConverter進行類型轉換);
- @ResponseBody:處理器功能處理方法的返回值作為響應體(通過HttpMessageConverter進行類型轉換);
- @ResponseStatus:定義處理器功能處理方法/異常處理器返回的狀態碼和原因;
- @ExceptionHandler:注解式聲明異常處理器;
- @PathVariable:請求URI中的模板變量部分到處理器功能處理方法的方法參數上的綁定,從而支持RESTful架構風格的URI;
6.DispatcherServlet
DispatcherServlet主要用作職責調度工作,本身主要用于控制流程,主要職責如下:
1、 文件上傳解析,如果請求類型是multipart將通過MultipartResolver進行文件上傳解析;
2、 通過HandlerMapping,將請求映射到處理器(返回一個HandlerExecutionChain,它包括一個處理器、多個HandlerInterceptor攔截器);
3、 通過HandlerAdapter支持多種類型的處理器(HandlerExecutionChain中的處理器);
4、 通過ViewResolver解析邏輯視圖名到具體視圖實現;
5、 本地化解析;
6、 渲染具體的視圖等;
7、 如果執行過程中遇到異常將交給HandlerExceptionResolver來解析。
DispatcherServlet主要負責流程的控制,而且在流程中的每個關鍵點都是很容易擴展的。
DispatcherServlet默認使用WebApplicationContext作為上下文,因此我們來看一下該上下文中有哪些特殊的Bean:
1、 Controller:處理器/頁面控制器,做的是MVC中的C的事情,但控制邏輯轉移到前端控制器了,用于對請求進行處理;
2、 HandlerMapping:請求到處理器的映射,如果映射成功返回一個HandlerExecutionChain對象(包含一個Handler處理器(頁面控制器)對象、多個HandlerInterceptor攔截器)對象;如BeanNameUrlHandlerMapping將URL與Bean名字映射,映射成功的Bean就是此處的處理器;
3、 HandlerAdapter:HandlerAdapter將會把處理器包裝為適配器,從而支持多種類型的處理器,即適配器設計模式的應用,從而很容易支持很多類型的處理器;如SimpleControllerHandlerAdapter將對實現了Controller接口的Bean進行適配,并且掉處理器的handleRequest方法進行功能處理;
4、 ViewResolver:ViewResolver將把邏輯視圖名解析為具體的View,通過這種策略模式,很容易更換其他視圖技術;如InternalResourceViewResolver將邏輯視圖名映射為jsp視圖;
5、 LocalResover:本地化解析,因為Spring支持國際化,因此LocalResover解析客戶端的Locale信息從而方便進行國際化;
6、 ThemeResovler:主題解析,通過它來實現一個頁面多套風格,即常見的類似于軟件皮膚效果;
7、 MultipartResolver:文件上傳解析,用于支持文件上傳;
8、 HandlerExceptionResolver:處理器異常解析,可以將異常映射到相應的統一錯誤界面,從而顯示用戶友好的界面(而不是給用戶看到具體的錯誤信息);
9、 RequestToViewNameTranslator:當處理器沒有返回邏輯視圖名等相關信息時,自動將請求URL映射為邏輯視圖名;
10、 FlashMapManager:用于管理FlashMap的策略接口,FlashMap用于存儲一個請求的輸出,當進入另一個請求時作為該請求的輸入,通常用于重定向場景。
ContextLoaderListener初始化的上下文加載的Bean是對于整個應用程序共享的,不管是使用什么表現層技術,一般如DAO層、Service層Bean;
DispatcherServlet初始化的上下文加載的Bean是只對Spring Web MVC有效的Bean,如Controller、HandlerMapping、HandlerAdapter等等,該初始化上下文應該只加載Web相關組件。
Controller
Controller控制器,是MVC中的部分C,為什么是部分呢?因為此處的控制器主要負責功能處理部分:
1、 收集、驗證請求參數并綁定到命令對象;
2、 將命令對象交給業務對象,由業務對象處理并返回模型數據;
3、 返回ModelAndView(Model部分是業務對象返回的模型數據,視圖部分為邏輯視圖名)。
還記得DispatcherServlet嗎?主要負責整體的控制流程的調度部分:
1、負責將請求委托給控制器進行處理;
2、根據控制器返回的邏輯視圖名選擇具體的視圖進行渲染(并把模型數據傳入)。
因此MVC中完整的C(包含控制邏輯+功能處理)由(DispatcherServlet + Controller)組成。
Controller繼承關系圖
7.攔截器
請求的映射分為如下幾種:
- URL路徑映射:使用URL映射請求到處理器的功能處理方法;
- 請求方法映射限定:如限定功能處理方法只處理GET請求;
- 請求參數映射限定:如限定只處理包含“abc”請求參數的請求;
- 請求頭映射限定:如限定只處理“Accept=application/json”的請求。
可以通過在一個POJO類上放置@Controller或@RequestMapping,即可把一個POJO類變身為處理器;
@RequestMapping(value = "/hello") 請求URL(/hello) 到 處理器的功能處理方法的映射;
模型數據和邏輯視圖名的返回。
package cn.javass.chapter6.web.controller;
//省略import
@Controller // 或 @RequestMapping ①將一個POJO類聲明為處理器
public class HelloWorldController {
@RequestMapping(value = "/hello") //②請求URL到處理器功能處理方法的映射
public ModelAndView helloWorld() {
//1、收集參數
//2、綁定參數到命令對象
//3、調用業務對象
//4、選擇下一個頁面
ModelAndView mv = new ModelAndView();
//添加模型數據 可以是任意的POJO對象
mv.addObject("message", "Hello World!");
//設置邏輯視圖名,視圖解析器會根據該名字解析到具體的視圖頁面
mv.setViewName("hello");
return mv; //③ 模型數據和邏輯視圖名
}
}
如果您使用的是Spring3.1之前版本,開啟注解式處理器支持的配置為:
<!—Spring3.1之前的注解 HandlerMapping -->
<bean
class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/>
<!—Spring3.1之前的注解 HandlerAdapter -->
<bean
class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>
如果您使用的Spring3.1開始的版本,建議使用RequestMappingHandlerMapping和RequestMappingHandlerAdapter。
<!--Spring3.1開始的注解 HandlerMapping -->
<bean
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
<!--Spring3.1開始的注解 HandlerAdapter -->
<bean
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/>
運行流程:
@Controller
public class HelloWorldController {
……
}
推薦使用這種方式聲明處理器,它和我們的@Service、@Repository很好的對應了我們常見的三層開發架構的組件。
@RequestMapping
@RequestMapping
public class HelloWorldController {
}
這種方式也是可以工作的,但如果在類上使用@ RequestMapping注解一般是用于
窄化功能處理方法的映射的,詳見6.4.3。
package cn.javass.chapter6.web.controller;
@Controller
@RequestMapping(value="/user") //①處理器的通用映射前綴
public class HelloWorldController2 {
@RequestMapping(value = "/hello2") //②相對于①處的映射進行窄化
public ModelAndView helloWorld() {
}
}
窄化請求映射
package cn.javass.chapter6.web.controller;
@Controller
@RequestMapping(value="/user") //①處理器的通用映射前綴
public class HelloWorldController2 {
@RequestMapping(value = "/hello2") //②相對于①處的映射進行窄化
public ModelAndView helloWorld() {
//省略實現
}
}
①類上的@RequestMapping(value="/user") 表示處理器的通用請求前綴;
②處理器功能處理方法上的是對①處映射的窄化。
因此http://localhost:9080/springmvc-chapter6/hello2 無法映射到HelloWorldController2的 helloWorld功能處理方法;而http://localhost:9080/springmvc-chapter6/user/hello2是可以的。
窄化請求映射可以認為是方法級別的@RequestMapping繼承類級別的@RequestMapping,窄化請求映射還有其他方式,如在類級別指定URL,而方法級別指定請求方法類型或參數等等
Servlet
Servlet是一套Web應用的開發規范,我們按照這套規范編碼就可以實現一個Web應用,使其在Web容器中運行。我們最開始學習J2EE時,學習和創建的就是Servlet的實現類,后來學習了MVC框架以后,尤其是SpringMVC,就很少直接創建Servlet的實現類了。雖然SpringMVC簡化和隱藏了Servlet,但是我們也要了解Servlet的運行原理,這樣對了解SpringMVC的原理也很有幫助。
Servlet 容器
Tomcat 的容器等級中,Context 容器是直接管理 Servlet 在容器中的包裝類 Wrapper,所以 Context 容器如何運行將直接影響 Servlet 的工作方式。
真正管理 Servlet 的容器是 Context 容器,一個 Context 對應一個 Web 工程,在 Tomcat 的配置文件中可以很容易發現這一點,如下:
<Context path="/projectOne " docBase="D:\projects\projectOne"
reloadable="true" />
當 Context容器初始化狀態設為init 時,添加在 Contex 容器的 Listener 將會被調用。ContextConfig 繼承了 LifecycleListener 接口,它是在調用Tomcat的addWebapp方法時被加入到 StandardContext 容器中。ContextConfig 類會負責整個 Web 應用的配置文件的解析工作。
ContextConfig 的 init 方法將會主要完成以下工作:
- 創建用于解析 xml 配置文件的 contextDigester 對象
- 讀取默認 context.xml 配置文件,如果存在解析它
- 讀取默認 Host 配置文件,如果存在解析它
- 讀取默認 Context 自身的配置文件,如果存在解析它
- 設置 Context 的 DocBase
ContextConfig 的 init 方法完成后,Context 容器的會執行 startInternal 方法,這個方法啟動邏輯比較復雜,主要包括如下幾個部分:
- 創建讀取資源文件的對象
- 創建 ClassLoader 對象
- 設置應用的工作目錄
- 啟動相關的輔助類如:logger、realm、resources 等
- 修改啟動狀態,通知感興趣的觀察者(Web 應用的配置)
- 子容器的初始化
- 獲取 ServletContext 并設置必要的參數
- 初始化“load on startup”的 Servlet
為什么要將 Servlet 包裝成 StandardWrapper 而不直接是 Servlet 對象。這里 StandardWrapper 是 Tomcat 容器中的一部分,它具有容器的特征,而 Servlet 為了一個獨立的 web 開發標準,不應該強耦合在 Tomcat 中。除了將 Servlet 包裝成 StandardWrapper 并作為子容器添加到 Context 中,其它的所有 web.xml 屬性都被解析到 Context 中,所以說 Context 容器才是真正運行 Servlet 的 Servlet 容器。一個 Web 應用對應一個 Context 容器,容器的配置屬性由 應用的 web.xml 指定,這樣我們就能理解 web.xml 到底起到什么作用了。
那服務器是如何根據這個 URL 來達到正確的 Servlet 容器中的呢?
Tomcat7.0 中這件事很容易解決,因為這種映射工作有專門一個類來完成的,這個就是 org.apache.tomcat.util.http.mapper,這個類保存了 Tomcat 的 Container 容器中的所有子容器的信息,當 org.apache.catalina.connector. Request 類在進入 Container 容器之前,mapper 將會根據這次請求的 hostnane 和 contextpath 將 host 和 context 容器設置到 Request 的 mappingData 屬性中。所以當 Request 進入 Container 容器之前,它要訪問那個子容器這時就已經確定了。
Session 工作的時序圖
Servlet 中的 Listener
用@Configuration注解該類,等價 與XML中配置beans;用@Bean標注方法等價于XML中配置bean。