SpringMVC源码分析总结

xiaoxiao2021-02-28  40

该文章基于《Spring源码深度解析》撰写,感谢郝佳老师的奉献 SpringMVC是基于Servlet功能实现的,通过带有Servlet接口的DispatcherServlet来封装核心功能,控制器则由实现了Controller接口的类,SpringMVC解决的痛点有三个: (1)将Web页面的请求传给服务器 (2)根据不同的请求处理不同的逻辑页面 (3)返回处理结果数据并跳转页面 Spring+SpringMVC开发的时候配置文件一般有 (1)web.xml(必须) 必须的原因在于:配置contexLoadListener,配置DispatcherServlet拦截请求 (2)applicationContext.xml(非必需,可以用来配置InternalResourceViewResolver来给返回的页面加上前缀进行试图查找) (3)model(pojo,非必需) (4)controller(继承AbstractController,用于处理Web请求) (5)视图(可以由jsp或其他试图框架完成) (6)Spring-servlet.xml(非必需,一般用于模块化开发) 现在先从最开始的web.xml文件进行分析,我们首先配置了contextConfigLocation如下所示:

<listener> <listener-class>org.Springframework.web.context.ContextLoaderListener</listener-class> </listener>

这个类的继承结构如下所示: 上图中的ServletContextListener该类位于servlet-api.jar包中(再tomcat的自带lib中可以找到) ContextLoaderListener实现了ServletContextListener接口,该接口保证了在能够为客户端提供服务之前向ServletContext中添加任何对象。每个Web应用都有一个ServletContext与之相关联,启动时被创建,关闭时被销毁,并且ServletContext在全局范围内有效。ServletContextListener的核心逻辑就是初始化WebApplicationContext实例并放入ServletContext中。 我们之所以使用了ContextLoaderListener是为了避免下面这种硬编码配置:

ApplicationContext ac = new ClassPathXmlApplecation("applicationContext.xml")

再来说说ContextLoaderListener的实际步骤是: (1)webApplication存在性验证(配置中只能有一个ServletContextListener接口,验证的方法就是检查该属性是否存在) (2)创建webApplication实例 (3)将属性注入(例如contextConfigLocation)webApplication (4)将实例记录到servletContext中 (5)映射当前的类加载器与创建的实例到全局变量currentContextPerThread中 讲完了ContextLoaderListener再来讲讲web.xml中另一个重要配置DispatcherServlet,对于DispatcherServlet的配置如下所示:

