博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
基于Spring MVC框架的Http流程分析
阅读量:6957 次
发布时间:2019-06-27

本文共 13017 字,大约阅读时间需要 43 分钟。

hot3.png

一、问题提出

我们可以方便的利用Spring MVC进行业务开发,请求的大部分工作都被框架和容器封装,使得我们只需要做很少量的工作。但是整个http请求流程是怎么样的?Spring MVC框架在其中起到什么作用?它是怎么和Web容器进行交互的?Controller中的一个方法怎么被暴露出来提供http请求服务的?本着这些想法,我们对整个http请求过程进行讨索。全文以为例

二、整体处理流程概述

整个过程包括三部分:应用启动、请求路由与处理、请求返回。

应用启动:web容器初始化(context建立等)、应用初始化(初始化handlerMap)。

请求路由与处理:请求路由(根据url找到Context、根据context找到dispatcherServlet、根据url找到handler、根据url找到handler的方法)、method反射调用获取ModelAndView。

请求返回:逻辑视图到物理视图的转换、物理视图的渲染、视图返回。

具体流程如下:

b9f147ab1000695fb0a424da0ebdb0cef07.jpg

系统启动:

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、外部发送请求()时,请求会被转发到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)的方法上打断点,获取信息如下:

0d03d63d23b9ae47d134c383ce98589f7eb.jpg

这里的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的值如下:

609663007a620b68a14b8bb73840d60dde2.jpg

对照web.xml的配置:

contextConfigLocation
classpath:/applicationContext.xml
org.springframework.web.context.ContextLoaderListener
smart
org.springframework.web.servlet.DispatcherServlet
1
smart
/
index.jsp

可以看到,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
urls = new ArrayList
(); 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
urls = new LinkedHashSet
(); 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中。

7d0601ebe452cd4b413d6e2bb35a6e9a8f0.jpg

四、请求发送

1、请求context获取

在org.apache.catalina.mapper.Mapper#internalMap方法中,会根据url去查找host和context。

a62543ed5d2c0213bb0966654fa5f4ea664.jpg

这里的host为localhost,根据这个去hosts列表中查找对应的host。

c877ba4178d49cc67d23deb6ff1e4fdab71.jpg

再在查找到的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
attributesSnapshot = null; if (WebUtils.isIncludeRequest(request)) { logger.debug("Taking snapshot of request attributes before include"); attributesSnapshot = new HashMap
(); 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。

275fadd63a85714bba710b809066e67671b.jpg

请求处理已经在《》说过,就不再详述了。

五、请求返回

视图渲染在方法:org.springframework.web.servlet.DispatcherServlet#render中进行,具体如下:

0ee694d696f78a1767609f339df8d54a1af.jpg

我们配置的视图为:org.springframework.web.servlet.view.JstlView,它会将视图渲染后,然后,通过JspServlet的service方法将视图通过writer.out输出到客户端。

我们打开register_jsp.java文件,其所在目录如下:

3dc8219dcaa61941a3cd3635c0f93955d8d.jpg

其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("\n");      out.write("\n");      out.write("    新增用户\n");      out.write("\n");      out.write("\n");      out.write("
\n"); out.write("
\n"); out.write("
\n"); out.write("
\n"); out.write("
\n"); out.write("
\n"); out.write("
\n"); out.write("
\n"); out.write("
\n"); out.write("
\n"); out.write("
\n"); out.write("
\n"); out.write("
\n"); out.write("
\n"); out.write("
\n"); out.write("
\n"); out.write("
\n"); out.write("
用户名:
密码:
姓名:
\n"); out.write("
\n"); out.write("\n"); out.write(""); } 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方法将视图输出到客户端的。

转载于:https://my.oschina.net/yangjianzhou/blog/3043724

你可能感兴趣的文章
移动互联网—品牌广告营销实战
查看>>
基于大数据分析的网络攻击检测
查看>>
蔡为东:行之有效的IT技术团队管理实践
查看>>
Android笔试总结
查看>>
MGDrawingSlate
查看>>
GIKAnimatedCallout
查看>>
内容IMG图片地址批量追加域名
查看>>
开源 免费 java CMS - FreeCMS2.0 移动APP生成栏目数据
查看>>
【Android Studio】为Android Studio设置HTTP代理
查看>>
mysql数据库相关
查看>>
ES 使用优化整理
查看>>
Android 常用代码总结 工具类
查看>>
ubuntu14.04安装mysql5.7
查看>>
php的引用符号 &
查看>>
使用svn的方式访问github(不成功)
查看>>
Jenkins(二)
查看>>
Node.js安装及环境配置之Windows篇
查看>>
从Oracle迁移到AntDB(一)-- ora2pg-install
查看>>
Oracle 关键字(保留字) 大全
查看>>
php-------代码加密的几种方法
查看>>