SpringBoot启动原理


前言

SpringBoot 的启动区别于传统的Spring需要搭建tomcat等相关的容器,SpringBoot默认是通过内置Tomcat启动,只需简单的运行java -jar xxx即可简单的启动一个SpringBoot工程。这里我们深入SpringBoot了解它是如何做到通过简简单单的一个jar包就把整个项目启动起来

SpringBoot jar加载流程

SrpingBoot 运行的时候是直接运行的一个jar文件

我们先看下jar文件解压后的目录结构

首先先简单介绍下这个目录结构

spring-boot-learn-0.0.1-SNAPSHOT
├── META-INF
│   └── MANIFEST.MF
├── BOOT-INF
│   ├── classes
│   │   └── 应用程序类
│   └── lib
│       └── 第三方依赖jar
└── org
    └── springframework
        └── boot
            └── loader
                └── springboot启动所需的class

在SpringBoot中会将所有所需的jar包都打包在BOOT-INF下的lib下面,而对于java中是无法加载jar中的jar,所以SpringBoot实现了自定义类加载器,这个类加载器通过org.springframework.boot.loader.JarLauncher创建了LaunchedURLClassLoader用以加载SpringBoot中的jar,在MANIFEST.MF中的Main-Class中有指定JarLauncher

JarLauncher

在一执行java -jar 就会来到JarLauncher中执行main方法

所有应用程序类文件均可通过/BOOT-INF/classes加载,所有依赖的第三方jar均可通过/BOOT-INF/lib加载。

public class JarLauncher extends ExecutableArchiveLauncher {

    static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";

    static final String BOOT_INF_LIB = "BOOT-INF/lib/";

    public JarLauncher() {
    }

    protected JarLauncher(Archive archive) {
        super(archive);
    }

    @Override
    protected boolean isNestedArchive(Archive.Entry entry) {
        if (entry.isDirectory()) {
            return entry.getName().equals(BOOT_INF_CLASSES);
        }
        return entry.getName().startsWith(BOOT_INF_LIB);
    }

    public static void main(String[] args) throws Exception {
        new JarLauncher().launch(args);
    }

}

protected void launch(String[] args) throws Exception {
    JarFile.registerUrlProtocolHandler();
    ClassLoader classLoader = createClassLoader(getClassPathArchives());
    launch(args, getMainClass(), classLoader);
}

protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
    List<URL> urls = new ArrayList<>(archives.size());
    for (Archive archive : archives) {
        urls.add(archive.getUrl());
    }
    return createClassLoader(urls.toArray(new URL[0]));
}

protected ClassLoader createClassLoader(URL[] urls) throws Exception {
    return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

SpringBoot是如何知道要加载哪些类的呢?

这里我们来带Launcher#launch中createClassLoader(getClassPathArchives())方法

@Override
protected List<Archive> getClassPathArchives() throws Exception {
    List<Archive> archives = new ArrayList<>(
        this.archive.getNestedArchives(this::isNestedArchive));
    postProcessClassPathArchives(archives);
    return archives;
}


@Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
    List<Archive> nestedArchives = new ArrayList<>();
    for (Entry entry : this) {
        if (filter.matches(entry)) {
            nestedArchives.add(getNestedArchive(entry));
        }
    }
    return Collections.unmodifiableList(nestedArchives);
}

@Override
protected boolean isNestedArchive(Archive.Entry entry) {
    if (entry.isDirectory()) {
        return entry.getName().equals(BOOT_INF_CLASSES);
    }
    return entry.getName().startsWith(BOOT_INF_LIB);
}

static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";

static final String BOOT_INF_LIB = "BOOT-INF/lib/";

从这段代码就可以看出来它是加载的BOOT-INF/classes/和BOOT-INF/lib/,然后把获取到的archives List传到LaunchedURLClassLoader去加载

总结

  1. SpringBoot通过maven打包成一个fat jar后
  2. 通过fat jar中的JarLauncher生成LaunchedURLClassLoader来加载fat jar中的jar
  3. 通过getMainClass拿到MANIFEST.MF中的Start-Class也就是我们SpringBoot应用启动类,通过launch开启一个新线程来运行

SpringBoot 应用启动流程

这里我们通过SpringBoot启动类开始看他究竟在启动类上做了什么。

在SpringBoot中只有一段代码SpringApplication.run(MyApplication.class),那么这段代码做了哪些事情把Spring容器启动了呢?

我们点进run方法里面去看一路跟到new SpringApplication(primarySources).run(args);通过这一句明白了他就做了new了一个new SpringApplication,然后run了一下就没了

SpringApplication

// new SpringApplication会做的事
// primarySources 传进来的配置类
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
   this.resourceLoader = resourceLoader;
   Assert.notNull(primarySources, "PrimarySources must not be null");
   // 把传进来的配置类存在SpringApplication.primarySources中,后面Spring会解析这个类
   this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
   // 根据classpath下是否存在某些类来判断当前web类型,REACTIVE,SERVLET,SpringBoot版本不一样这里类型会有差别,后面版本会有WebFlux
   this.webApplicationType = WebApplicationType.deduceFromClasspath();
   // 从spring.factories中获取ApplicationContextInitializer这个key
   setInitializers((Collection) getSpringFactoriesInstances(
         ApplicationContextInitializer.class));
   // 从spring.factories中获取ApplicationListener这个key
   setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
   // 从调用堆栈中,通过方法名是否是main推算出mainApplicationClass 
   this.mainApplicationClass = deduceMainApplicationClass();
}

我们跟进getSpringFactoriesInstances#loadSpringFactories

发现这里它把所有的spring.factories中的key都存放在了一个cache中,包括之前的自动配置类,所以后面的自动配置类获取是直接从cache中拿的。

ApplicationContextInitializer

ApplicationListener

run

public ConfigurableApplicationContext run(String... args) {
   // 用来记录当前springboot启动耗时,只是简单的记录下启动时间
   StopWatch stopWatch = new StopWatch();
   stopWatch.start();
   // spring上下文的接口,后面会根据web类型创建一个Spring上下文
   ConfigurableApplicationContext context = null;
   Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
   // 开启HeadLess模式,Headless模式是系统的一种配置模式。在该模式下,系统缺少了显示设备、键盘或鼠标,正常运行
   // 可查看官网解释:https://www.oracle.com/technical-resources/articles/javase/headless.html
   configureHeadlessProperty();
   // 从spring.factories中获取factroies中读取了SpringApplicationRunListener这个key
   SpringApplicationRunListeners listeners = getRunListeners(args);
   // 发布ApplicationStartingEvent事件
   listeners.starting();
   try {
      // 实例化传入参数
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(
            args);
      // 读取环境变量,读取配置文件信息(基于监听器)
      ConfigurableEnvironment environment = prepareEnvironment(listeners,
            applicationArguments);
      // 忽略beaninfo的bean
      configureIgnoreBeanInfo(environment);
      // 打印banner横幅
      Banner printedBanner = printBanner(environment);
      // 根据web环境创建Spring上下文,这里创建的是AnnotationConfigServletWebServerApplicationContext
      context = createApplicationContext();
      exceptionReporters = getSpringFactoriesInstances(
            SpringBootExceptionReporter.class,
            new Class[] { ConfigurableApplicationContext.class }, context);
      //预初始化spring上下文
      prepareContext(context, environment, listeners, applicationArguments,
            printedBanner);
      // 加载spring ioc容器,使用AnnotationConfigServletWebServerApplicationContext启动的spring容器所以springboot对它做了扩展,这里会调用到AbstractApplicationContext#refresh方法,来到了spring最核心的地方创建Bean
      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) {
      handleRunFailure(context, ex, exceptionReporters, listeners);
      throw new IllegalStateException(ex);
   }

   try {
      listeners.running(context);
   }
   catch (Throwable ex) {
      handleRunFailure(context, ex, exceptionReporters, null);
      throw new IllegalStateException(ex);
   }
   return context;
}

prepareEnvironment

初始化环境

private ConfigurableEnvironment prepareEnvironment(
      SpringApplicationRunListeners listeners,
      ApplicationArguments applicationArguments) {
   // 根据web类型创建默认环境,创建就会读取java环境变量和系统环境变量
   ConfigurableEnvironment environment = getOrCreateEnvironment();
   // 将命令行参数放到环境变量
   configureEnvironment(environment, applicationArguments.getSourceArgs());
   // 发布了ApplicationEnvironmentPreparedEvent的监听器,读取了全局配置文件
   listeners.environmentPrepared(environment);
   // 将所有spring.main配置信息绑定SpringApplication
   bindToSpringApplication(environment);
   if (!this.isCustomEnvironment) {
      environment = new EnvironmentConverter(getClassLoader())
            .convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
   }
   ConfigurationPropertySources.attach(environment);
   return environment;
}

prepareContext

初始化上下文

private void prepareContext(ConfigurableApplicationContext context,
      ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
      ApplicationArguments applicationArguments, Banner printedBanner) {
   context.setEnvironment(environment);
   postProcessApplicationContext(context);
   // 拿到之前读取到所有ApplicationContextInitializer的组件调用initialize方法
   applyInitializers(context);
   applyInitializers(context);
    // 发布事件,在我这个版本contextPrepared是空,没有发布任何事件,后面版本会有ApplicationContextInitializedEvent事件
   listeners.contextPrepared(context);
   if (this.logStartupInfo) {
      logStartupInfo(context.getParent() == null);
      logStartupProfileInfo(context);
   }

   // Add boot specific singleton beans
   // 获取当前spring上下文beanFactory (负责创建bean)
   context.getBeanFactory().registerSingleton("springApplicationArguments",
         applicationArguments);
   if (printedBanner != null) {
      context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
   }

   // Load the sources
   Set<Object> sources = getAllSources();
   Assert.notEmpty(sources, "Sources must not be empty");
   // 读取主启动类,将它注册为BeanDefinition
   load(context, sources.toArray(new Object[0]));
   // 发布ApplicationPreparedEvent事件
   listeners.contextLoaded(context);
}

tomcat启动

在run方法中会初始化ApplicationContext上下文AnnotationConfigServletWebServerApplicationContext它继承了AbstractApplicationContext,在这其中AnnotationConfigServletWebServerApplicationContext重写了onRefresh方法,也就是在AbstractApplicationContext#refresh中的onRefresh,所以在new一个ApplicationContext上下文的时候会通过onRefresh创建tomcat,我们通过AnnotationConfigServletWebServerApplicationContext#onRefresh看看究竟做了什么

onRefresh

@Override
protected void onRefresh() {
   super.onRefresh();
   try {
      createWebServer();
   }
   catch (Throwable ex) {
      throw new ApplicationContextException("Unable to start web server", ex);
   }
}

private void createWebServer() {
    WebServer webServer = this.webServer;
    // 获取servletContext,如果有外部容器这里的servletContext就有值
    ServletContext servletContext = getServletContext();
    // 启用内部tomcat
    if (webServer == null && servletContext == null) {
        ServletWebServerFactory factory = getWebServerFactory();
        this.webServer = factory.getWebServer(getSelfInitializer());
    }
    // 启用外部tomcat
    else if (servletContext != null) {
        try {
            getSelfInitializer().onStartup(servletContext);
        }
        catch (ServletException ex) {
            throw new ApplicationContextException("Cannot initialize servlet context",
                                                  ex);
        }
    }
    initPropertySources();
}

getWebServerFactory

getWebServerFactory主要做的事是从bean工厂中获取ServletWebServerFactory类型的bean,

那么ServletWebServerFactory这个类型的类什么时候注入到spring容器中的呢?

我们可以看下ServletWebServerFactoryAutoConfiguration这个自动配置类

@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
      ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
      ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
      ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {

从自动配置类中发现import了一个EmbeddedTomcat类,而在这个类中又注入了bean TomcatServletWebServerFactory

@Configuration
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedTomcat {

   @Bean
   public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
      return new TomcatServletWebServerFactory();
   }

}

getWebServer

getWebServer主要做的事是创建内嵌tomcat

private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
   return this::selfInitialize;
}

private void selfInitialize(ServletContext servletContext) throws ServletException {
   prepareWebApplicationContext(servletContext);
   registerApplicationScope(servletContext);
   WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(),
         servletContext);
   // 获取所有servlet组件然后回调onStart方法,将serclet注册进tomcat容器中
   for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
      beans.onStartup(servletContext);
   }
}

factory.getWebServer(getSelfInitializer())这个方法中传入了getSelfInitializer,但并不会调用,会最后回调,回调完之后就创建了servlet组件,所以我们只需要关注它是如何回调的

这里是将传入的参一步步注入到TomcatStart中去然后将TomcatStart注入到TomcatEmbeddedContext中去。最后通过tomcat.start方法运行

大致调用栈是

TomcatServletWebServerFactory#getWebServer
    -- TomcatServletWebServerFactory#prepareContext
        -- TomcatServletWebServerFactory#configureContext
            -- context.addServletContainerInitializer(starter, NO_CLASSES);
    -- TomcatServletWebServerFactory#getTomcatWebServer
        -- TomcatWebServer#initialize
            -- this.tomcat.start();

文章作者: dm
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 dm !
评论
 上一篇
SpringBoot使用外部tomcat SpringBoot使用外部tomcat
前言大家都知道SpringBoot有内置Tomcat的功能,在启动原理中有介绍SpringBoot内置Tomcat的原理。那么如果我们不像使用SpringBoot的内置Tomcat,想用自己的web容器怎么操作。springboot支持默认
2022-04-27
下一篇 
Spring声明式事务源码剖析 Spring声明式事务源码剖析
事务包含范围较广不仅仅包括数据库事务也包括事务消息。事务是使有限操作满足ACID属性,A:原子性,C:一致性,I:隔离性,D:持久性,严格遵循ACID规则的叫做刚性事务,事务执行的中间状态可以暂时不支持ACID的叫柔性事务。 关于MySQL
2022-03-27
  目录