1. 程式人生 > >Spring MVC中DispatcherServlet工作原理探究

Spring MVC中DispatcherServlet工作原理探究

下面類圖將主要的類及方法抽離出來,以便檢視方便,根據類的結構來說明整個請求是如何工作的

主要使用到的技術有Spring的IOC容器和Servlet。

假如我們要實現一個請求home.htm然後返回home.jsp檢視資源則

當home.htm請求到達時,我們需要DispatcherServlet來處理該請求,所以首先配置該Servlet

第一步需要在web.xml中配置DispatcherServlet,使該servlet來接收請求並做進一步處理。

 <servlet>
 	<servlet-name>dispatch</servlet-name>
 	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
 	<load-on-startup>1</load-on-startup>
 </servlet>
 <servlet-mapping>
 	<servlet-name>dispatch</servlet-name>
 	<url-pattern>*.htm</url-pattern>
 </servlet-mapping>

這個部分很好理解,如果請求以.htm結尾則交給名為dispatch類為DispatcherServlet的Servlet處理。

從類圖中很容易看出DispatcherServlet最終繼承的是HttpServlet,也就是說它同樣滿足Servlet的工作原理

Servlet初始化時需要呼叫init方法,在HttpServletBean中實現,該init方法呼叫了initServletBean,該方法在FrameworkServlet中實現

initServletBean主要初始化關於配置檔案的內容,比如{servlet-name}-servlet.xml


第二步,需要在/WebRoot/WEB-INF下新建名為{servlet-name}-servlet.xml的spring bean配置檔案。(該示例中即為dispatch-servlet.xml)

在初始化過程中會去尋找該配置檔案,當然我們也可以自己去設定引數來更改配置檔案所在路徑

比如我們如果在src下新建的該配置檔案dispatch-servlet,在編譯後會被複制到WEB-INF/classes資料夾下,

配置檔案還是按照命名規範做吧(可以修改為其他名字)

<servlet>
		<servlet-name>dispatch</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>namespace</param-name>
			<param-value>classes/dispatch-servlet</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>

此時的配置就會去尋找/WEB-INF/classes/dispatch-servlet.xml

當請求到達後Servlet將呼叫service方法進行處理,由於我們是通過輸入網址方式的get方法請求,Servlet將呼叫doGet方法

此處的doGet方法在FrameworkServlet中實現,doGet方法呼叫processRequest方法,processRequest則呼叫doService方法處理

而doService在DispatcherServlet中實現,doService再呼叫了DispatcherServlet的doDispatch方法,

該方法則會根據request找到轉發物件,並進行請求轉發操作,

下面是獲取實際的檢視資源部分

public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		return ((Controller) handler).handleRequest(request, response);
	}

這裡需要我們自己實現Controller介面並實現handleRequest方法,返回對應的ModelAndView物件。

下面是請求轉發的部分

/**
	 * Render the internal resource given the specified model.
	 * This includes setting the model as request attributes.
	 */
	@Override
	protected void renderMergedOutputModel(
			Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

		// Determine which request handle to expose to the RequestDispatcher.
		HttpServletRequest requestToExpose = getRequestToExpose(request);

		...
                 exposeModelAsRequestAttributes(model, requestToExpose);//這個方法看下面原始碼,request.setAttribute操作
                  // Determine the path for the request dispatcher.
		String dispatcherPath = prepareForRendering(requestToExpose, response);

	        ...

		// If already included or response already committed, perform include, else forward.
		if (useInclude(requestToExpose, response)) {
			......
		}

		else {//重點看這部分,在根據請求以及配置檔案獲取到RequestDispatcher 物件之後,使用該物件做轉發處理
			// Note: The forwarded resource is supposed to determine the content type itself.
			exposeForwardRequestAttributes(requestToExpose);
			if (logger.isDebugEnabled()) {
				logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
			}
			rd.forward(requestToExpose, response);
		}
	}
