| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 系统运维 -> SpringMVC启动原理与父子容器源码剖析 -> 正文阅读 |
|
[系统运维]SpringMVC启动原理与父子容器源码剖析 |
1、父子容器的关系,启动过程。父容器加载service,dao服务,子容器加载controller等mvc组件类。tomcat通过SPI的机制加载ServletContainerInitializer的实现类的方式来找到WebApplicationContainer的实现类并执行WebApplicationContainer实现类的onStartup方法的方式来启动父子容器的(AbstractContextLoaderInitializer#onStartup中通过registerContextLoaderListener(servletContext)创建父容器,AbstractDispatcherServletInitializer#onStartup中通过registerDispatcherServlet(servletContext)创建子容器)。 1、Spring整合SpringMVC 2、零配置SpringMVC实现方式: 3、实现基于SPI规范的SpringMVC 4、SPI的方式SpringMVC启动原理 流程图: 源码流程 创建父容器——ContextLoaderListener 创建子容器——DispatcherServlet 4. 初始化ContextLoaderListener 5. 初始化DispatcherServlet 总结 用几道面试题做个总结: 1、Spring整合SpringMVC 特性: 说到Spring整合SpringMVC唯一的体现就是父子容器: 通常我们会设置父容器( Spring)管理Service、Dao层的Bean, 子容器 (SpringMVC)管理Controller的Bean . 子容器可以访问父容器的Bean, 父容器无法访问子容器的Bean。 实现: 相信大家在SSM框架整合的时候都曾在web.xml配置过这段: 1 <!‐‐spring 基于web应用的启动‐‐> 2 <listener> 3 <listener‐class>org.springframework.web.context.ContextLoaderListener</liste ner‐class> 4 </listener> 5 <!‐‐全局参数:spring配置文件‐‐> 6 <context‐param>7 <param‐name>contextConfigLocation</param‐name> 8 <param‐value>classpath:spring‐core.xml</param‐value> 9 </context‐param> 10 <!‐‐前端调度器servlet‐‐> 11 <servlet> 12 <servlet‐name>dispatcherServlet</servlet‐name> 13 <servlet‐class>org.springframework.web.servlet.DispatcherServlet</servlet‐c lass> 14 <!‐‐设置配置文件的路径‐‐> 15 <init‐param> 16 <param‐name>contextConfigLocation</param‐name> 17 <param‐value>classpath:spring‐mvc.xml</param‐value> 18 </init‐param> 19 <!‐‐设置启动即加载‐‐> 20 <load‐on‐startup>1</load‐on‐startup> 21 </servlet> 22 <servlet‐mapping> 23 <servlet‐name>dispatcherServlet</servlet‐name> 24 <url‐pattern>/</url‐pattern> 25 </servlet‐mapping> 但是它的作用是什么知道吗? 有人可能只知道DispatcherServlet叫前端控制器,是SpringMVC处理前端请求的一个核心调度 器 那它为什么能处理请求?处理之前做了什么准备工作呢?又是怎么和Spring结合起来的呢? 为什么有了DispatcherServlet还要个ContextLoaderListener, 配一个不行吗?干嘛要配俩 啊? 看完本文你就会有答案! 还有人可能会觉得, 我现在都用SpringBoot开发, 哪还要配这玩意....... 这就是典型的SpringBoot使用后遗症,SpringBoot降低了使用难度,但是从某种程度来说,也 让初级的程序员变得更加小白,把实现原理都隐藏起来了而我们只管用,一旦涉及扩展就束手 无策。 那当然我们今天不讲SpringBoot,我们今天用贴近SpringBoot的方式来讲SpringMVC。也就是零配置(零xml)的放式来说明SpringMVC的原理!! 此方式作为我们本文重点介绍,也是很多人缺失的一种方式, 其实早在Spring3+就已经提供, 只不过 我们直到SpringBoot才使用该方式进行自动配置, 这也是很多人从xml调到SpringBoot不适应的原 因, 因为你缺失了这个版本。 所以我们以这种方式作为源码切入点既可以理解到XML的方式又能兼顾 到SpringBoot的方式 。 2、零配置SpringMVC实现方式: 那没有配置就需要省略掉web.xml 怎么省略呢? 在Servlet3.0提供的规范文档中可以找到2种方式: 1. 注解的方式 a. @WebServlet b. @WebFilter c. @WebListener 但是这种方式不利于扩展, 并且如果编写在jar包中tomcat是无法感知到的。 2. SPI的方式 在Serlvet3-1的规范手册中:就提供了一种更加易于扩展可用于共享库可插拔的一种方式, 参见8.2.4:也就是让你在应用META-INF/services 路径下 放一个 javax.servlet.ServletContainerInitailizer ——即SPI规范 啥?? 啥是SPI?? SPI 我们叫他服务接口扩展,(Service Provider Interface) 直译服务提供商接口, 不 要被这个名字唬到了, 其实很好理解的一个东西: 其实就是根据Servlet厂商(服务提供商)提供要求的一个接口, 在固定的目录 (META-INF/services)放上以接口全类名 为命名的文件, 文件中放入接口的实现的 全类名,该类由我们自己实现,按照这种约定的方式(即SPI规范),服务提供商会 调用文件中实现类的方法, 从而完成扩展。 SPI演示案例: 假设我们自己是服务提供商: 现在要求的一个接口 IUserDao 1.在固定的目录放上接口的文件名 2.文件中放入实现类(该实现类由你实现): 一行一个实现类。 3.通过java.util.ServiceLoader提供的ServiceLoader就可以完成SPI的实现类加载 1 public class App { 2 public static void main(String[] args) { 3 ServiceLoader<IUserDao> daos = ServiceLoader.load(IUserDao.class); 4 for (IUserDao dao : daos) { 5 dao.save(); 6 } 7 } 8 }ok 那我们知道了SPI是什么,我们是不是可以在Web应用中,在Servlet的SPI放入对应的接口文 件: 放入实现类: 通过ServletContext就可以动态注册三大组件:以Servlet注册为例: 1 2 public class TulingSpringServletContainerInitializer extends SpringServletCon tainerInitializer { 3 4 @Override 5 public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException { 6 7 // 通过servletContext动态添加Servlet 8 servletContext.addServlet("spiServlet", new HttpServlet() { 9 @Override 10 protected void doGet(HttpServletRequest req, HttpServletResponse resp) thro ws ServletException, IOException { 11 resp.getWriter().write("spiServlet‐‐doGet"); 12 } 13 }).addMapping("/spiServlet.do"); 14 15 16 } 17 } 当然在SpringMVC中, 这个接口文件和实现类都把我们实现好了,甚至 ContextLoaderListener和DispatcherServlet都帮我们注册好了,我们只要让他生效,来,看 看他是怎么做的:3、实现基于SPI规范的SpringMVC TulingStarterInitializer 此类继承AbstractAnnotationConfigDispatcherServletInitializer 这是个啥? 待会 我们讲原理来介绍 getRootConfigClasses 提供父容器的配置类 getServletConfigClasses 提供子容器的配置类 getServletMappings 设置DispatcherServlet的映射 1 2 public class TulingStarterInitializer extends AbstractAnnotationConfigDispatc herServletInitializer { 3 4 /** 5 * 方法实现说明:IOC 父容器的启动类 6 * @author:xsls 7 * @date:2019/7/31 22:12 8 */ 9 @Override 10 protected Class<?>[] getRootConfigClasses() { 11 return new Class[]{RootConfig.class}; 12 } 13 14 /** 15 * 方法实现说明 IOC子容器配置 web容器配置 16 * @author:xsls 17 * @date:2019/7/31 22:12 18 */ 19 @Override 20 protected Class<?>[] getServletConfigClasses() { 21 return new Class[]{WebAppConfig.class}; 22 } 23 24 /** 25 * 方法实现说明 26 * @author:xsls 27 * @return: 我们前端控制器DispatcherServlet的拦截路径 28 * @exception: 29 * @date:2019/7/31 22:16 30 */ 31 @Override32 protected String[] getServletMappings() { 33 return new String[]{"/"}; 34 } RootConfig 父容器的配置类 =以前的spring.xml 扫描的包排除掉@Controller 1 @Configuration 2 @ComponentScan(basePackages = "com.tuling",excludeFilters = { 3 @ComponentScan.Filter(type = FilterType.ANNOTATION,value={RestController.cla ss,Controller.class}), 4 @ComponentScan.Filter(type = ASSIGNABLE_TYPE,value =WebAppConfig.class ), 5 }) 6 public class RootConfig { 7 } WebAppConfig 子容器的配置类 =以前的spring-mvc.xml 扫描的包:包含掉@Controller 1 2 @Configuration 3 @ComponentScan(basePackages = {"com.tuling"},includeFilters = { 4 @ComponentScan.Filter(type = FilterType.ANNOTATION,value = {RestController.c lass, Controller.class}) 5 },useDefaultFilters =false) 6 @EnableWebMvc // ≈<mvc:annotation‐driven/> 7 public class WebAppConfig implements WebMvcConfigurer{ 8 9 /** 10 * 配置拦截器 11 * @return 12 */ 13 @Bean 14 public TulingInterceptor tulingInterceptor() { 15 return new TulingInterceptor(); 16 } 17 18 /** 19 * 文件上传下载的组件 20 * @return 21 */ 22 @Bean23 public MultipartResolver multipartResolver() { 24 CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(); 25 multipartResolver.setDefaultEncoding("UTF‐8"); 26 multipartResolver.setMaxUploadSize(1024*1024*10); 27 return multipartResolver; 28 } 29 30 /** 31 * 注册处理国际化资源的组件 32 * @return 33 */ 34 /* @Bean 35 public AcceptHeaderLocaleResolver localeResolver() { 36 AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLoc aleResolver(); 37 return acceptHeaderLocaleResolver; 38 }*/ 39 40 @Override 41 public void addInterceptors(InterceptorRegistry registry) { 42 registry.addInterceptor(tulingInterceptor()).addPathPatterns("/*"); 43 } 44 45 46 /** 47 * 方法实现说明:配置试图解析器 48 * @author:xsls 49 * @exception: 50 * @date:2019/8/6 16:23 51 */ 52 @Bean 53 public InternalResourceViewResolver internalResourceViewResolver() { 54 InternalResourceViewResolver viewResolver = new InternalResourceViewResolve r(); 55 viewResolver.setSuffix(".jsp"); 56 viewResolver.setPrefix("/WEB‐INF/jsp/"); 57 return viewResolver; 58 } 59 60 61 62 @Override63 public void configureMessageConverters(List<HttpMessageConverter<?>> conver ters) { 64 converters.add(new MappingJackson2HttpMessageConverter()); 65 } 66 67 } 自己去添加个Controller进行测试 OK, 现在可以访问你的SpringMVC了 4、SPI的方式SpringMVC启动原理 接着我们来看看SPI方式的原理是什么: SpringMVC 大致可以分为 启动 和请求 2大部分, 所以我们本文先研究启动部分 流程图: ? ? 源码流程 1. 外置Tomcat启动的时候通过SPI 找到我们应用中的/META- INF/service/javax.servlet.ServletContainerInitializer2. 调用SpringServletContainerInitializer.onStartUp() a. 调用onStartUp()前会先找到 @HandlesTypes(WebApplicationInitializer.class) 所有实 现了WebApplicationInitializer的类,传入到OnStartup的 webAppInitializerClasses参数中,并传入Servlet上下文对 象。 b. 重点关注这组类:他们组成了父子容器3. 找到所有WebApplicationInitializer的实现类后, 不是接口、不是抽象则 通过反射进行实例化(所以,你会发现内部实现类都是抽象的,你想让其起作 用我们必须添加一个自定义实现类,在下文提供我的自定义实现类) 4. 调用所有上一步实例化后的对象的onStartup方法1. 首先来到AbstractDispatcherServletInitializer#onStartup再执行 super.onStartup(servletContext); 1 @Override 2 public void onStartup(ServletContext servletContext) throws ServletException { 3 //实例化我们的spring root上下文 4 super.onStartup(servletContext); 5 //注册我们的DispatcherServlet 创建我们spring web 上下文对象 6 registerDispatcherServlet(servletContext); 7 } 创建父容器——ContextLoaderListener 2.父类AbstractContextLoaderInitializer#onStartup执行 registerContextLoaderListener(servletContext); 1. createRootApplicationContext()该方法中会创建父容器 a. 该方法是抽象方法,实现类是 AbstractAnnotationConfigDispatcherServletInitializer i. 调用getRootConfigClasses();方法获取父容器配置类 (此抽象方法在我们自定义的子类中实现提供我们自定 义的映射路径 ) ii. 创建父容器,注册配置类 2. 会创建ContextLoaderListener并通过ServletContext注册看完大家是不是感觉跟我们XML的配置ContextLoaderListener对上了: 创建子容器——DispatcherServlet 3.回到AbstractDispatcherServletInitializer#onStartup再执行 registerDispatcherServlet(servletContext); registerDispatcherServlet方法说明: 1. 调用createServletApplicationContext创建子容器 a. 该方法是抽象方法,实现类是 AbstractAnnotationConfigDispatcherServletInitializeri. 创建子容器(下图很明显不多介绍) ii. 调用抽象方法:getServletConfigClasses();获得配 置类(此抽象方法在我们自定义的子类中实现提供我们 自定义的配置类 ) iii. 配置类除了可以通过ApplicationContext()构造函数 的方式传入 , 也可以通过这种方式动态添加,不知道 了吧~ 2. 调用createDispatcherServlet(servletAppContext);创建DispatcherServlet 3. 设置启动时加载:registration.setLoadOnStartup(1); 4. 调用抽象方法设置映射路径:getServletMappings()(此抽象方法在我们自定义的子 类中实现提供我们自定义的映射路径 ) 看完大家是不是感觉跟我们XML的配置DispatcherServlet对上了 4. 初始化ContextLoaderListenerContextLoaderListener加载过程比较简单: 外置tomcat会帮我们调用ContextLoaderListener#contextInitialized 进行初始化 1. xml的方式下会判断容器为空时创建父容器 2. 在里面会调用父容器的refresh方法加载 3. 将父容器存入到Servlet域中供子容器使用 5. 初始化DispatcherServlet可以看到流程比ContextLoaderListener流程更多 外置tomcat会帮我们调用DispatcherServlet#init() 进行初始化--->重点关注: initWebApplicationContext方法 1 . getWebApplicationContext(getServletContext())获得父容器(从之前的 Servlet域中拿到) 2. cwac.setParent(rootContext);给子容器设置父容器 3. 调用configureAndRefreshWebApplicationContext(cwac);a. 注册一个监听器(该监听会初始化springmvc所需信息) i. ContextRefreshedEvent可以看到该监听器监听 的是容器refreshed事件, 会在finishRefresh中发布 b. 刷新容器 当执行refresh 即加载ioc容器 完了会调用finishRefresh(): 1. publishEvent(new ContextRefreshedEvent(this));发布 ContextRefreshedEvent事件 2. 触发上面的ContextRefreshListener监听器: ---->FrameworkServlet.this.onApplicationEvent(event); -------->onRefresh(event.getApplicationContext()); -------------->initStrategies(context); 1 protected void initStrategies(ApplicationContext context) { 2 //初始化我们web上下文对象的 用于文件上传下载的解析器对象 3 initMultipartResolver(context);4 //初始化我们web上下文对象用于处理国际化资源的 5 initLocaleResolver(context); 6 //主题解析器对象初始化 7 initThemeResolver(context); 8 //初始化我们的HandlerMapping 9 initHandlerMappings(context); 10 //实例化我们的HandlerAdapters 11 initHandlerAdapters(context); 12 //实例化我们处理器异常解析器对象 13 initHandlerExceptionResolvers(context); 14 initRequestToViewNameTranslator(context); 15 //给DispatcherSerlvet的ViewResolvers处理器 16 initViewResolvers(context); 17 initFlashMapManager(context); 18 } 这里面的每一个方法不用太细看, 就是给SpringMVC准备初始化的数据, 为后续 SpringMVC处理请求做准备 基本都是从容器中拿到已经配置的Bean(RequestMappingHandlerMapping、 RequestMappingHandlerAdapter、HandlerExceptionResolver )放到 dispatcherServlet中做准备: ... 但是这些Bean又是从哪来的呢?? 来来来, 回到我们的WebAppConfig 我们使用的一个@EnableWebMvc 1. 导入了 DelegatingWebMvcConfiguration@Import(DelegatingWebMvcConfiguratio n.class) 2. DelegatingWebMvcConfiguration的父类就配置了这些Bean3. 而且我告诉你SpringBoot也是用的这种方式, 总结 1. Tomcat在启动时会通过SPI注册 ContextLoaderListener和DispatcherServlet对象 a. 同时创建父子容器 i. 分别创建在ContextLoaderListener初始化时创建父 容器设置配置类 ii. 在DispatcherServlet初始化时创建子容器 即2个 ApplicationContext实例设置配置类 2. Tomcat在启动时执行ContextLoaderListener和DispatcherServlet对象的初始化方 法, 执行容器refresh进行加载 3. 在子容器加载时 创建SpringMVC所需的Bean和预准备的数据:(通过配置类 +@EnableWebMvc配置(DelegatingWebMvcConfiguration)——可实现 WebMvcConfigurer进行定制扩展) a. RequestMappingHandlerMapping,它会处理 @RequestMapping 注解 b. RequestMappingHandlerAdapter,则是处理请求的适配 器,确定调用哪个类的哪个方法,并且构造方法参数,返回 值。 c. HandlerExceptionResolver 错误视图解析器 d. addDefaultHttpMessageConverters 添加默认的消息转 换器(解析json、解析xml) e. 等.... 4. 子容器需要注入父容器的Bean时(比如Controller中需要@Autowired Service的Bean); 会先从子容器中找,没找到会去父容器中找: 详情见 AbstractBeanFactory#doGetBean方法 1 /**2 * 一般情况下,只有Spring 和SpringMvc整合的时才会有父子容器的概念, 3 * 作用: 4 * 比如我们的Controller中注入Service的时候,发现我们依赖的是一个引用对象,那么他就 会调用getBean去把service找出来 5 * 但是当前所在的容器是web子容器,那么就会在这里的 先去父容器找 6 */ 7 BeanFactory parentBeanFactory = getParentBeanFactory(); 8 //若存在父工厂,且当前的bean工厂不存在当前的bean定义,那么bean定义是存在于父beanFaco try中 9 if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { 10 //获取bean的原始名称 11 String nameToLookup = originalBeanName(name); 12 //若为 AbstractBeanFactory 类型,委托父类处理 13 if (parentBeanFactory instanceof AbstractBeanFactory) { 14 return ((AbstractBeanFactory) parentBeanFactory).doGetBean( 15 nameToLookup, requiredType, args, typeCheckOnly); 16 } 17 else if (args != null) { 18 // 委托给构造函数 getBean() 处理 19 return (T) parentBeanFactory.getBean(nameToLookup, args); 20 } 21 else { 22 // 没有 args,委托给标准的 getBean() 处理 23 return parentBeanFactory.getBean(nameToLookup, requiredType); 24 } 25 } 用几道面试题做个总结: Spring和SpringMVC为什么需要父子容器?不要不行吗? 就实现层面来说不用子父容器也可以完成所需功能(参考:SpringBoot就没用子父 容器) 1. 所以父子容器的主要作用应该是早期Spring为了划分框架边界。有点单一 职责的味道。service、dao层我们一般使用spring框架来管理、controller 层交给springmvc管理 2. 规范整体架构 使 父容器service无法访问子容器controller、子容器 controller可以访问父容器 service3. 方便子容器的切换。如果现在我们想把web层从spring mvc替换成struts, 那么只需要将springmvc.xml替换成Struts的配置文件struts.xml即可,而 springcore.xml不需要改变。 4. 为了节省重复bean创建 是否可以把所有Bean都通过Spring容器来管理?( Spring 的applicationContext.xml中配置全局扫描) 不可以,这样会导致我们请求接口的时候产生404。 如果所有的Bean都交给父容器, SpringMVC在初始化HandlerMethods的时候( initHandlerMethods)无法根据 Controller的handler方法注册HandlerMethod,并没有去查找父容器的bean; 也就无法根据请求URI 获取到 HandlerMethod来进行匹配. 是否可以把我们所需的Bean都放入Spring-mvc子 容器里面来管理( springmvc的spring- servlet.xml中配置全局扫描)? 可以 , 因为父容器的体现无非是为了获取子容器不包含的bean, 如果全部包含在子容器完全用 不到父容器了, 所以是可以全部放在springmvc子容器来管理的。 虽然可以这么做不过一般应该是不推荐这么去做的,一般人也不会这么干的。如果你的项目 里有用到事物、或者aop记得也需要把这部分配置需要放到Spring-mvc子容器的配 置文件来,不然一部分内容在子容器和一部分内容在父容器,可能就会导致你的事物 或者AOP不生效。 所以如果aop或事物如果不生效也有可能是通过父容器 (spring)去增强子容器(Springmvc),也就无法增强 这也是很多同学会遇到的问题。 |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 | -2025/1/6 20:35:29- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |