内容简介:我们可以方便的利用Spring MVC进行业务开发,请求的大部分工作都被框架和容器封装,使得我们只需要做很少量的工作。但是整个http请求流程是怎么样的?Spring MVC框架在其中起到什么作用?它是怎么和Web容器进行交互的?Controller中的一个方法怎么被暴露出来提供http请求服务的?本着这些想法,我们对整个http请求过程进行讨索。全文以整个过程包括三部分:应用启动、请求路由与处理、请求返回。应用启动:web容器初始化(context建立等)、应用初始化(初始化handlerMap)。
一、问题提出
我们可以方便的利用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方法将视图输出到客户端的。
以上所述就是小编给大家介绍的《基于Spring MVC框架的Http流程分析 原 荐》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Yii2框架的启动流程
- Lararel 框架执行流程详解(1) 入口文件
- 架构设计:基于 SpringCloud 框架模拟灰度发布流程
- 工具 | 一款流程可视化的前端框架
- gojs 流程图框架-编辑类模板(二)
- Linux 网络层收发包流程及 Netfilter 框架浅析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Web Designer's Idea Book, Vol. 2
Patrick McNeil / How / 2010-9-19 / USD 30.00
Web Design Inspiration at a Glance Volume 2 of The Web Designer's Idea Book includes more than 650 new websites arranged thematically, so you can easily find inspiration for your work. Auth......一起来看看 《The Web Designer's Idea Book, Vol. 2》 这本书的介绍吧!