1. 程式人生 > >基於Spring MVC框架的Http流程分析

基於Spring MVC框架的Http流程分析

一、問題提出

我們可以方便的利用Spring MVC進行業務開發,請求的大部分工作都被框架和容器封裝,使得我們只需要做很少量的工作。但是整個http請求流程是怎麼樣的?Spring MVC框架在其中起到什麼作用?它是怎麼和Web容器進行互動的?Controller中的一個方法怎麼被暴露出來提供http請求服務的?本著這些想法,我們對整個http請求過程進行討索。全文以spring-mvc-demo為例

二、整體處理流程概述

整個過程包括三部分:應用啟動、請求路由與處理、請求返回。

應用啟動:web容器初始化(context建立等)、應用初始化(初始化handlerMap)。

請求路由與處理:請求路由(根據url找到Context、根據context找到dispatcherServlet、根據url找到handler、根據url找到handler的方法)、method反射呼叫獲取ModelAndView。

請求返回:邏輯檢視到物理檢視的轉換、物理檢視的渲染、檢視返回。

具體流程如下:

系統啟動:

1、web容器自己去將contextPath、docBase設定到一個context裡面,這裡面的一個context就是對應一個web應用。

2、web容器會根據docBase的值去獲取web.xml,並解析它來獲取servlet資訊,並設定web容器啟動完畢的監聽器。

3、web容器啟動後,會觸發spring mvc容器的啟動,spring mvc容器啟動時,會解析controller,並將@RequestMapping、@GetMapping、@PostMapping的值設定到handlerMap中,方便後續請求路由。

請求傳送:

1、外部發送請求(http://localhost:8080/spring-mvc-demo/user/register)時,請求會被轉發到web容器(這裡以tomcat為例),實際上就是tomcat與客戶端建立了socket連結。

2、根據url,tomcat會對應的host,host找到context,context找到對應的servlet(這裡為dispatcherServlet)。

3、dispatcherServlet會根據url,在handlerMap中去查到到對應的handler,然後將handler轉化為handlerAdapter。

4、AnnotationMethodHandlerAdapter會呼叫ServletHandlerMethodInvoker.invokeHandlerMethod方法,ServletHandlerMethodInvoker會通過反射的方式去呼叫controller的對應方法。

請求返回:

1、根據controller的返回,獲取對應的ModelAndView。

2、DispatcherServlet的resolveViewName方法會將邏輯檢視轉換為物理檢視。

3、org.springframework.web.servlet.view.AbstractView#render方法會進行檢視渲染工作,具體的渲染檢視為org.springframework.web.servlet.view.JstlView

4、jsp檔案會被編譯成一個servlet,然後,jspServlet會呼叫service方法,最後會將檢視寫到客戶端。

三、系統啟動

1、context設定

我們通過shell指令碼呼叫gradle的tomcatRun方法來啟動應用,然後在本地debug的方式來獲取執行引數。在org.apache.catalina.startup.Tomcat#addWebapp(org.apache.catalina.Host, java.lang.String, java.lang.String)的方法上打斷點,獲取資訊如下:

這裡的listener為ContextConfig,它會監聽容器相關事件,其中一項工作就是監聽tomcat啟動後去解析web.xml。也可以看出contextPath、docBase的值。

被呼叫的addWebapp方法就是初始化context,並將context新增到host中。具體如下:

    public Context addWebapp(Host host, String contextPath, String docBase,
            LifecycleListener config) {

        silence(host, contextPath);

        Context ctx = createContext(host, contextPath);
        ctx.setPath(contextPath);
        ctx.setDocBase(docBase);
        ctx.addLifecycleListener(new DefaultWebXmlListener());
        ctx.setConfigFile(getWebappConfigFile(docBase, contextPath));

        ctx.addLifecycleListener(config);

        if (config instanceof ContextConfig) {
            // prevent it from looking ( if it finds one - it'll have dup error )
            ((ContextConfig) config).setDefaultWebXml(noDefaultWebXmlPath());
        }

        if (host == null) {
            getHost().addChild(ctx);
        } else {
            host.addChild(ctx);
        }

        return ctx;
    }

2、context中servlet設定

通過在ContextConfig的lifecycleEvent方法是監聽系統事件的入口:

    public void lifecycleEvent(LifecycleEvent event) {

        // Identify the context we are associated with
        try {
            context = (Context) event.getLifecycle();
        } catch (ClassCastException e) {
            log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
            return;
        }

        // Process the event that has occurred
        if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
            configureStart();
        } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
            beforeStart();
        } else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
            // Restore docBase for management tools
            if (originalDocBase != null) {
                context.setDocBase(originalDocBase);
            }
        } else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
            configureStop();
        } else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
            init();
        } else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
            destroy();
        }

    }

