SpringMVC的源碼分析

1. SpringMVC的執行過程分析

首先,我們先來看一下springmvc 官方文檔中提供的執行過程圖。
在這裏插入圖片描述
通過此圖,我們可以看到其實都是由前端控制器負責找到要執行的控制器方法。這個前端控制器就是SpringMVC 的核心控制器DispatcherServlet

接下來,我們通過一個示例來看一下SpringMVC 請求的全流程。

/**
*
* <p>Title: HelloControler</p>
* <p>Description: 第一個SpringMVC 的控制器</p>
*/
@Controller
public class HelloControler {

	/**
	* 處理請求的控制器方法
	* @return
	*/
	@RequestMapping("hello")
		public String sayHello() {
		System.out.println("控制器方法執行了");
		return "success";
	}
	/**
	* 處理請求的控制器方法
	* @return
	*/
	@RequestMapping("hello2")
		public String sayHello2() {
		System.out.println("控制器方法執行了2");
		return "success";
	}
}

示例代碼的整個執行過程如下圖所示:
在這裏插入圖片描述
我們關注的是DispatcherServlet 是如何找到我們的控制器的,下圖展示了代碼跟蹤,最終發現它是通過反射調用的。

DispatcherServlet本身也就是一個Servlet(有doGet和doPost和doService方法),調用其doService方法發現:
在這裏插入圖片描述

2. SpringMVC中三大組件詳解

2.1 處理器映射器

它指的是:RequestMappingHandlerMapping,它的出現,可以讓使用者更加輕鬆的去配置SpringMVC 的請求路徑映射。去掉了早期繁瑣的xml 的配置。

它的配置有兩種方式:都是在springmvc.xml 中加入配置。
第一種方式:

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>

第二種方式:

<mvc:annotation-driven></mvc:annotation-driven>

在這兩種方式中,第二種方式更加的完善,它可以幫我們在容器中添加很多的bean。

它起的作用是爲我們建立起@RequestMapping 註解和控制器方法的對應關係。並且存在於
MappingRegistry 對象中的mappingLookup 的Map 中,該Map 是個LinkedHashMap。對應關係的建立時機是在應用加載的時候,也就是當服務器啓動完成後,這些對應關係已經建立完成了。從而做到在我們訪問的時候,只是從Map 中獲取對應的類和方法的信息,並調用執行。

這種思想在我們連接池等技術中也經常用到!
在這裏插入圖片描述

2.2 處理器適配器

一般使用RequestMappingHandlerAdapter(@Controller註解的形式)

要清晰的認識SpringMVC 的處理器適配器,就先必須知道適配器以及它的作用。

我們知道適配器它是把不同的接口都轉換成了USB 接口。

抽象到我們的SpringMVC 中,就是把不同的控制器,最終都可以看成是適配器類型,從而執行適配器中定義的方法。更深層次的是,我們可以把公共的功能都定義在適配器中,從而減少每種控制器中都有的重複性代碼,做到分工合作。類似於接口的作用。

通過學習了SpringMVC 的執行過程,最終調用的是前端控制器DispatcherServlet 的doDispatch 方法,而該方法中的HandlerAdapter 的handle 方法實際調用了我們自己寫的控制器方法。而我
們寫的控制方法名稱各不一樣,它是通過handle 方法反射調用的。但是我們不知道的是,其實SpringMVC 中處理器適配器也有多個。

2.2.1 SimpleControllerHandlerAdapter

第一個:org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter
使用此適配器,適用的控制器寫法:要求實現Controller 接口

/**
* 
*/
public class HelloController2 implements Controller {
	@Override
	public ModelAndView handleRequest(HttpServletRequest httpServletRequest,
	HttpServletResponse httpServletResponse) throws Exception {
		ModelAndView mv = new ModelAndView();
		mv.setViewName("success");
		return mv;
	}
}

同時要求我們在springmvc.xml 中添加:

<bean id="simpleControllerHandlerAdapter " class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter">
</bean>
<bean name="/sayhello2" class="com.itheima.web.controller.HelloController2"></bean>

2.2.2 HttpRequestHandlerAdapter

第二個:org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter
使用此適配器的控制器寫法:要求實現HttpRequestHandler 接口,與我們web工程是一樣的,使用request來進行請求的轉發。

/**
* 
*/
public class HelloController3 implements HttpRequestHandler {
	@Override
	public void handleRequest(HttpServletRequest request,
	HttpServletResponse response) throws ServletException, IOException {
		request.getRequestDispatcher("/WEB-INF/pages/success.jsp").forward(request,response);
	}
}

同時要求我們在springmvc.xml 中添加:

<bean name="/sayhello3" class="com.itheima.web.controller.HelloController3"></bean>
<bean id=" httpRequestHandlerAdapter " class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter"></bean>

2.2.3 使用@Controller註解

第三個:
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
這種方式也是我們實際開發中採用最多的。它的要求是我們用註解@Controller 配置控制器

使用的是RequestMappingHandlerAdapter

/**
* <p>Title: HelloControler</p>
* <p>Company: http://www.itheima.com/ </p>
*/
@Controller
public class HelloControler {
	@RequestMapping("hello")
	public String sayHello() {
		System.out.println("控制器方法執行了");
		return "success";
	}
}

它的xml 中配置:

<bean id="requestMappingHandlerAdapter" class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"></bean>

不過通常情況下我們都是直接配置:

<mvc:annotation-driven></mvc:annotation-driven>

2.3 視圖解析器(瞭解即可)

我們只需要瞭解一下SpringMVC 中的視圖即可。視圖的作用是渲染模型數據,將模型裏的數據以某種形式呈現給客戶。爲了實現視圖模型和具體實現技術的解耦,Spring org.springframework.web.servlet 包中定義了一個高度抽象的View 接口。

作用:
渲染將處理結果通過頁面展示給用戶。

3. 不需要視圖解析器的場景分析

在分析之前,我們先需要回顧下控制器方法的返回值,此處我們都是以註解@Controller 配置控制器爲例,控制器的方法返回值其實支持三種方式:

第一種:String 類型。藉助視圖解析器,可以在指定位置爲我們找到指定擴展名的視圖。視圖可以是JSP,HTML 或者其他的控制器方法上的RequestMapping 映射地址。前往指定視圖的方式,默認是請求轉發,可以通過redirect:前綴控制其使用重定向。

第二種:void,即沒有返回值。因爲我們在控制器方法的參數中可以直接使用原始SerlvetAPI 對象
HttpServletRequest 和HttpServletResponse 對象,所以無論是轉發還是重定向都可以輕鬆實現,而無需使用返回值。

第三種:ModelAndView 類型。其實我們跟蹤源碼可以發現在DispatcherServlet 中的doDispatch 方法執行時,HandlerAdapter處理器適配器 的handle 方法的返回值就是ModelAndView,只有我們的控制器方法定義爲void時,纔不會返回此類型。當返回值是String 的時候也會創建ModelAndView 並返回

通過上面三種控制器方法返回值,我們可以再深入的剖析一下我們請求之後接收響應的方式,其實無外乎就三種。

在我們的實際開發中,如果我們不需要頁面跳轉,即基於ajax 的異步請求,用json 數據交互時,即可不配置任何視圖解析器前後端分離並且交互是通過json 數據的,利用@RequestBody@ResponseBody 實現數據到java對象的綁定(當然還要藉助Jackson 開源框架)。

4. 請求參數封裝

4.1 請求參數封裝的實現原理

在使用SpringMVC 實現請求參數封裝時,它支持基本類型,POJO 類型和集合類型。其封裝原理其實就是使用我們原始的ServletAPI 中的方法,利用了request.getParameterValues方法並且配合反射實現的封裝。

此處我們以最簡單的String 和Integer 兩個方法爲例,帶着大家把整個執行過程走一圈。
先來看控制器的方法:

/**
* <p>Title: HelloControler</p>
* <p>Company: http://www.itheima.com/ </p>
*/
@Controller
public class HelloControler {
	@RequestMapping("hello")
	public String sayHello(String name,Integer age) {
		System.out.println("控制器方法執行了"+name+","+age);
		return "success";
	}
}

執行過程如下源碼部分:
在這裏插入圖片描述
如圖可知,通過RequstParamMethodArgumentResolver的resolveArgument方法來解析我們傳過來的參數,封裝到變量裏面。
在這裏插入圖片描述

4.2 常用封裝參數註解的使用:

4.2.1 RequestParam

首先我們要明確,我們的請求參數體現形式是什麼樣的。無論get/post/put/delete 請求方式,參數的體現形式都是key=value

再來,通過上一小節我們知道,SpringMVC 是使用我們控制器方法的形參作爲參數名稱,再使用request 的getParameterValues 方法獲取的參數。所以纔會有請求參數的key 必須和方法形參變量名稱保持一致的要求。

但是如果形參變量名稱和請求參數的key 不一致呢?此時,參數將無法封裝成功。
此時@RequestParam 註解就會起到作用,它會把該註解value 屬性的值作爲請求參數的key 來獲取請求參數
的值,並傳遞給控制器方法。

@Controller
public class ParamController1 {
	/**
	* 處理請求的控制器方法
	* @return
	*/
	@RequestMapping("hello")
	public String sayHello(@RequestParam("username")String name,Integer age) {
		System.out.println("控制器方法執行了"+name+","+age);
		return "success";
	}
}

在這裏插入圖片描述

4.2.2 RequestBody

我們通過源碼分析得知,SpringMVC 在封裝請求參數的時候,默認只會獲取參數的值,而不
會把參數名稱一同獲取出來,這在我們使用表單提交的時候沒有任何問題。因爲我們的表單提交,請求參數是key=value 的。
但是當我們使用ajax 進行提交時,請求參數可能是json 格式的:{key:value},在此種情況
下,要想實現封裝以我們前面的內容是無法實現的。此時需要我們使用@RequestBody 註解。

代碼片段:

<script src="${pageContext.request.contextPath}/js/jquery.min.js"></script>

<script type="text/javascript">
	$(function(){
		$("#ajaxBtn").click(function(){
			$.ajax({
				type:"POST",
				url:"${pageContext.request.contextPath}/hello2",
				dataType:"text",
				data:"{'name':'test','age':18}",
				contentType:"application/json",
				success:function(data){
					alert(data);
				}
			});
		});
	})
</script>

<title>SpringMVC</title>
</head>

<body>
	<button id="ajaxBtn">異步請求</button>
</body>

</html>

控制器代碼片段:

@Controller
public class ParamController {
/**
* 處理請求的控制器方法
* @return
*/
@RequestMapping("hello2")
public String sayHello2(@RequestBody String body) {
	System.out.println("控制器方法執行了2"+body);
	return "success";
	}
}

它的執行過程如下圖:首先前面的執行和2.1.4 小節是一致的,在下圖紅框中進行參數解析時:
在這裏插入圖片描述通過最後這個方法,我們可以看出,它是先獲取的請求參數MIME 類型MediaType,然後再把整個內容獲取出來,並傳遞給我們的控制器方法。

需要注意的是,此註解並不能爲我們提供封裝到pojo 的操作,它只能把請求體中全部內容獲取出來,僅此而已,要想實現封裝,需要藉助jackson 開源框架。

4.2.3 PathVariable

它是SpringMVC 在3.0 之後新加入的一個註解,是SpringMVC 支持Restful 風格URL 的一個重要標誌。
該註解的作用大家已經非常熟悉了,就是把藏在請求URL 中的參數,給我們控制器方法的形參賦值。而Restful風格的URL,在現如今的開發中使用越來越普遍了。那麼它是如何實現封裝的呢?請看下圖:

首先還是執行到紅框中解析參數這行代碼,接下來執行的是AbstarctNameValueMethodArgumentResolver 這個類的方法:最後是執行PathVariableMethodArgumentResolver 類中的方法:
在這裏插入圖片描述
通過上面執行過程的全圖,我們看出SpringMVC 在實現請求URL 使用佔位符傳參並封裝到控制器方法的形參中,是通過請求域來實現的。最後把請求域轉成一個Map,再根據形參的名稱作爲key,從map 中獲取value,並給形參賦值。當然如果我們使用了PathVariable 註解的value 屬性,則不會以形參名稱爲key,而是直接使用value屬性的值作爲key 了。

