SpringBoot 启动流程源码分析

说明:本章在之前章节《SpringBoot 重构Jar包源码分析》的基础上进行继续源码分析。

 

在前面《SpringBoot 重构Jar包源码分析》章节中,我们通过springboot脚手架创建了一个只包含Web起步依赖Demo工程 sourcecode-analysis-springboot,同时我们也分析了springboot重构maven-jar-plugin插件打出来的jar包的核心源码。在重构的过程中往jar包的META-INF/MANIFEST.MF文件中写入了Main-Class配置值为:org.springframework.boot.loader.JarLauncher,我们知道java -jar命名类加载完成后运行的便是META-INF/MANIFEST.MF文件中Main-Class(这是java基础知识),所以我们要分析springboot的启动流程,就应该从org.springframework.boot.loader.JarLauncher类的main方法开始。为了巩固大家记忆,下面把前面章节中写入JarLauncher类的源码再复制出来一遍:

 

 

首先将JarLauncher类所在的spring-boot-loader模块打成 jar包,遍历jar包方式将class类文件写入,下面也给出源码spring-boot-loader模块项目源码截图:

 

可以看到org包下面所有目录和文件对应的正是我们Demo工程打出来jar中的org目录下面的内容。

 

下面我们直接找到JarLauncher主方法,源码如下:

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

这里首先是new 自己实例化一个对象,然后调用launch方法,继续跟入源码之前我们先思考一下,如果让你来写这块代码你首先应该写那块逻辑,整个过程又应该包含哪些逻辑?

笔者这里根据反推的方式简单列一下至少包含的逻辑如下:

1、参照IDEA运行来说,通过jar运行时至少要通过代码例如反射的方式去调用我们手工点Run的那个Application的main方法,例如我们创建的Demo项目中的SourcecodeAnalysisSpringbootApplication.java的main方法。

2、从第一步反推出要调用SourcecodeAnalysisSpringbootApplication的main方法就必须从jar包中加载SourcecodeAnalysisSpringbootApplication.class类文件,例如通过Class.forName的方式。

3、从第二步反推出要加载SourcecodeAnalysisSpringbootApplication.class类文件必须要创建一个classloader,把项目所有lib依赖和class目录的URL对象添加到这个classloader的classpath下,并且将该classloader设置到线程上下文,让后面所有通过线程上下文获取的类加载器都使用它。这样才拥有正确的运行时环境。

 

我们带着我们的猜测继续跟入launch方法的源码如下:

protected void launch(String[] args) throws Exception {
   //这里固定返回true
   if (supportsNestedJars()) {
      //通过设置属性java.protocol.handler.pkgs
      //注册自定义URL协议处理器包路径(多个以|分隔)
      //注册的包为:org.springframework.boot.loader
      //根据URL查找规则:是否存在继承了URLStreamHandler的<package>.<protocol>.Handler类
      //找到的URL协议处理器就是org.springframework.boot.loader.jar.Handler
      //后面解析jar协议URL都是通过org.springframework.boot.loader.jar.Handler
      JarFile.registerUrlProtocolHandler();
   }


   //创建classloader
   //getClassPathArchivesIterator()逻辑是拿到springboot重构后jar包
   //的BOOT-INF/classes目录以及BOOT-INF/lib目录下所有的jar文件
   //作为迭代器返回,传入createClassLoader方法
   ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());


   //继续调用重载的launch方法
   //getMainClass()方法逻辑是获取META-INF/MANIFEST.MF文件中
   //Start-Class的值
   launch(args, getMainClass(), classLoader);
}


protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {


   //将classloader设置到线程上下文
   //关于线程上下文类加载器可以自行查阅资料,它当初设计的作用是解决
   //java SPI设计无法通过双亲委派模型类加载问题
   Thread.currentThread().setContextClassLoader(classLoader);


   //创建main方法包装对象MainMethodRunner,并调用它的run方法
   //return new MainMethodRunner(mainClass, args);
   createMainMethodRunner(mainClass, args, classLoader).run();
}


public class MainMethodRunner {


   private final String mainClassName;


   private final String[] args;


   /**
    * Create a new {@link MainMethodRunner} instance.
    * @param mainClass the main class
    * @param args incoming arguments
    */
   public MainMethodRunner(String mainClass, String[] args) {
      this.mainClassName = mainClass;
      this.args = (args != null) ? args.clone() : null;
   }


   public void run() throws Exception {


      //终于看到关键源码,这里直接通过创建的类加载器loadClass我们的Application类文件
      //注意这里并不是像我们思考那样使用Class.forName,不过原理差不多
      //这个类文件是通过前面springboot重构jar包是写入到
      //META-INF/MANIFEST.MF文件中Start-Class的值,然后获取出来的
      Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);


      //通过方法签名信息获取main方法,例如Demo项目中的
      //SourcecodeAnalysisSpringbootApplication.java的main方法
      Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);


      //反射调用
      mainMethod.invoke(null, new Object[] { this.args });
   }


}

源码核心逻辑如下:

1、注册自定义实现的URL协议处理器。

2、获取class目录和lib依赖jar文件的URL作为classpath参数创建classloader。

3、将classloader保存到线程上下文。

4、通过创建的classloader加载Application类文件,反射调用main方法。

可以看到实际源码跟我们上面反推思路基本匹配。

 

下面对设计的调用进行深度源码分析:

1、注册自定义URL协议处理源码分析:

private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";


public static void registerUrlProtocolHandler() {
   String handlers = System.getProperty(PROTOCOL_HANDLER, "");
   System.setProperty(PROTOCOL_HANDLER,
         ("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
   resetCachedUrlHandlers();
}

这段源码比较简单,没有什么好说的。

2、下面是获取classpath的URL包装对象迭代器源码分析。为了方便阅读,这里我把关联调用的源码从其他类复制过来,放到一块来分析:

@Override
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {


   //获取BOOT-INF/classes/目录和BOOT-INF/lib/目录的每个jar文件,封装为自定义的NestedArchiveIterator迭代器中
   //this.archive是通过构造器实例化的,源码在后面
   Iterator<Archive> archives = this.archive.getNestedArchives(this::isSearchCandidate, this::isNestedArchive);


   //后置处理器,加载完lib文件后可以自定义扩展做点事情
   if (isPostProcessingClassPathArchives()) {


      //这里调用自定义的后置处理器,类似spring的BeanPostProcessor
      //如果你了解spring源码,你会发现spring中的PostProcessor设计非常重要,AOP就是靠它织入的
      //默认空实现方法,没有逻辑
      archives = applyClassPathArchivePostProcessing(archives);
   }
   return archives;
}


private final Archive archive;


public ExecutableArchiveLauncher() {
   try {
      this.archive = createArchive();
   }
   catch (Exception ex) {
      throw new IllegalStateException(ex);
   }
}


protected final Archive createArchive() throws Exception {
   ProtectionDomain protectionDomain = getClass().getProtectionDomain();
   CodeSource codeSource = protectionDomain.getCodeSource();
   URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
   //最后就是得到jar包的path
   String path = (location != null) ? location.getSchemeSpecificPart() : null;
   if (path == null) {
      throw new IllegalStateException("Unable to determine code source archive");
   }
   File root = new File(path);
   if (!root.exists()) {
      throw new IllegalStateException("Unable to determine code source archive from " + root);
   }
   //这里实例化的是JarFileArchive
   return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}


//这个构造器是JarFileArchive类的,为了方便查看我把它复制到这里
public JarFileArchive(File file) throws IOException {
   this(file, file.toURI().toURL());
}


//这个构造器是JarFileArchive类的,为了方便查看我把它复制到这里
public JarFileArchive(File file, URL url) throws IOException {
   //这里的JarFile是springboot自己包装的,可以支持迭代器功能
   this(new JarFile(file));
   this.url = url;
}


//这个构造器是JarFileArchive类的,为了方便查看我把它复制到这里
public JarFileArchive(JarFile jarFile) {
   this.jarFile = jarFile;
}




//这个方法是JarFileArchive类的,为了方便查看我把它复制到这里
//其中this.jarFile在实例化archive时创建,实际是springboot重构的jar文件对象
//NestedArchiveIterator是springboot自己实现的迭代器,继承Iterator
//作用是在迭代过程调用传入的两个过滤匹配器
@Override
public Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException {
   return new NestedArchiveIterator(this.jarFile.iterator(), searchFilter, includeFilter);
}


//过滤器1
//这个方法是当前类子类JarLauncher重写的,为了方便查看我把它复制到这里
@Override
protected boolean isSearchCandidate(Archive.Entry entry) {
   //必须以BOOT-INF/开头
   return entry.getName().startsWith("BOOT-INF/");
}


//过滤器2
//这个方法是当前类子类JarLauncher重写的,为了方便查看我把它复制到这里
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
   return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
}


static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
   //这里要普及点classpath的知识:
   //加入到classpath时如果是class类文件可以到父目录,但jar包必须是到具体的文件
   if (entry.isDirectory()) {


      //所以classes是全匹配,只到目录
      return entry.getName().equals("BOOT-INF/classes/");
   }
   //但lib下面的jar是具体jar,而是以开头去匹配的
   return entry.getName().startsWith("BOOT-INF/lib/");
};

 