通過在這個方法上打斷點,在監聽到after_init事件後,我們可以看到context的servletMappings的值如下:

對照web.xml的配置:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
	http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <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>

    <servlet>
        <servlet-name>smart</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>smart</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

</web-app>

可以看到,DispatcherServlet被載入到context中,因此,該context中的“/”請求會被分配給DispatcherServlet處理。

3、handlerMap初始化

在org.springframework.web.servlet.handler.AbstractDetectingUrlHandlerMapping#detectHandlers上打斷點,我們可以看見org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping和org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping會被用來檢測handler。

其中BeanNameUrlHandlerMapping的檢測方式如下:

	protected String[] determineUrlsForHandler(String beanName) {
		List<String> urls = new ArrayList<String>();
		if (beanName.startsWith("/")) {
			urls.add(beanName);
		}
		String[] aliases = getApplicationContext().getAliases(beanName);
		for (String alias : aliases) {
			if (alias.startsWith("/")) {
				urls.add(alias);
			}
		}
		return StringUtils.toStringArray(urls);
	}

它會檢測到如下型別的handler

@Controller("/person")
public class PersonController{}

DefaultAnnotationHandlerMapping的檢測方式如下:

@Override
	protected String[] determineUrlsForHandler(String beanName) {
		ApplicationContext context = getApplicationContext();
		Class<?> handlerType = context.getType(beanName);
		RequestMapping mapping = context.findAnnotationOnBean(beanName, RequestMapping.class);
		if (mapping != null) {
			// @RequestMapping found at type level
			this.cachedMappings.put(handlerType, mapping);
			Set<String> urls = new LinkedHashSet<String>();
			String[] typeLevelPatterns = mapping.value();
			if (typeLevelPatterns.length > 0) {
				// @RequestMapping specifies paths at type level
				String[] methodLevelPatterns = determineUrlsForHandlerMethods(handlerType, true);
				for (String typeLevelPattern : typeLevelPatterns) {
					if (!typeLevelPattern.startsWith("/")) {
						typeLevelPattern = "/" + typeLevelPattern;
					}
					boolean hasEmptyMethodLevelMappings = false;
					for (String methodLevelPattern : methodLevelPatterns) {
						if (methodLevelPattern == null) {
							hasEmptyMethodLevelMappings = true;
						}
						else {
							String combinedPattern = getPathMatcher().combine(typeLevelPattern, methodLevelPattern);
							addUrlsForPath(urls, combinedPattern);
						}
					}
					if (hasEmptyMethodLevelMappings ||
							org.springframework.web.servlet.mvc.Controller.class.isAssignableFrom(handlerType)) {
						addUrlsForPath(urls, typeLevelPattern);
					}
				}
				return StringUtils.toStringArray(urls);
			}
			else {
				// actual paths specified by @RequestMapping at method level
				return determineUrlsForHandlerMethods(handlerType, false);
			}
		}
		else if (AnnotationUtils.findAnnotation(handlerType, Controller.class) != null) {
			// @RequestMapping to be introspected at method level
			return determineUrlsForHandlerMethods(handlerType, false);
		}
		else {
			return null;
		}
	}

即根據@RequestMapping來檢測url,檢測到url後,會將url為key,對應的controller為value放到handlerMap中。

四、請求傳送

1、請求context獲取

在org.apache.catalina.mapper.Mapper#internalMap方法中,會根據url去查詢host和context。

這裡的host為localhost,根據這個去hosts列表中查詢對應的host。

再在查詢到的host的contextlist中去查詢context。找到後,會將context的資訊設定到mappingData

2、servlet獲取

獲取到context後,在根據請求url以及context中的servletMapping就可以得到對應的servlet,之後就會呼叫對應的servlet的service方法。以請求http://localhost:8080/spring-mvc-demo/user/register(get方法)為例,會呼叫org.springframework.web.servlet.FrameworkServlet#doGet方法,順著流程,就會走到DispatcherServlet的doDispatch方法了。

protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {

		if (logger.isDebugEnabled()) {
			String requestUri = urlPathHelper.getRequestUri(request);
			String resumed = WebAsyncUtils.getAsyncManager(request).hasConcurrentResult() ? " resumed" : "";
			logger.debug("DispatcherServlet with name '" + getServletName() + "'" + resumed +
					" processing " + request.getMethod() + " request for [" + requestUri + "]");
		}

		// Keep a snapshot of the request attributes in case of an include,
		// to be able to restore the original attributes after the include.
		Map<String, Object> attributesSnapshot = null;
		if (WebUtils.isIncludeRequest(request)) {
			logger.debug("Taking snapshot of request attributes before include");
			attributesSnapshot = new HashMap<String, Object>();
			Enumeration<?> attrNames = request.getAttributeNames();
			while (attrNames.hasMoreElements()) {
				String attrName = (String) attrNames.nextElement();
				if (this.cleanupAfterInclude || attrName.startsWith("org.springframework.web.servlet")) {
					attributesSnapshot.put(attrName, request.getAttribute(attrName));
				}
			}
		}

		// Make framework objects available to handlers and view objects.
		request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
		request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
		request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
		request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

		FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
		if (inputFlashMap != null) {
			request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
		}
		request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
		request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);

		try {
			doDispatch(request, response);
		}
		finally {
			if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
				return;
			}
			// Restore the original attribute snapshot, in case of an include.
			if (attributesSnapshot != null) {
				restoreAttributesAfterInclude(request, attributesSnapshot);
			}
		}
	}