5. 攔截器的AOP 思想

AOP 思想是Spring 框架的兩大核心之一,是解決方法調用依賴以及提高方便後期代碼維護的重要思想。它是把我們代碼中高度重複的部分抽取出來,並在適當的時機,通過代理機制來執行,從而做到不修改源碼對已經寫好的方法進行增強。而攔截器正式這種思想的具體實現。

攔截器代碼:必須繼承HandlerInterceptor

public class MyInterceptor1 implements HandlerInterceptor{
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		System.out.println("攔截器執行了");
		return false;
	}

	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response,
	Object handler, ModelAndView modelAndView) throws Exception {
		System.out.println("執行了postHandle 方法");
	}
	
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse
	response, Object handler, Exception ex) throws Exception {
		System.out.println("執行了afterCompletion 方法");
	}
}

它是在DispatcherServlet 中使用的,在DispatcherServlet的代碼中:
在這裏插入圖片描述
去執行了HandlerExecutionChain 類中的:
在這裏插入圖片描述
而此時還沒有執行我們的控制器方法,所以此時爲前置增強
在執行完調用控制方法,
在這裏插入圖片描述
之後,會執行HandlerExecutionChain 類中的:
在這裏插入圖片描述
此時是後置增強,但是此時還沒有響應結果視圖

最後是HandlerExecutionChain 中的triggerAfterCompletion 方法執行:
在這裏插入圖片描述
而此方法執行時,結果視圖的創建已經完成,只待展示,所以此時爲最終增強

當然我們自定義攔截器可以選擇多種方式,通常我們實現HandlerInterceptor 接口,但是我們也可以選擇繼承HandlerInterceptorAdapter 類,而如果我們選擇繼承此類,則還會有HandlerExecutionChain 類中的:
在這裏插入圖片描述

6.自定義攔截器中三個方法說明及使用場景

6.1 preHandle

此方法的執行時機是在控制器方法執行之前,所以我們通常是使用此方法對請求部分進行增強。同時由於結果
視圖還沒有創建生成,所以此時我們可以指定響應的視圖。

6.2 postHandle

此方法的執行時機是在控制器方法執行之後,結果視圖創建生成之前。所以通常是使用此方法對響應部分進行
增強。因爲結果視圖沒有生成,所以我們此時仍然可以控制響應結果。

6.3 afterCompletion

此方法的執行時機是在結果視圖創建生成之後,展示到瀏覽器之前。所以此方法執行時,本次請求要準備的數
據具已生成完畢,且結果視圖也已創建完成,所以我們可以利用此方法進行清理操作。同時,我們也無法控制響應 結果集內容。

7. 爲什麼不使用XML 配置SpringMVC

我們先來看基於XML 的SpringMVC 配置:
第一步:配置web.xml
第二步:編寫控制器
第三步:編寫springmvc.xml
第四步:配置控制器
第五步:配置處理器映射器,處理器適配器。
第六步:配置視圖解析器。
其中,前3 步和第六步基於註解配置時也都有,而第四第五步註解配置只需:

<!-- 開啓springmvc 對註解的支持-->
<mvc:annotation-driven></mvc:annotation-driven>

而XML 配置則需:

<!-- 實現Controller 接口-->
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>
<bean name="/sayhello2" class="com.itheima.web.controller.HelloController2"/>
<!-- 繼承HttpRequestHandler 類-->
<bean name="/sayhello3" class="com.itheima.web.controller.HelloController3"/>
<bean class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter"/>

而對比註解配置只需一個Controller 註解和一個RequestMapping 註解來比,顯然註解來的更方便。

8. mvc:annotation-driven 的說明

它就相當於在xml中配置了:

<!-- Begin -->
<!-- HandlerMapping -->
<bean
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerM
apping"></bean>
<bean
class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"></bean>
<!-- HandlerAdapter -->
<bean
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerA
dapter"></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.ExceptionHandlerExcept
ionResolver"></bean>
<bean
class="org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolv
er"></bean>
<bean
class="org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver"
></bean>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章