返回目录:金融新闻
一、问题提出
我们可以方便的利用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
\thttp://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) {
\t\tList<String> urls = new ArrayList<String>();
\t\tif (beanName.startsWith("/")) {
\t\t\turls.add(beanName);
\t\t}
\t\tString[] aliases = getApplicationContext().getAliases(beanName);
\t\tfor (String alias : aliases) {
\t\t\tif (alias.startsWith("/")) {
\t\t\t\turls.add(alias);
\t\t\t}
\t\t}
\t\treturn StringUtils.toStringArray(urls);
\t}
它会检测到如下类型的handler
@Controller("/person")
public class PersonController{}
DefaultAnnotationHandlerMapping的检测方式如下:
@Override
\tprotected String[] determineUrlsForHandler(String beanName) {
\t\tApplicationContext context = getApplicationContext();
\t\tClass<?> handlerType = context.getType(beanName);
\t\tRequestMapping mapping = context.findAnnotationOnBean(beanName, RequestMapping.class);
\t\tif (mapping != null) {
\t\t\t// @RequestMapping found at type level
\t\t\tthis.cachedMappings.put(handlerType, mapping);
\t\t\tSet<String> urls = new LinkedHashSet<String>();
\t\t\tString[] typeLevelPatterns = mapping.value();
\t\t\tif (typeLevelPatterns.length > 0) {
\t\t\t\t// @RequestMapping specifies paths at type level
\t\t\t\tString[] methodLevelPatterns = determineUrlsForHandlerMethods(handlerType, true);
\t\t\t\tfor (String typeLevelPattern : typeLevelPatterns) {
\t\t\t\t\tif (!typeLevelPattern.startsWith("/")) {
\t\t\t\t\t\ttypeLevelPattern = "/" + typeLevelPattern;
\t\t\t\t\t}
\t\t\t\t\tboolean hasEmptyMethodLevelMappings = false;
\t\t\t\t\tfor (String methodLevelPattern : methodLevelPatterns) {
\t\t\t\t\t\tif (methodLevelPattern == null) {
\t\t\t\t\t\t\thasEmptyMethodLevelMappings = true;
\t\t\t\t\t\t}
\t\t\t\t\t\telse {
\t\t\t\t\t\t\tString combinedPattern = getPathMatcher().combine(typeLevelPattern, methodLevelPattern);
\t\t\t\t\t\t\taddUrlsForPath(urls, combinedPattern);
\t\t\t\t\t\t}
\t\t\t\t\t}
\t\t\t\t\tif (hasEmptyMethodLevelMappings ||
\t\t\t\t\t\t\torg.springframework.web.servlet.mvc.Controller.class.isAssignableFrom(handlerType)) {
\t\t\t\t\t\taddUrlsForPath(urls, typeLevelPattern);
\t\t\t\t\t}
\t\t\t\t}
\t\t\t\treturn StringUtils.toStringArray(urls);
\t\t\t}
\t\t\telse {
\t\t\t\t// actual paths specified by @RequestMapping at method level
\t\t\t\treturn determineUrlsForHandlerMethods(handlerType, false);
\t\t\t}
\t\t}
\t\telse if (AnnotationUtils.findAnnotation(handlerType, Controller.class) != null) {
\t\t\t// @RequestMapping to be introspected at method level
\t\t\treturn determineUrlsForHandlerMethods(handlerType, false);
\t\t}
\t\telse {
\t\t\treturn null;
\t\t}
\t}
即根据@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 {
\t\tif (logger.isDebugEnabled()) {
\t\t\tString requestUri = urlPathHelper.getRequestUri(request);
\t\t\tString resumed = WebAsyncUtils.getAsyncManager(request).hasConcurrentResult() ? " resumed" : "";
\t\t\tlogger.debug("DispatcherServlet with name '" + getServletName() + "'" + resumed +
\t\t\t\t\t" processing " + request.getMethod() + " request for [" + requestUri + "]");
\t\t}
\t\t// Keep a snapshot of the request attributes in case of an include,
\t\t// to be able to restore the original attributes after the include.
\t\tMap<String, Object> attributesSnapshot = null;
\t\tif (WebUtils.isIncludeRequest(request)) {
\t\t\tlogger.debug("Taking snapshot of request attributes before include");
\t\t\tattributesSnapshot = new HashMap<String, Object>();
\t\t\tEnumeration<?> attrNames = request.getAttributeNames();
\t\t\twhile (attrNames.hasMoreElements()) {
\t\t\t\tString attrName = (String) attrNames.nextElement();
\t\t\t\tif (this.cleanupAfterInclude || attrName.startsWith("org.springframework.web.servlet")) {
\t\t\t\t\tattributesSnapshot.put(attrName, request.getAttribute(attrName));
\t\t\t\t}
\t\t\t}
\t\t}
\t\t// Make framework objects available to handlers and view objects.
\t\trequest.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
\t\trequest.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
\t\trequest.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
\t\trequest.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
\t\tFlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
\t\tif (inputFlashMap != null) {
\t\t\trequest.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
\t\t}
\t\trequest.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
\t\trequest.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
\t\ttry {
\t\t\tdoDispatch(request, response);
\t\t}
\t\tfinally {
\t\t\tif (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
\t\t\t\treturn;
\t\t\t}
\t\t\t// Restore the original attribute snapshot, in case of an include.
\t\t\tif (attributesSnapshot != null) {
\t\t\t\trestoreAttributesAfterInclude(request, attributesSnapshot);
\t\t\t}
\t\t}
\t}
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,
\t\t\tnull, 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方法将视图输出到客户端的。