<servlet> <servlet-name>SpringMVC(可自定义)</servlet-name> <servlet-class>org.SpringMVC.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>SpringMVC(可自定义)<servlet-name> <url-pattern>自定义匹配规则(比如*.htm,那么将会匹配以htm为结尾的请求</url-pattern> </servlet-mapping>

接下来我们先给出DispatcherServlet的类层次结构,如下图所示: 可以看到,DispatcherServlet实际上是实现了Servlet接口的类,下面来讲讲Servlet接口: Servlet接口是基于HTTP协议的,servlet框架主要由两个java包组成javax.servlet(提供所有必须实现或扩展的通用接口和类)和javax.servlet.http(定义了采用HTTP通信协议的HttpServlet类),Servlet的生命周期是由servlet容器来控制的,它的生命周期分为三个阶段: (1)初始化阶段 servlet容器加载servlet类,将.class文件读入内存; servlet容器创建ServletConfig对象,包含了servlet的初始化配置信息; servlet容器创建servlet对象; servlet容器调用servlet对象的init方法进行初始化; 初始化的过程通过将当前的servlet类型转化为BeanWrapper类型实例(其目的是方便对应属性注入,属性可以在web.xml中通过<context-param>进行注入,其对应的属性位于其父类FrameworkServlet中,属性注入的步骤:封装及验证初始化参数→将当前servlet实例转化为BeanWrapper实例→注册相对于Resource(Resource类型的属性将会使用ResourceEditor进行解析)的属性编辑器→属性注入→servletBean的初始化(主要是对webApplicationContext实例进行填充)) (2)运行阶段 servlet接受请求时,针对请求创建servletRequest,servletResponse,调用service方法(通过servletRequest获得请求信息,根据servletResponse生成相应结果),最后销毁servletRequest,servletResponse两个对象 servlet被设计为请求驱动,请求会被封装为HttpServletRequest对象,然后传给对应服务方法。请求的类型一共有八中(常用的:GET,POST,PUT,DELETE,不太常用的:HEAD,TRACE,CONNECT.OPTIONS),在HttpServlet类中分别提供了对应的服务方法例如doDelete(),例外的是CONNECT并没有提供对应方法 (3)销毁阶段 调用servlet容器的destory方法之后再销毁servlet对象,我们可以将一些servlet占用的资源的释放放在destroy方法中实现

webApplicationContext及其他属性的初始化

由于在ContextLoaderListener和servlet的初始化中都出现了initWebApplicationContext方法,但是ContextLoaderListener中出现的是public WebApplicationContext initWebApplicationContext(ServletContext)该方法位于ContextLoader类中,servlet的方法则为 protected WebApplicationContext initWebApplicationContext() 该方法位于FrameworkServlet类中,下面来介绍后者的逻辑 1.寻找或创建对应的WebApplicationContext实例,该逻辑为

if(WebApplicationContext存在(仅可能通过构造器注入)){ 则通过构造函数的注入进行初始化webApplicationContext; 如果webApplication没有初始化Spring环境,那么进行初始化Spring环境包括加载配置文件; }else if(WebApplicationContext仍不存在){ 通过contextAttribute进行初始化webApplicationContext; }else if(WebApplicationContext仍不存在){ 通过反射的方式重新创建WebApplicationContext实例进行初始化,之后初始化Spring环境包括加载配置文件; if(如果没有刷新Spring中可能会用到的全局变量){ (文件上传解析器,只允许一个实例)初始化并添加MultipartResolver(MultipartResolver主要用于文件上传)至DispatcherServlet ; /*一般配置如<bean id = "xxx" class="org.Springframework.web.multipart.commons.CommonsMultipartResolver">*/ (本地化解析器,只允许一个实例)初始化LocaleResolver(国际化配置); /*第一种配置基于URL参数 配置如<bean id="XXX" class="org.Springframework.web.servlet.i8ln.AcceptHeaderLocaleResolver"> 配合<a href="?locale=zh_CN">控制使用国际化化参数 第二种配置基于session,如果会话属性不存在,那么通过accept-language HTTP头部确定默认区域 第三种配置基于Cookie,这种策略常用于应用不支持会话或者状态必须保持在客户端的情况 配置如<bean id="XXX" class="org.Springframework.web.servlet.i8ln.CookieLocaleResolver">*/ (主题解析器,只允许一个实例)初始化ThemeResolver; /* org.Springframework.ui.context.ThemeSource是Spring中主题资源的接口,实现存放主题信息资源的类都应该实现该接口 通过配置 <bean id="xxx" class="org.Springframework.ui.context.support.ResourceBundleThemeSource"> 其中ResourceBundleThemeSource就是实现了ThemeSource接口 <property name="basenamePrefix" value="com.test"> 配置在com.test路径下查找资源文件 </bean> ThemeSource配置了主题资源,不同用户的不同资源则由主题解析器定义。 由org.Springframework.web.servlet.ThemeResolver作为主题解析器的接口 该接口有三个实现类: 第一个是FixedThemeResolver用于设置固定主题 第二个是CookieThemeResolver效果是将主题以cookie的形式放在客户端的机器上 第三个是SessionThemeResolver效果是将主题保存在HTTP Session中 第四个是AbstractThemeResolver(是CookieThemeResolver和SessionThemeResolver的父类),用户可以继承它来实现自己的主题解析器 如果需要根据用户请求改变主题,那么Spring提供了一个已经实现的拦截器ThemeChangeInterceptor 主题拦截器的配置如下: <bean id="XXX" class="org.Springframework.web.servlet.theme.ThemeChangeInterceptor"> <property name="paramName" value="themeName"></property> </bean> 当然还需要在handlerMapping中添加该拦截器 <property name="interceptors"> <list> <ref local="themeChangeInterceptor"> </list> </property> */ (处理器映射器,允许多个实例)初始化HandlerMappings; /* 当客户端发出Request时DispatchServlet会将Request提交给HandlerMapping, 然后HandlerMapping根据WebApplicationContext的配置回传给DispatcherServlet相应的Controller。 (当然调用时会按照优先级进行排序,优先级高的优先调用) 默认情况下SpringMVC会加载当前所有实现了HandlerMapping接口的bean,如果只希望加载指定的bean,那么可以如下配置: <init-param> <param-name>detectAllHandlerMappings</param-name> <param-value>false</param-value> </init-param> 之后将按照Dispatcher.properties中所定义的规则来默认加载 */ (处理器适配器,允许多个实例)初始化HandlerAdapters; /* Spring中有三个默认的适配器,如果没有在配置文件中定义自己的适配器,那么Spring会默认加载这三个适配器 当servlet通过处理器映射得到处理器之后,就会轮询处理器适配器模块,查找能够处理当前HTTP请求的处理器适配器的实现。下面介绍这三种默认的适配器: 1.HTTP请求处理器适配器 2.简单控制器处理器适配器 3.注解方法处理器适配器 */ (处理器异常解析器,允许多个实例)初始化HandlerExceptionResolvers; /* 需要实现HandlerExceptionResolver接口,该接口只有一个方法,该方法返回ModelAndView对象。 如果实现类的该方法返回了null那么Spring会继续寻找其他实现了该接口的bean,直到返回一个ModelAndView对象。 该类必须声明到Spring的applicationContext.xml中进行管理: <bean id="xxx" class="实现类"/> */ (视图名称解析器,只允许一个实例)初始化RequestToViewNameTranslator; /* 用于处理没有返回View对象或者逻辑视图名称,并且该方法中没有直接在response的输出流中写数据时,提供逻辑视图名称。 Spring提供了一个默认的实现DefaultRequestToViewNameTranslator */ (视图解析器,允许多个实例)初始化ViewResolvers; /* 配置方法(于applicationContext.xml): <bean class="org.Springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="xxx"/> ... </bean> */ (映射个管理器)初始化FlashMapManager; /* Flashattributes提供了一个请求存储属性,在重定向时非常有用(在重定向之前暂存,以便继续使用) FlashMap用于保持flashattributes; FlashMapManager用于存储,检索,管理FlashMap实例 */ }

DispatcherServlet的处理逻辑

虽然HTTP1.1报文头一共有八种,常用的:GET、POST、PUT、DELETE,不太常用的:HEAD、TRACE、CONNECT、OPTIONS,但是实际上FrameworkServlet并没有实现CONNECT和HEAD。他们分别对应着doXXX()方法。 对于不同的方法,Spring统一的将他们引入到processRequest方法中,该方法完成了以下操作: (1)提取当前线程的LocaleContext和RequestAttribute以便在当前请求后还能够恢复 (2)根据当前request创建LocaleContext和RequestAttribute,并绑定到当前线程 (3)委托doService方法进一步处理 仍然是准备工作,将webApplicationContext,localeResolver等属性注入request中,然后继续由doDispatch方法进行处理,doDispatch方法的处理逻辑为: 第一步:将MultipartContent类的request转化为MultipartHttpServletRequest类型的request 第二步:根据request信息寻找对应的Handler,Spring在request到达后,通过遍历所有的HandlerMapping,如果只是String那么通过查找对应的bean找到controller,否则通过URL找出对应的controller(考虑直接匹配和通配符),之后通过getHandlerExecutionChain进行Handler封装,将拦截器加入执行链,这样方便扩展和拦截,也是AOP的重要基础,其中AbstractUrlHandlerMapping与AbstractController的关系如下所示: 第三步:若如果没有找到对应的Handler则通过response向用户报错 第四步:根据当前的Handler寻找对应的HandlerAdapter(遍历寻找) 第五步:缓存处理,根据Last-Modified缓存机制(第一次成功请求后如果在第二次请求之间内容没有改变,那么返回304状态码),Controller需要实现LastModified接口 第六步:HandlerInterceptor的处理,处理拦截必须实现HandlerInterceptor接口 第七步:逻辑处理,通过适配器中转调用Handler(处理用户定义的逻辑)并返回视图 第八步:异常视图处理,由HandlerExceptionResolver的resolveException完成 第九步:根据视图跳转页面,主要进行了一下几个方面的处理:基于效率的考虑,提供了对缓存的支持;提供了对redirect:xx和forward:xx的支持;添加了前缀以及后缀,并向View中加入了必需的属性设置。在页面跳转以前,还需要处理的就是将需要用到的属性放入request中。 (4)请求处理结束后恢复线程到原始状态 (5)发布事件通知

转载请注明原文地址: https://www.6miu.com/read-2350376.html

最新回复(0)