【5】Spring源码-SpringMVC
6. SpringMVC
SpringMVC的实现原理是通过servlet拦截所有URL来达到控制的目的。SpringMVC是基于Servlet的实现。真正的逻辑实现是在DispatcherServlet中进行的,DispatcherServlet继承了httpServlet,httpServlet继承了Servlet。httpServlet重写service方法,根据请求方法调用doGet,doPost等。Servlet接到请求就调用service方法。
-
对于一个web应用,其部署在web容器中,web容器提供其一个全局的上下文环境,这个上下文就是ServletContext,其为后面的spring IoC容器提供宿主环境;
-
在web.xml中会提供有contextLoaderListener。在web容器启动时,会触发容器初始化事件,此时contextLoaderListener会监听到这个事件,其contextInitialized方法会被调用,在这个方法中,spring会初始化一个启动上下文,这个上下文被称为根上下文,即WebApplicationContext,这是一个接口类,确切的说,其实际的实现类是XmlWebApplicationContext。这个就是spring的IoC容器,其对应的Bean定义的配置由web.xml中的context-param标签指定。在这个IoC容器初始化完毕后,spring以WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE为属性Key,将其存储到ServletContext中,便于获取;
-
contextLoaderListener监听器初始化完毕后,开始初始化web.xml中配置的Servlet,这个servlet可以配置多个(一个web应用可以有多个DispatcherServlet),以最常见的DispatcherServlet为例,这个servlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个servlet请求。DispatcherServlet上下文在初始化的时候会建立自己的IoC上下文,用以持有spring mvc相关的bean。在建立DispatcherServlet自己的IoC上下文时,会利用WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE先从ServletContext中获取之前的根上下文(即WebApplicationContext)作为自己上下文的parent上下文。有了这个parent上下文之后,再初始化自己持有的上下文。这个DispatcherServlet初始化自己上下文的工作在其initStrategies方法中可以看到,大概的工作就是初始化处理器映射、视图解析等。这个servlet自己持有的上下文默认实现类也是xmlWebApplicationContext。初始化完毕后,spring以与servlet的名字相关(此处不是简单的以servlet名为Key,而是通过一些转换)的属性为属性Key,也将其存到ServletContext中,以便后续使用。这样每个servlet就持有自己的上下文,即拥有自己独立的bean空间,同时各个servlet共享相同的父bean,即根上下文(第2步中初始化的上下文)定义的那些bean。
6.1 DispatcherServlet的初始化
DispatcherServlet的初始化过程主要是:
-
通过将当前的servlet类型实例转换为BeanWrapper类型实例,以便使用Spring中提供的注入功能进行对应属性的注入。
-
初始化WebApplicationContext,
DispatcherServlet中会持有一个WebApplicationContext对象的成员变量(DispatcherServlet中持有的Ioc容器中有Controller、Spring默认的各种HandlerMapping、HandlerAdapter):
a. 初始化WebApplicationContext,并设置其parent ApplicationContext。
b. 调用WebApplicationContext的refresh方法进行配置文件加载。
c. 调用DispatcherServlet重写的OnRefresh方法,初始化各种:
-
initMultipartResolver:用于处理文件上传的类,
使用applicationContext.getBean()方法获取,并传入DispatcherServlet的一个成员变量中
-
initLocaleResolver:处理国际化问题,也是使用applicationContext.getBean()方法获取,如果拿不到则使用默认的策略(DefaultStrategy:在DispatcherServlet有静态代码块初始化,从文件中读取)
-
initThemeResolver:处理网站的主题资源,也是使用applicationContext.getBean()方法获取,如果拿不到则使用默认的策略
-
initHandlerMappings
:当客户端发出Request时DispatcherServlet会将Request提交给HandlerMapping,然后HandlerMapping根据Web Application Context的配置来回传给DispatcherServlet相应的Controller。
可以为DispatcherServlet提供多个Handler Mapping供其使用。DispatcherServlet在选用HandlerMapping的过程中,将根据我们所指定的一系列HandlerMapping的优先级进行排序(@Order注解
),然后优先使用优先级在前的HandlerMapping。如果当前的HandlerMapping能够返回可用的Handler,DispatcherServlet则使用当前返回的Handler进行Web请求的处理,而不再继续询问其他的HandlerMapping。否则,DispatcherServlet将继续按照各个HandlerMapping的优先级进行询问,直到获取一个可用的Handler为止。也是使用applicationContext.getBean()方法获取,如果拿不到则使用默认的策略可以有多个HandlerMapping。 -
initHandlerAdapters
:作为总控制器的派遣器servlet通过处理器映射得到处理器后,会轮询处理器适配器模块,查找能够处理当前HTTP请求的处理器适配器的实现,处理器适配器模块根据处理器映射返回的处理器类型,例如简单的控制器类型、注解控制器类型或者远程调用处理器类型,来选择某一个适当的处理器适配器的实现,从而适配当前的HTTP请求。
也是使用applicationContext.getBean()方法获取,如果拿不到则使用默认的策略,可以有多个HandlerAdapter。 -
initHandlerExceptionResolvers:异常处理。也是使用applicationContext.getBean()方法获取,如果拿不到则使用默认的策略,可以有多个HandlerExceptionResolver。
-
initRequestToViewNameTranslator:Controller处理器方法没有返回一个View对象或逻辑视图名称,并且在该方法中没有直接往response的输出流里面写数据的时候,Spring就会采用约定好的方式提供一个逻辑视图名称。也是使用applicationContext.getBean()方法获取,如果拿不到则使用默认的策略。
-
initViewResolvers
:在SpringMVC中,当Controller将请求处理结果放入到ModelAndView中以后, DispatcherServlet会根据ModelAndView选择合适的视图View进行渲染。ViewResolver接口定义了resolverViewName方法,根据viewName创建合适类型的View实现。可以配置JSP相关的viewResolver。
也是使用applicationContext.getBean()方法获取,如果拿不到则使用默认的策略,可以有多个ViewResolver。 -
initFlashMapManager:SpringMVC Flash attributes提供了一个请求存储属性,可供其他请求使用。在使用重定向时候非常必要,例如Post/Redirect/Get模式。Flash attributes在重定向之前暂存(就像存在session中)以便重定向之后还能使用,并立即删除。默认开启。也是使用applicationContext.getBean()方法获取,如果拿不到则使用默认的策略。
// DispatcherServlet
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
6.2 DispatcherServlet的执行逻辑
请求->调用HttpServlet的doGet、doPost方法->DispatcherServlet的doService->DispatcherServlet的doDispatch
doDispatch中的逻辑:
-
检查是否为multipart请求,如果是则转换为MultipartHTTPServletRequest;
-
根据Request信息在HandlerMappings中寻找对应的handler;
image.png
a. 变量HandlerMappings中的所有hanlder,可以根据url查找匹配相应的handler;
i. 匹配时会将用户输入的url去除多余的”/“,如”/test//a///b”变成”/test/a/b"
ii. RequestMapping(“/test/a/b”)在匹配时也会在最后加上”/“,保证能更”test/a/b/“匹配上
b.将查找到的handler加入到拦截器链中;
c. HandlerMapping中包含对应的controller和方法,启动时会将controller中的方法注入RequestMappingHandlerMapping中;
i. Spring启动时配置好相应的HandlerMapping
ii. RequestMappingHandlerMapping中会注入使用@RequestMapping注解的url映射关系
image.png
d. handler是一个封装了Controller和具体要调用到Controller中的某个method的类(HandlerMethod),包含url到Controller到method的映射关系;
-
根据当前handler寻找对应的HandlerAdapter;
a. 遍历所有的HandlerAdapter;
b. 调用HandlerAdapter的supports方法判断是否匹配,supports方法实际上就是判断handler的类型(如SimpleControllerHandlerAdapter就是判断handler是否为Controller类型);
c.HandlerAdapter主要做一些校验和准备工作(处理方法参数、相关注解、数据绑定、消息转换、返回值、调用视图解析器等等,如加了@ResponseBody就不会返回视图),并调用handler的反射执行方法;
-
处理lastModified缓存;
-
调用拦截器preHandler方法;
-
执行handler(调用HandlerAdapter.handle方法,该方法把具体处理逻辑委托给handler)并返回视图ModelAndView;
a. @Controller注解由这个处理:org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
b. 最终用拿到Controller的bean,以及对应要执行的method,利用反射调用method;
c. ModelAndView,view为要跳转的页面,model为注入的信息;
image.png -
调用拦截器的postHandler方法
-
处理页面跳转(redirect:xx, forward:xx),并将Model中的属性设置到request中;
6.3 父子容器
-
DispatcherServlet中持有一个Ioc容器,其父容器为Spring的根Ioc容器,DispatcherServlet可以有多个,他们持有的多个Ioc容器均为同一个Spring跟Ioc容器。
-
使用这种父子容器的主要原因在于:
a. 解耦:
i. 在J2EE三层架构中,在service层我们一般使用spring框架, 而在web层则有多种选择,如spring mvc、struts等。因此,通常对于web层我们会使用单独的配置文件。如果我们将父子容器都配置与同一个applicationContext.xml中,那么我们想要将spring mvc替换为structs,还需要修改applicationContext.xml,而分为两种配置则无需修改applicationContext.xml。
ii. 事实上,如果你的项目确定了只使用spring和spring mvc的话,你甚至可以将service 、dao、web层的bean都放到spring-servlet.xml中进行配置,并不是一定要将service、dao层的配置单独放到applicationContext.xml中,然后使用ContextLoaderListener来加载。在这种情况下,就没有了Root WebApplicationContext,只有Servlet WebApplicationContext。
iii. 如果在子容器中取不到bean,则会去父容器中取。