本文共 16227 字,大约阅读时间需要 54 分钟。
Spring Boot是一个容纳百川的启动框架,在这套框架体系中,可以非常方便地使用各种基于Spring 实现的功能,这其中也包括 Spring MVC。Spring MVC在Spring Boot框架的支持下,大大地简化了Web开发的过程。
简化开发者们使用SpringMVC并不是一件简单的事,Spring Boot做了很多额外的工作才实现了这个目的。把SpringMVC嵌入到整个SpringBoot框架中,经历了一系列的过程。本章从Spring Boot 应用的启动方法开始,以启动方法为入口进行调试来研究源码的方式,一步步地研究基于Tomcat的内嵌Servlet容器的创建与启动过程,最终分析Spring MVC的核心组件DispatcherServlet的注册过程。通过本章可以学到以下知识点。
SpringBoot应用的启动过程。
基于SpringBoot的自动配置生效原理。
内嵌ServletWebServer如何通过自动配置创建和启动。
DispatcherServlet 如何通过自动配置注册到ServletWebServer中。
SpringBoot应用的启动过程。就是我们今天要讲的内容,先下一个结论:SpringBoot应用的启动过程可以分成两个步骤执行:创建 SpringApplication和运行 SpringApplication,接下来的章节就是划分成这两个步骤来讲解。
作者在这里,想先给出源码的调试过程,继而再进行源码解读,如果直接上源码解读,可能会给大家带来迷惑,这些方法都是哪来的????
如下是作者做的一个毕业设计管理项目的主程序类,
@SpringBootApplicationpublic class SpringBootGraduationDesignApplication{ public static void main(String[] args) { 4 SpringApplication.run(SpringBootGraduationDesignApplication.class, args); }}
在第4行打上断点,并且以debug模式启动,创建 SpringApplication过程的源码调试步骤流程如下
上面我们已经介绍了SpringApplication创建的源码调试,接下来,我们就讲解出现在调试过程中的一些方法。
SpringApplication类/**静态方法启动springAplication@param source创建上下文使用的资源@param args 启动时的命令行参数@return 返回创建的应用上下文ConfigurableApplicationContext,也就是最常用的Spring的Appl icatinContext*/ public static ConfigurableApplicationContext run(Class primarySource, String... args) { //调用下面的重载方法,只传入一个source return run(new Class [] { primarySource }, args); }
SpringApplication类
/**静态方法启动springAplication@param source创建上下文使用的资源@param args 启动时的命令行参数@return 返回创建的应用上下文ConfigurableApplicationContext,也就是最常用的Spring的Appl icatinContext*/ public static ConfigurableApplicationContext run(Class [] primarySources, String[] args) { //创建SpringApplication实例,执行run方法之后返回ApplicationContext return new SpringApplication(primarySources).run(args); }
本节重点讲解SpringApplication的创建,看一下创建的逻辑,代码如下
SpringApplication类/**@param sources创建上下文使用的资源数组*/ public SpringApplication(Class ... primarySources) { //构造时直接进行初始化 this(null, primarySources); }
SpringApplication类
/**初始化方法,初始化一些属性@param sources 创建上下文使用资源数组*/ public SpringApplication(ResourceLoader resourceLoader, Class ... primarySources) { //把启动类添加到sources列表中 this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null"); this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); //推断是否是Web环境 this.webApplicationType = WebApplicationType.deduceFromClasspath();// 从spring.factories文件工厂中获取ApplicationContextInitializer并设置到当前实例中 setInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer.class));//从spring.factories文件工厂中获取ApplicationListener并设置到当前实例中 setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));//推断启动类 this.mainApplicationClass = deduceMainApplicationClass(); }
如上SpringApplication的构造方法就是创建SpringApplication的过程,总结成如下步骤依次说明
SpringApplication类中的deduceFromClasspath方法
//基于Netty的WebFlux框架private static final String WEBFLUX_INDICATOR_CLASS = "org." + "springframework.web.reactive.DispatcherHandler";//基于Servlet的web框架,多数用于Tomcat的Servlet容器private static final String WEBMVC_INDICATOR_CLASS = "org.springframework." + "web.servlet.DispatcherServlet"; /**推断是否是Web环境@return 推断结果:None,Servlet,Reactive*/ static WebApplicationType deduceFromClasspath() { //如果只存在Reactive相关类,则判断为Reactive if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null) && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) { return WebApplicationType.REACTIVE; } //如果不存在任意一 个Web相关环境的依赖,则判断为非Web环境 for (String className : SERVLET_INDICATOR_CLASSES) { if (!ClassUtils.isPresent(className, null)) { return WebApplicationType.NONE; } } //否则都是Servlet环境 return WebApplicationType.SERVLET; }
检测特定类型的Web环境相关的类是否存在,接着两行代码逻辑基本相同,从spring.fatories工厂中分别获取Initializer组件类列表和Listener 组件类列表,并对它们进行实例化之后设置到当前应用中,代码如下:
/**从工厂获取指定类型的所有类实例@param type要获取的类型@return 实例化好的所有类*/ privateCollection getSpringFactoriesInstances(Class type) { //从spring.factories中获取所有type类型的实例对象 return getSpringFactoriesInstances(type, new Class [] { }); }
/**从工厂获取指定类型的所有类实例@param type 要获取的类型@param parameterTypes 实例化时使用的构造器的参数类型数组@param args 调用构造器实例化时的构造器参数@return 实例化好的所有类*/privateCollection getSpringFactoriesInstances(Class type, Class [] parameterTypes, Object... args) { // 获取当前上下文类加载器,用于Classpath下的加载资源文件 ClassLoader classLoader = getClassLoader();//调用SpringFactoriesLoader.loadFactoryNames获取指定类的所有类型名,使用Set保证没有重复类型名 Set names = new LinkedHashSet<>( SpringFactoriesLoader.loadFactoryNames(type, classLoader));//遍历names 通过反射获取拥有参数类型为parameterTypes的构造器,反射调用进行实例化 List instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names); AnnotationAwareOrderComparator.sort(instances); return instances; }
获取工厂中指定类对应所有类名的功能,是通过Spring工厂加载器SpringFactoriesLoader的loadFactoryNames方法实现。该方法根据类型Class和类加载器ClassLoader获取工厂中所有该Class类对应的类名列表,其中ClassLoader参数用于获取classpath 下的资源。
SpringFactoriesLoader.loadFactoryNames(type, classLoader));
在loadFactoryNames的逻辑中,会通过ClassLoader 查找classpath 下所有jar包中META-INF路径下的spring.factories文件。而spring. factories文件是properties类型的属性文件,本质上是key=value 类型的配置文件。spring 通过Properties 加载资源文件并生成Properties类型的实例,Properties 最终是key:value类型的map,在spring. factories中,key是类的全名,即通过Class getName()获取到的值。
而value则是用逗号分隔的所有工厂类名的拼接,这个方法就是通过传入的Class参数的全名作为key,获取到spring. factories中定义的所有value即所有的工厂类。
通过断点调试我们可以看到这个Initializer和Listener 组件列表
上面获取的组件列表,都是从spring boot包下面的META-INF/spring.factories获取的 spring.factories# Application Context Initializersorg.springframework.context.ApplicationContextInitializer=\org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\org.springframework.boot.context.ContextIdApplicationContextInitializer,\org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer# Application Listenersorg.springframework.context.ApplicationListener=\org.springframework.boot.ClearCachesApplicationListener,\org.springframework.boot.builder.ParentContextCloserApplicationListener,\org.springframework.boot.context.FileEncodingApplicationListener,\org.springframework.boot.context.config.AnsiOutputApplicationListener,\org.springframework.boot.context.config.ConfigFileApplicationListener,\org.springframework.boot.context.config.DelegatingApplicationListener,\org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\org.springframework.boot.context.logging.LoggingApplicationListener,\org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
在获取到所有工厂类类名并通过反射实例化之后,把上面这些类型的实例设置到当前SpringAplication中。
main这个方法很有意思,在Java已有的API中,并没有直接获取main方法,也就是应用启动入口所在的类的API,那么这个方法是如何做推断的呢???
/**推动启动入口所在的类*/private Class deduceMainApplicationClass() { try { // 通过创建一 个异常,获取当前方法执行的调用栈信息 StackTraceElement[] stackTrace = new RuntimeException().getStackTrace(); for (StackTraceElement stackTraceElement : stackTrace) { // 从底到顶遍历栈,找到方法名是main的栈,尝试推断该栈中的Class就是启动类 if ("main".equals(stackTraceElement.getMethodName())) { return Class.forName(stackTraceElement.getClassName()); } } } catch (ClassNotFoundException ex) { // Swallow and continue } return null; }
创建一个异常,再从异常栈中获取信息。
如下是调试过程中,变量窗口获取到的值至此SpringApplication的创建过程分析完,接下来进入启动阶段。
SpringApplication类
public static ConfigurableApplicationContext run(Class [] primarySources, String[] args) { 3 return new SpringApplication(primarySources).run(args); }
由第3行代码可知,在SpringApplication创建完成后,可以调用run方法启动应用,传入String[]类型的参数用的启动参数。在run方法中,有准备环境、准备上下文、刷新上下文、启动完成等这些步骤构成了整个应用的启动逻辑。下面详细分析run方法,代码如下:
public ConfigurableApplicationContext run(String... args) { //记录应用的启动时间 StopWatch stopWatch = new StopWatch();//启动停表 stopWatch.start(); ConfigurableApplicationContext context = null;//可配置应用上下文 CollectionexceptionReporters = new ArrayList<>(); configureHeadlessProperty();//SpringApplicationRunListener的容器,用于监听SpringApplication的各个启动事件 SpringApplicationRunListeners listeners = getRunListeners(args);//触发启动中的事件 listeners.starting(); try { //通过启动参数生成应用参数 ApplicationArguments applicationArguments = new DefaultApplicationArguments( args);//准备可配置的环境,在ApplicationContext中使用此环境 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); Banner printedBanner = printBanner(environment);//创建ioc容器 创建ApplicationContext ( 应用上下文的创建) context = createApplicationContext(); exceptionReporters = getSpringFactoriesInstances( SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context);// 准备ApplicationContext IOC容器的基本信息 ( 应用上下文的准备) prepareContext(context, environment, listeners, applicationArguments, printedBanner);//刷新IOC容器 (应用上下文的刷新 ) refreshContext(context); afterRefresh(context, applicationArguments);//启动停表 stopWatch.stop(); if (this.logStartupInfo) { //打印启动日志 new StartupInfoLogger(this.mainApplicationClass) .logStarted(getApplicationLog(), stopWatch); }//调用启动完成的事件回调 listeners.started(context); callRunners(context, applicationArguments); } catch (Throwable ex) { //异常 } try { //正常启动,调用运行中事件回调 listeners.running(context); } catch (Throwable ex) { //异常 } return context; }
如上方法有几个关键点,分别是prepareEnvironment(应用环境的创建与准备) 、createApplicationContext(应用上下文的创建)、prepareContext (应用上下文的准备)、refreshContext (应用上下文的刷新),接下来依次分析整个过程。
代码如下
/**准备应用环境@param listeners Spring应用的启动监听器,用于处理些事件回调@param applicationArguments启动时传入的启动参数的封装,一般以 “--参数名一参数值”方式配置参数@return 返回可配置的应用环境*/private ConfigurableEnvironment prepareEnvironment( SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) { //根据情况获取或者创建环境。SpringApplication构造时可以主动指定环境。//如果是自动获取的环境,则会根据当前Web类型返回不同的环境//Servlet 环境返回StandardServletEnvironment//其他情况返回StandardEnvironment ConfigurableEnvironment environment = getOrCreateEnvironment();//配置应用环境,添加些默认的PropertySource 属性源 configureEnvironment(environment, applicationArguments.getSourceArgs());//调用监听器的环境已准备事件 listeners.environmentPrepared(environment);// 环境初始化完毕,绑定环境中一些属性到当前SpringApplicationt bindToSpringApplication(environment);//如果当前不是Web环境,则把环境做转换 if (!this.isCustomEnvironment) { environment = new EnvironmentConverter(getClassLoader()) .convertEnvironmentIfNecessary(environment, deduceEnvironmentClass()); } ConfigurationPropertySources.attach(environment); return environment; }
在这一步中, 会加载以下数据源。
通过 System.getProperties()获取 Jvm相关变量,通过Java 启动参数-Dkey=value指定。
通过System.getenv()获取的当前系统的环境变量,Linux 通过export key=value指定,Windows 通过set key=value指定。
通过命令行参数获取,使用应用启动参数-key=value来配置。
通过 resources 或者resources/config 目录下的配置 文件application.yml 或者application.properties指定。
其中我们所熟知的Spring Boot的application.yml或者application.properties配置文件的属性加载,就和listeners.environmentPrepared事件相关,ConfigFileApplicationListener 的onApplicationEnvironmentPreparedEvent方法的执行逻辑中,会对resources目录下的文件进行加载并绑定到环境中。
环境创建和准备完毕,就开始创建应用上下文。
创建上下文的逻辑在createApplicationContext逻辑里面,我们来看一下具体应用上下文的创建,代码如下
public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context." + "annotation.AnnotationConfigApplicationContext";/**根据环境创建对应类型的ApplicationContext@return 返回创建的应用上下文ConfigurableApplicationContext , 也就是最常用的Spring的ApplicationContext*/protected ConfigurableApplicationContext createApplicationContext() { Class contextClass = this.applicationContextClass;//如果当前配置中的applicationContextClass为空则根据环境自动获取,否则直接以配置为准 if (contextClass == null) { try { switch (this.webApplicationType) { //根据是web环境类型,返回不同的ApplicationContext类型 case SERVLET: contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS); break; case REACTIVE: contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS); break; default://默认是AnnotationConfigApplicationContext contextClass = Class.forName(DEFAULT_CONTEXT_CLASS); } } catch (ClassNotFoundException ex) { // } }//实例化应用上下文类 return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass); }
通过断点调试我们可以知道,在web环境下,使用的是org.springframework.context.annotation.AnnotationConfigApplicationContext上下文。下面我们对上下文的准备进行阐述
具体代码如下
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { //保存环境信息 context.setEnvironment(environment);//IOC容器的后置处理流程。 postProcessApplicationContext(context);//应用初始化器 applyInitializers(context);//所有的监听器 调用 contextLoaded listeners.contextPrepared(context); if (this.logStartupInfo) { logStartupInfo(context.getParent() == null); logStartupProfileInfo(context); } // Add boot specific singleton beans ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); beanFactory.registerSingleton("springApplicationArguments", applicationArguments); if (printedBanner != null) { beanFactory.registerSingleton("springBootBanner", printedBanner); } if (beanFactory instanceof DefaultListableBeanFactory) { ((DefaultListableBeanFactory) beanFactory) .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding); }//加载并获取到当前应用的配置资源 Set
通过上面的过程,得知使用应用上下文是AnnotationConfigServletWebServerApplicationContext类型。在run方法的refreshContext逻辑中,会调用生成ConfigApplicationContext上下文的refresh方法对创建的上下文进行刷新,刷新后,整个Spring应用启动起来
对于AnnotationConfigServletWebServerApplicationContext类型的ConfigurableApplication-Context,它的refresh 方法是在父类ServletWebServerApplicationContext 中的重写,通过断点调试来验证此结果
如上调用父类,也就是AbstractApplicationContext的refresh方法 ServletWebServerApplicationContext 类中的refresh方法。如下才是真正的刷新上下文的执行方法/**刷新上下文*/ @Override public final void refresh() throws BeansException, IllegalStateException { try { //调用父类,也就是AbstractApplicationContext的refresh方法 //父类的refresh是个模板方法,其中执行了很多抽象方法,子类根据情况重写抽象方法,达到模板化refresh整个应用上下文 super.refresh(); } catch (RuntimeException ex) { stopAndReleaseWebServer(); throw ex; } }
AbstractApplicationContext
上面说了这么多,大家是不是都没感觉到这跟Spring MVC框架的启动原理,连DispatcherServlet的创建和注册都没看见呀!!!!!上面讲的这些好比吃饭时,服务员肯定要先上小菜才可以上大餐啊!!! SpringBoot应用的启动过程中有一个刷新ioc容器方法(refresh),该方法在ServletWebServerApplicationContext 中实现。而该类就跟ServletWebServer有关,而DispatcherServlet的创建入口又和,WebServer启动过程中执行的逻辑有关。只有我们先了解了SpringBoot应用的启动过程,一步一步脚印走,才可以对DispatcherServlet的创建与注册拨云见雾,就好像我们找DispathcherServlet的请求入口时,我如果直接就告诉你直接找doDispath方法是不是会很懵逼啊!
从2.4节,我们可以知道ServletWebServerApplicationContext 类中的refresh方法中有一个 stopAndReleaseWebServer();方法
/**刷新上下文*/ @Override public final void refresh() throws BeansException, IllegalStateException { try { //调用父类,也就是AbstractApplicationContext的refresh方法 //父类的refresh是个模板方法,其中执行了很多抽象方法,子类根据情况重写抽象方法,达到模板化refresh整个应用上下文 super.refresh(); } catch (RuntimeException ex) { stopAndReleaseWebServer(); throw ex; } }
stopAndReleaseWebServer方法作用:如果有异常,则停止内嵌的ServletWebServer。该方法的代码逻辑如下
/**停止内嵌Servlet容器*/ private void stopAndReleaseWebServer() { WebServer webServer = this.webServer; if (webServer != null) { try { webServer.stop(); this.webServer = null; } catch (Exception ex) { throw new IllegalStateException(ex); } } }
这里有停止内嵌ServletWebServer的方法,内嵌ServletWebServer是从this.webServer获取。根据这些信息可以推断,内嵌Servlet容器是在ServletWebServerApplicationContext中维护,那么这个容器的来源是什么呢?这就是我们以后要探讨的问题:ServletWebServer的注册,创建与启动过程。了解了这个以后,我们就可以找到DispathcerServlet的创建入口,然后了解它的创建和注册。SpringBoot在帮我们完成上面的全部操作的时候,Web容器在接收到HTTP请求时,经过预处理后才会把该请求交给DispatcherServlet处理。
转载地址:http://orozi.baihongyu.com/