3、handler獲取

在前文說過,handler會被放到handlerMap中,key為請求的url。

請求處理已經在《Spring MVC請求處理流程分析》說過,就不再詳述了。

五、請求返回

檢視渲染在方法:org.springframework.web.servlet.DispatcherServlet#render中進行,具體如下:

我們配置的檢視為:org.springframework.web.servlet.view.JstlView,它會將檢視渲染後,然後,通過JspServlet的service方法將檢視通過writer.out輸出到客戶端。

我們開啟register_jsp.java檔案,其所在目錄如下:

其service方法內容如下:

public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
        throws java.io.IOException, javax.servlet.ServletException {

final java.lang.String _jspx_method = request.getMethod();
if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method) && !javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSPs only permit GET POST or HEAD");
return;
}

    final javax.servlet.jsp.PageContext pageContext;
    javax.servlet.http.HttpSession session = null;
    final javax.servlet.ServletContext application;
    final javax.servlet.ServletConfig config;
    javax.servlet.jsp.JspWriter out = null;
    final java.lang.Object page = this;
    javax.servlet.jsp.JspWriter _jspx_out = null;
    javax.servlet.jsp.PageContext _jspx_page_context = null;


    try {
      response.setContentType("text/html; charset=UTF-8");
      pageContext = _jspxFactory.getPageContext(this, request, response,
      			null, true, 8192, true);
      _jspx_page_context = pageContext;
      application = pageContext.getServletContext();
      config = pageContext.getServletConfig();
      session = pageContext.getSession();
      out = pageContext.getOut();
      _jspx_out = out;

      out.write("\n");
      out.write("\n");
      out.write("\n");
      out.write("<html>\n");
      out.write("<head>\n");
      out.write("    <title>新增使用者</title>\n");
      out.write("</head>\n");
      out.write("<body>\n");
      out.write("<form method=\"post\" action=\"");
      if (_jspx_meth_c_005furl_005f0(_jspx_page_context))
        return;
      out.write("\">\n");
      out.write("    <table>\n");
      out.write("        <tr>\n");
      out.write("            <td>使用者名稱:</td>\n");
      out.write("            <td><input type=\"text\" name=\"userName\"  value=\"");
      out.write((java.lang.String) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate("${user.userName}", java.lang.String.class, (javax.servlet.jsp.PageContext)_jspx_page_context, null));
      out.write("\"/></td>\n");
      out.write("        </tr>\n");
      out.write("        <tr>\n");
      out.write("            <td>密碼:</td>\n");
      out.write("            <td><input type=\"password\" name=\"password\" value=\"");
      out.write((java.lang.String) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate("${user.password}", java.lang.String.class, (javax.servlet.jsp.PageContext)_jspx_page_context, null));
      out.write("\"/></td>\n");
      out.write("        </tr>\n");
      out.write("        <tr>\n");
      out.write("            <td>姓名:</td>\n");
      out.write("            <td><input type=\"text\" name=\"realName\" value=\"");
      out.write((java.lang.String) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate("${user.realName}", java.lang.String.class, (javax.servlet.jsp.PageContext)_jspx_page_context, null));
      out.write("\"/></td>\n");
      out.write("        </tr>\n");
      out.write("        <tr>\n");
      out.write("            <td colspan=\"2\"><input type=\"submit\" name=\"提交\"/></td>\n");
      out.write("        </tr>\n");
      out.write("    </table>\n");
      out.write("</form>\n");
      out.write("</body>\n");
      out.write("</html>");
    } catch (java.lang.Throwable t) {
      if (!(t instanceof javax.servlet.jsp.SkipPageException)){
        out = _jspx_out;
        if (out != null && out.getBufferSize() != 0)
          try {
            if (response.isCommitted()) {
              out.flush();
            } else {
              out.clearBuffer();
            }
          } catch (java.io.IOException e) {}
        if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
        else throw new ServletException(t);
      }
    } finally {
      _jspxFactory.releasePageContext(_jspx_page_context);
    }
  }

因此,我們可以猜測,register.jsp被渲染後,通過writer.out方法將檢視輸