下面是設定model和modelValue
/**
	 * Expose the model objects in the given map as request attributes.
	 * Names will be taken from the model Map.
	 * This method is suitable for all resources reachable by {@link javax.servlet.RequestDispatcher}.
	 * @param model Map of model objects to expose
	 * @param request current HTTP request
	 */
	protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception {
		for (Map.Entry<String, Object> entry : model.entrySet()) {
			String modelName = entry.getKey();
			Object modelValue = entry.getValue();
			if (modelValue != null) {
				request.setAttribute(modelName, modelValue);
				if (logger.isDebugEnabled()) {
					logger.debug("Added model object '" + modelName + "' of type [" + modelValue.getClass().getName() +
							"] to request in view with name '" + getBeanName() + "'");
				}
			}
			else {
				request.removeAttribute(modelName);
				if (logger.isDebugEnabled()) {
					logger.debug("Removed model object '" + modelName +
							"' from request in view with name '" + getBeanName() + "'");
				}
			}
		}
	}



第三步,編寫實現Controller的類

public class HomeController implements Controller
{
	private String greeting;

	public String getGreeting()
	{
		return greeting;
	}

	public void setGreeting(String greeting)
	{
		this.greeting = greeting;
	}

	public ModelAndView handleRequest(HttpServletRequest arg0,
			HttpServletResponse arg1) throws Exception
	{
		System.out.println(arg0.getRequestURI());//請求地址
		return new ModelAndView("home", "message", greeting);
//返回一個檢視資源物件,名為home,model為message的物件(即上面的exposeModelAsRequestAtrributes方法中使用的request.setAttribute
	}

}

第四步,在dispatch-servlet.xml中配置該bean提供給spring web使用。
	<bean name="/home.htm" class="com.iss.spring.web.HomeController">
		<property name="greeting"><value>Hello!This is Training!你好,這裡是訓練營!</value></property>
	</bean>

這裡name將用來匹配請求的資源(預設的使用BeanNameUrlHandlerMapping處理,由bean Name對映 URL),在home.htm請求到達時,

spring將使用實現了Controller介面的HomeController的handleRequest方法來返回對映的檢視資源。

在得到MoldelAndView物件後,需要根據這個MoldelAndView物件得到View name然後來解析得到View物件

/**
	 * Resolve the given view name into a View object (to be rendered).
	 * <p>The default implementations asks all ViewResolvers of this dispatcher.
	 * Can be overridden for custom resolution strategies, potentially based on
	 * specific model attributes or request parameters.
	 * @param viewName the name of the view to resolve
	 * @param model the model to be passed to the view
	 * @param locale the current locale
	 * @param request current HTTP servlet request
	 * @return the View object, or <code>null</code> if none found
	 * @throws Exception if the view cannot be resolved
	 * (typically in case of problems creating an actual View object)
	 * @see ViewResolver#resolveViewName
	 */
	protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
			HttpServletRequest request) throws Exception {

		for (ViewResolver viewResolver : this.viewResolvers) {
			View view = viewResolver.resolveViewName(viewName, locale);
			if (view != null) {
				return view;
			}
		}
		return null;
	}

此處需要我們配置viewResolver bean給spring使用,指明使用哪個類充當viewResolver並具有什麼屬性

第五步,配置viewResolver bean

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="suffix"><value>.jsp</value></property>
    </bean>

中間可以加上prefix或者suffix

這些配置完成後,spring就會根據請求地址以及配置資訊,找到檢視資源並做請求轉發操作

總結:整個流程分析下來,其實主要就是做兩個操作,

首先請求資訊到達DispatchServlet,Servlet中根據請求資訊與配置檔案找到對映的檢視資源

然後使用RequestDispatch請求轉發到該檢視資源。

另外,可以分成多個bean配置檔案,在web.xml中配置載入

 <listener>
 	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 </listener>
 <context-param>
 	<param-name>contextConfigLocation</param-name>
 	<param-value>/WEB-INF/dispatch-data.xml,/WEB-INF/dispatch-service.xml</param-value>
 </context-param>

其中contextConfigLocation這個名字可能是匹配FrameworkServlet的setContextConfigLocation方法

也有可能是匹配ContextLoaderListener繼承ContextLoader的CONFIG_LOCATION_PARAM

public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
(不確定,不太瞭解context-param的用法,API上兩個類關於這個變數的說明都類似,也分不太清楚,反正可以這麼記- -||)

然後配置的viewResolver bean的id為什麼要為viewResolver,下面的是DispatcherServlet中一個靜態字串說明了一切

public static final String VIEW_RESOLVER_BEAN_NAME = "viewResolver";