特性:
说到Spring整合SpringMVC唯一的体现就是父子容器:
实现:
相信大家在SSM框架整合的时候都曾在web.xml配置过这段:
- <listener>
- <listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
- listener>
- <context-param>
- <param-name>contextConfigLocationparam-name>
- <param-value>classpath:spring-core.xmlparam-value>
- context-param>
- <servlet>
- <servlet-name>dispatcherServletservlet-name>
- <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
-
- <init-param>
- <param-name>contextConfigLocationparam-name>
- <param-value>classpath:spring-mvc.xmlparam-value>
- init-param>
-
- <load-on-startup>1load-on-startup>
- servlet>
- <servlet-mapping>
- <servlet-name>dispatcherServletservlet-name>
- <url-pattern>/url-pattern>
但是它的作用是什么知道吗?
有人可能只知道DispatcherServlet叫前端控制器,是SpringMVC处理前端请求的一个核心调度器
那它为什么能处理请求?处理之前做了什么准备工作呢?又是怎么和Spring结合起来的呢?
为什么有了DispatcherServlet还要个ContextLoaderListener, 配一个不行吗?干嘛要配俩啊?
看完本文你就会有答案!
还有人可能会觉得, 我现在都用SpringBoot开发, 哪还要配这玩意.......
这就是典型的SpringBoot使用后遗症,SpringBoot降低了使用难度,但是从某种程度来说,也让初级的程序员变得更加小白,把实现原理都隐藏起来了而我们只管用,一旦涉及扩展就束手无策。
那当然我们今天不讲SpringBoot,我们今天用贴近SpringBoot的方式来讲SpringMVC。也就是零配置(零xml)的放式来说明SpringMVC的原理!!
此方式作为我们本文重点介绍,也是很多人缺失的一种方式, 其实早在Spring3+就已经提供, 只不过我们直到SpringBoot才使用该方式进行自动配置, 这也是很多人从xml调到SpringBoot不适应的原因, 因为你缺失了这个版本。 所以我们以这种方式作为源码切入点既可以理解到XML的方式又能兼顾到SpringBoot的方式 。
那没有配置就需要省略掉web.xml 怎么省略呢?
在Servlet3.0提供的规范文档中可以找到2种方式:
但是这种方式不利于扩展, 并且如果编写在jar包中tomcat是无法感知到的。
在Serlvet3-1的规范手册中:就提供了一种更加易于扩展可用于共享库可插拔的一种方式,参见8.2.4:
也就是让你在应用META-INF/services 路径下 放一个 javax.servlet.ServletContainerInitailizer ——即SPI规范
SPI 我们叫他服务接口扩展,(Service Provider Interface) 直译服务提供商接口, 不要被这个名字唬到了, 其实很好理解的一个东西:
其实就是根据Servlet厂商(服务提供商)提供要求的一个接口, 在固定的目录(META-INF/services)放上以接口全类名 为命名的文件, 文件中放入接口的实现的全类名,该类由我们自己实现,按照这种约定的方式(即SPI规范),服务提供商会调用文件中实现类的方法, 从而完成扩展。
ok 那我们知道了SPI是什么,我们是不是可以在Web应用中,在Servlet的SPI放入对应的接口文件:
放入实现类:
通过ServletContext就可以动态注册三大组件:以Servlet注册为例:
- public class TulingSpringServletContainerInitializer extends SpringServletContainerInitializer {
-
- @Override
- public void onStartup(Set
> webAppInitializerClasses, ServletContext servletContext) throws ServletException { -
- // 通过servletContext动态添加Servlet
- servletContext.addServlet("spiServlet", new HttpServlet() {
- @Override
- protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
- resp.getWriter().write("spiServlet--doGet");
- }
- }).addMapping("/spiServlet.do");
-
-
- }
当然在SpringMVC中, 这个接口文件和实现类都把我们实现好了,甚至ContextLoaderListener和DispatcherServlet都帮我们注册好了,我们只要让他生效,来,看看他是怎么做的:
TulingStarterInitializer
- public class TulingStarterInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
-
- /**
- * 方法实现说明:IOC 父容器的启动类
- * @author:xsls
- * @date:2019/7/31 22:12
- */
- @Override
- protected Class>[] getRootConfigClasses() {
- return new Class[]{RootConfig.class};
- }
-
- /**
- * 方法实现说明 IOC子容器配置 web容器配置
- * @author:xsls
- * @date:2019/7/31 22:12
- */
- @Override
- protected Class>[] getServletConfigClasses() {
- return new Class[]{WebAppConfig.class};
- }
-
- /**
- * 方法实现说明
- * @author:xsls
- * @return: 我们前端控制器DispatcherServlet的拦截路径
- * @exception:
- * @date:2019/7/31 22:16
- */
- @Override
- protected String[] getServletMappings() {
- return new String[]{"/"};
- @Configuration
- @ComponentScan(basePackages = "com.tuling",excludeFilters = {
- @ComponentScan.Filter(type = FilterType.ANNOTATION,value={RestController.class,Controller.class}),
- @ComponentScan.Filter(type = ASSIGNABLE_TYPE,value =WebAppConfig.class ),
- })
- public class RootConfig {
-
- }
- @Configuration
- @ComponentScan(basePackages = {"com.tuling"},includeFilters = {
- @ComponentScan.Filter(type = FilterType.ANNOTATION,value = {RestController.class, Controller.class})
- },useDefaultFilters =false)
- @EnableWebMvc // ≈
- public class WebAppConfig implements WebMvcConfigurer{
-
- /**
- * 配置拦截器
- * @return
- */
- @Bean
- public TulingInterceptor tulingInterceptor() {
- return new TulingInterceptor();
- }
-
- /**
- * 文件上传下载的组件
- * @return
- */
- @Bean
- public MultipartResolver multipartResolver() {
- CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
- multipartResolver.setDefaultEncoding("UTF-8");
- multipartResolver.setMaxUploadSize(1024*1024*10);
- return multipartResolver;
- }
-
- /**
- * 注册处理国际化资源的组件
- * @return
- */
- /* @Bean
- public AcceptHeaderLocaleResolver localeResolver() {
- AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
- return acceptHeaderLocaleResolver;
- }*/
-
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(tulingInterceptor()).addPathPatterns("/*");
- }
-
-
- /**
- * 方法实现说明:配置试图解析器
- * @author:xsls
- * @exception:
- * @date:2019/8/6 16:23
- */
- @Bean
- public InternalResourceViewResolver internalResourceViewResolver() {
- InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
- viewResolver.setSuffix(".jsp");
- viewResolver.setPrefix("/WEB-INF/jsp/");
- return viewResolver;
- }
-
-
-
- @Override
- public void configureMessageConverters(List
> converters) { - converters.add(new MappingJackson2HttpMessageConverter());
- }
自己去添加个Controller进行测试
OK, 现在可以访问你的SpringMVC了
接着我们来看看SPI方式的原理是什么:
SpringMVC 大致可以分为 启动 和请求 2大部分, 所以我们本文先研究启动部分
流程图:
源码流程
1. 首先来到AbstractDispatcherServletInitializer#onStartup再执行super.onStartup(servletContext);
- @Override
- public void onStartup(ServletContext servletContext) throws ServletException {
- //实例化我们的spring root上下文
- super.onStartup(servletContext);
- //注册我们的DispatcherServlet 创建我们spring web 上下文对象
- registerDispatcherServlet(servletContext);
2.父类AbstractContextLoaderInitializer#onStartup执行registerContextLoaderListener(servletContext);
看完大家是不是感觉跟我们XML的配置ContextLoaderListener对上了:
创建子容器——DispatcherServlet
3.回到AbstractDispatcherServletInitializer#onStartup再执行registerDispatcherServlet(servletContext);
registerDispatcherServlet方法说明:
看完大家是不是感觉跟我们XML的配置DispatcherServlet对上了
4. 初始化ContextLoaderListener
ContextLoaderListener加载过程比较简单:
外置tomcat会帮我们调用ContextLoaderListener#contextInitialized 进行初始化
5. 初始化DispatcherServlet
可以看到流程比ContextLoaderListener流程更多
外置tomcat会帮我们调用DispatcherServlet#init() 进行初始化--->重点关注:initWebApplicationContext方法
当执行refresh 即加载ioc容器 完了会调用finishRefresh():
---->FrameworkServlet.this.onApplicationEvent(event);
-------->onRefresh(event.getApplicationContext());
-------------->initStrategies(context);
- protected void initStrategies(ApplicationContext context) {
- //初始化我们web上下文对象的 用于文件上传下载的解析器对象
- initMultipartResolver(context);
- //初始化我们web上下文对象用于处理国际化资源的
- initLocaleResolver(context);
- //主题解析器对象初始化
- initThemeResolver(context);
- //初始化我们的HandlerMapping
- initHandlerMappings(context);
- //实例化我们的HandlerAdapters
- initHandlerAdapters(context);
- //实例化我们处理器异常解析器对象
- initHandlerExceptionResolvers(context);
- initRequestToViewNameTranslator(context);
- //给DispatcherSerlvet的ViewResolvers处理器
- initViewResolvers(context);
- initFlashMapManager(context);
这里面的每一个方法不用太细看, 就是给SpringMVC准备初始化的数据, 为后续SpringMVC处理请求做准备
基本都是从容器中拿到已经配置的Bean(RequestMappingHandlerMapping、RequestMappingHandlerAdapter、HandlerExceptionResolver )放到dispatcherServlet中做准备:
...
但是这些Bean又是从哪来的呢?? 来来来, 回到我们的WebAppConfig
我们使用的一个@EnableWebMvc
- /**
- * 一般情况下,只有Spring 和SpringMvc整合的时才会有父子容器的概念,
- * 作用:
- * 比如我们的Controller中注入Service的时候,发现我们依赖的是一个引用对象,那么他就会调用getBean去把service找出来
- * 但是当前所在的容器是web子容器,那么就会在这里的 先去父容器找
- */
- BeanFactory parentBeanFactory = getParentBeanFactory();
- //若存在父工厂,且当前的bean工厂不存在当前的bean定义,那么bean定义是存在于父beanFacotry中
- if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
- //获取bean的原始名称
- String nameToLookup = originalBeanName(name);
- //若为 AbstractBeanFactory 类型,委托父类处理
- if (parentBeanFactory instanceof AbstractBeanFactory) {
- return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
- nameToLookup, requiredType, args, typeCheckOnly);
- }
- else if (args != null) {
- // 委托给构造函数 getBean() 处理
- return (T) parentBeanFactory.getBean(nameToLookup, args);
- }
- else {
- // 没有 args,委托给标准的 getBean() 处理
- return parentBeanFactory.getBean(nameToLookup, requiredType);
- }