咱們能夠方便的利用Spring MVC進行業務開發,請求的大部分工做都被框架和容器封裝,使得咱們只須要作不多量的工做。可是整個http請求流程是怎麼樣的?Spring MVC框架在其中起到什麼做用?它是怎麼和Web容器進行交互的?Controller中的一個方法怎麼被暴露出來提供http請求服務的?本着這些想法,咱們對整個http請求過程進行討索。全文以spring-mvc-demo爲例html
整個過程包括三部分:應用啓動、請求路由與處理、請求返回。java
應用啓動:web容器初始化(context創建等)、應用初始化(初始化handlerMap)。git
請求路由與處理:請求路由(根據url找到Context、根據context找到dispatcherServlet、根據url找到handler、根據url找到handler的方法)、method反射調用獲取ModelAndView。github
請求返回:邏輯視圖到物理視圖的轉換、物理視圖的渲染、視圖返回。web
具體流程以下:spring
系統啓動:shell
一、web容器本身去將contextPath、docBase設置到一個context裏面,這裏面的一個context就是對應一個web應用。apache
二、web容器會根據docBase的值去獲取web.xml,並解析它來獲取servlet信息,並設置web容器啓動完畢的監聽器。spring-mvc
三、web容器啓動後,會觸發spring mvc容器的啓動,spring mvc容器啓動時,會解析controller,並將@RequestMapping、@GetMapping、@PostMapping的值設置到handlerMap中,方便後續請求路由。tomcat
請求發送:
一、外部發送請求(http://localhost:8080/spring-mvc-demo/user/register)時,請求會被轉發到web容器(這裏以tomcat爲例),實際上就是tomcat與客戶端創建了socket連接。
二、根據url,tomcat會對應的host,host找到context,context找到對應的servlet(這裏爲dispatcherServlet)。
三、dispatcherServlet會根據url,在handlerMap中去查到到對應的handler,而後將handler轉化爲handlerAdapter。
四、AnnotationMethodHandlerAdapter會調用ServletHandlerMethodInvoker.invokeHandlerMethod方法,ServletHandlerMethodInvoker會經過反射的方式去調用controller的對應方法。
請求返回:
一、根據controller的返回,獲取對應的ModelAndView。
二、DispatcherServlet的resolveViewName方法會將邏輯視圖轉換爲物理視圖。
三、org.springframework.web.servlet.view.AbstractView#render方法會進行視圖渲染工做,具體的渲染視圖爲org.springframework.web.servlet.view.JstlView
四、jsp文件會被編譯成一個servlet,而後,jspServlet會調用service方法,最後會將視圖寫到客戶端。
咱們經過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; }
經過在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處理。
在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中。
在org.apache.catalina.mapper.Mapper#internalMap方法中,會根據url去查找host和context。
這裏的host爲localhost,根據這個去hosts列表中查找對應的host。
再在查找到的host的contextlist中去查找context。找到後,會將context的信息設置到mappingData
獲取到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); } } }
在前文說過,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方法將視圖輸出到客戶端的。