这个方法看上去代码量挺多的,其实核心就干了一件事情,就是将jar包中的BOOT-INF/classes目录和BOOT-INF/lib下面的每一个jar文件封装为springboot自己定义的迭代器返回。迭代器源码如下:

private class NestedArchiveIterator extends AbstractIterator<Archive> {


   NestedArchiveIterator(Iterator<JarEntry> iterator, EntryFilter searchFilter, EntryFilter includeFilter) {
      /**
       *
       super的源码:其中poll
       this.iterator = iterator;
       this.searchFilter = searchFilter;
       this.includeFilter = includeFilter;
       this.current = poll();


       */
      super(iterator, searchFilter, includeFilter);


   }


   @Override
   protected Archive adapt(Entry entry) {
      try {
         return getNestedArchive(entry);
      }
      catch (IOException ex) {
         throw new IllegalStateException(ex);
      }
   }


}


//父类
private abstract static class AbstractIterator<T> implements Iterator<T> {


   private final Iterator<JarEntry> iterator;


   private final EntryFilter searchFilter;


   private final EntryFilter includeFilter;


   private Entry current;


   AbstractIterator(Iterator<JarEntry> iterator, EntryFilter searchFilter, EntryFilter includeFilter) {
      //springboot 构建的jar包迭代器
      this.iterator = iterator;
      this.searchFilter = searchFilter;
      this.includeFilter = includeFilter;
      this.current = poll();
   }


   @Override
   public boolean hasNext() {
      return this.current != null;
   }


   @Override
   public T next() {
      T result = adapt(this.current);
      this.current = poll();
      return result;
   }


   //每调用next就会调这里,主要过滤匹配器
   //一旦发现下一个不匹配就返回Null,迭代器就会退出
   //所以jar包里面目录的顺序是很重要的,不信你想办法调整下顺序试试
   private Entry poll() {
      while (this.iterator.hasNext()) {
         JarFileEntry candidate = new JarFileEntry(this.iterator.next());
         //这里执行外面传入的两个过滤器
         if ((this.searchFilter == null || this.searchFilter.matches(candidate))
               && (this.includeFilter == null || this.includeFilter.matches(candidate))) {
            return candidate;
         }
      }
      return null;
   }


   //子类实现,主要通过创建临时目录处理jar in jar
   //里面读写文件逻辑挺多,咱们不打算维护springboot,不必每一行代码都分析一遍
   //分析源码主要是了解它的思想和设计即可,等遇到问题在具体调试
   protected abstract T adapt(Entry entry);


}

 

3、下面是创建classloader的源码分析:

protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
   List<URL> urls = new ArrayList<>(50);
   while (archives.hasNext()) {
      urls.add(archives.next().getUrl());
   }
   return createClassLoader(urls.toArray(new URL[0]));
}




protected ClassLoader createClassLoader(URL[] urls) throws Exception {


   //这里固定返回true
   if (supportsNestedJars()) {


      //最后实例化的classloader便是LaunchedURLClassLoader
      return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
   }
   return new URLClassLoader(urls, getClass().getClassLoader());
}

这里我们要关注具体创建的classloader类是LaunchedURLClassLoader。

 

4、获取Application主类源码分析如下:

protected String getMainClass() throws Exception {
   Manifest manifest = this.archive.getManifest();
   String mainClass = null;
   if (manifest != null) {
      mainClass = manifest.getMainAttributes().getValue("Start-Class");
   }
   if (mainClass == null) {
      throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
   }
   return mainClass;
}

到这里,我们基本分析完通过java -jar命令是如何调用到我们应用的主方法,也分析了springboot是如何构建运行时环境的。这里要注意一点就是如果你是通过IDEA断点调试的话看到的classloader是jdk默认的应用程序类加载器Launcher$AppClassLoader,只有通过java -jar运行jar包时的classloader才是我们本次分析的classloader。

如果要了解springboot整个启动过程,这还不够,我们需要继续分析Application的主方法到底干了些什么。回到我们前面创建的Demo项目,找到SourcecodeAnalysisSpringbootApplication类main方法以及关联调用源码如下:

@SpringBootApplication
public class SourcecodeAnalysisSpringbootApplication {


    public static void main(String[] args) {
        SpringApplication.run(SourcecodeAnalysisSpringbootApplication.class, args);
    }


}




public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
   return new SpringApplication(primarySources).run(args);
}




//SpringApplication构造方法
public SpringApplication(Class<?>... primarySources) {
   this(null, primarySources);
}


//SpringApplication重载构造方法
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
   this.resourceLoader = resourceLoader;
   Assert.notNull(primarySources, "PrimarySources must not be null");
   //应用主类Class对象,实际上就是SourcecodeAnalysisSpringbootApplication类
   this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
   //判断如果classpath下存在javax.servlet.Servlet和
   //org.springframework.web.context.ConfigurableWebApplicationContext
   //则为servlet类型,其它类型我们暂不关心
   this.webApplicationType = WebApplicationType.deduceFromClasspath();
   //从classpath下找到所有META-INF/spring.factories文件中
   //所有实现了ApplicationContextInitializer接口的子类配置,进行实例化
   //赋值给List<ApplicationContextInitializer<?>> initializers属性
   //springboot starter中的META-INF/spring.factories配置文件就是在这一步就被读取并缓存在内存中的
   //所以后面所有读取META-INF/spring.factories信息的都是从缓存Map中获取的
   //Map的key就是接口名称,val为子类名称类全路径名称
   setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
   //这里逻辑同上,实例化实现ApplicationListener接口的所有子类
   setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
   //这一步的逻辑是遍历当前方法调用栈,找到包含名称为main方法的类并推断它是主类
   //这里被推断SourcecodeAnalysisSpringbootApplication为主类,因为只有它有方法名称为main
   this.mainApplicationClass = deduceMainApplicationClass();
}

进入实例化SpringApplication类的run方法,源码如下:

public ConfigurableApplicationContext run(String... args) {


   //创建程序计时器
   StopWatch stopWatch = new StopWatch();
   //启动程序计时器
   stopWatch.start();
   ConfigurableApplicationContext context = null;
   Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
   //设置java.awt.headless系统属性值
   configureHeadlessProperty();
   //从缓存的META-INF/spring.factories Map中获取
   //SpringApplicationRunListeners接口的所有子类
   SpringApplicationRunListeners listeners = getRunListeners(args);
   //回调通知应用开始启动事件监听器
   listeners.starting();
   try {


      //封装main方法参数
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);


      //根据前面判断的webApplicationType创建环境
      //配置main方法参数到环境信息中,如果有的话
      //回调通知环境创建事件监听器
      //绑定spring.main开头的配置到SpringApplication
      //根据配置判断是否需要转换环境对象
      ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);


      //配置spring.beaninfo.ignore系统属性
      configureIgnoreBeanInfo(environment);


      //打印banner.txt
      Banner printedBanner = printBanner(environment);


      //根据前面webApplicationType值实例化ApplicationContext
      //servlet类型实例化的是AnnotationConfigServletWebServerApplicationContext
      context = createApplicationContext();


      //从缓存的META-INF/spring.factories Map中获取
      //SpringBootExceptionReporter接口的所有子类
      exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
            new Class[]{ConfigurableApplicationContext.class}, context);


      //准备Spring上下文信息,这里包含了Spring 核心流程
      prepareContext(context, environment, listeners, applicationArguments, printedBanner);


      //刷新Spring上下文,这里包含了Spring 核心流程
      refreshContext(context);


      //这里啥都没做,空方法
      afterRefresh(context, applicationArguments);


      //停止程序计时器
      stopWatch.stop();




      if (this.logStartupInfo) {
         //打印启动成功信息日志,这里将程序计时器也一并打印处理
         //Started SourcecodeAnalysisSpringbootApplication in 4066.346 seconds
         new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
      }


      //回调通知应用启动完成事件监听器
      listeners.started(context);


      //回调通知spring bean容器中所有实现ApplicationRunner和CommandLineRunner接口的run方法
      //调用之前先根据Order进行排序
      callRunners(context, applicationArguments);


   } catch (Throwable ex) {


      //调用异常处理组件
      handleRunFailure(context, ex, exceptionReporters, listeners);
      throw new IllegalStateException(ex);
   }


   try {


      //回调通知应用启动run方法执行完成事件监听器
      listeners.running(context);
   } catch (Throwable ex) {
 
      handleRunFailure(context, ex, exceptionReporters, null);
      throw new IllegalStateException(ex);
   }


   //到这里,整个spring容器加载完毕
   return context;
}

由于篇幅的原因,这里暂时分析到这里,不再对每一个方法调用进行深入分析,因为里面实际包含了整个Spring IOC和AOP的核心源码过程。本章标题是启用流程源码分析,主要关注流程这里除了缺少启动tomcat那一块基本都包含了。至于每一个方法更深入的分析留到后面的文章分解式去讲解。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot是一个开源的Java框架,用于构建独立的、可执行的、生产级的Spring应用程序。它提供了一个快速、简单的方式来开发和部署应用程序。而在Spring Boot的启动过程中,有以下几个主要的步骤: 1. 加载启动类:Spring Boot应用程序的启动类通常是一个带有`@SpringBootApplication`注解的Java类。在应用程序启动时,会通过`main`方法加载这个启动类。 2. 创建Spring Application对象:Spring Boot会创建一个`SpringApplication`对象,用于启动应用程序。`SpringApplication`是Spring Boot框架的核心类,它负责管理整个应用程序的生命周期。 3. 解析配置信息:在启动过程中,`SpringApplication`会解析`application.properties`或`application.yaml`文件中的配置信息,并将其加载到Spring环境中。这些配置信息可以用来配置应用程序的各个方面,如数据库连接、日志级别等。 4. 创建并配置Spring容器:Spring Boot使用Spring容器来管理应用程序中的各个Bean。在启动过程中,`SpringApplication`会根据配置信息创建并配置一个Spring容器,该容器负责加载和管理应用程序中的所有Bean。 5. 执行自定义逻辑:在Spring Boot的启动过程中,可以添加自定义的逻辑。例如,可以通过实现`CommandLineRunner`接口来在应用程序启动后执行一些初始化操作。 6. 启动应用程序:完成上述步骤后,`SpringApplication`会启动应用程序,并通过Servlet容器(如Tomcat、Jetty等)监听端口,开始接收和处理HTTP请求。 总体而言,Spring Boot的启动流程是一个通过加载启动类、解析配置信息、创建和配置Spring容器的过程。通过Spring Boot的自动配置和快速启动能力,开发者可以更加方便地构建和部署Spring应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值