源码分析SpringBoot启动

17次阅读

共计 8937 个字符,预计需要花费 23 分钟才能阅读完成。

遇到一个问题,需要从 yml 文件中读取数据初始化到 static 的类中。搜索需要实现 ApplicationRunner,并在其实现类中把值读出来再 set 进去。于是乎就想探究一下 SpringBoot 启动中都干了什么。

引子

就像引用中说的,用到了 ApplicationRunner 类给静态 class 赋 yml 中的值。代码先量一下,是这样:

@Data
@Component
@EnableConfigurationProperties(MyApplicationRunner.class)
@ConfigurationProperties(prefix = "flow")
public class MyApplicationRunner implements ApplicationRunner {

    private String name;
    private int age;
    @Override
    public void run(ApplicationArguments args) throws Exception {System.out.println("ApplicationRunner...start...");
        MyProperties.setAge(age);
        MyProperties.setName(name);
        System.out.println("ApplicationRunner...end...");
    }
}
public class  MyProperties {
    private static String name;
    private static int age;
    public static String getName() {return name;}
    public static void setName(String name) {MyProperties.name = name;}
    public static int getAge() {return age;}
    public static void setAge(int age) {MyProperties.age = age;}
}

从 SpringApplication 开始

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

这是一个 SpringBoot 启动入口,整个项目环境搭建和启动都是从这里开始的。我们就从 SpringApplication.run() 点进去看一下,Spring Boot 启动的时候都做了什么。点进去 run 看一下。

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

首先经过了两个方法,马上就要进入关键了。SpringApplication(primarySources).run(args),这句话做了两件事,首先初始化 SpringApplication,然后进行开启 run。首先看一下初始化做了什么。

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = deduceMainApplicationClass();}

首先读取资源文件 Resource, 然后读取 FlowApplication 这个类信息(就是 primarySources),然后从 classPath 中确定是什么类型的项目,看一眼 WebApplicationType 这里面有三种类型:

public enum WebApplicationType {
    NONE, // 不是 web 项目
    SERVLET,// 是 web 项目
    REACTIVE;//2.0 之后新加的,响应式项目
    ...
}

回到 SpringApplication 接着看,确定好项目类型之后,初始化一些信息 setInitializers(),getSpringFactoriesInstances() 看一下都进行了什么初始化:

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, Object... args) {ClassLoader classLoader = getClassLoader();
    // Use names and ensure unique to protect against duplicates
    Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
            classLoader, args, names);
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

首先得到 ClassLoader,这个里面记录了所有项目 package 的信息、所有 calss 的信息啊什么什么的,然后初始化各种 instances,在排个序,ruturn 之。

再回到 SpringApplication,接着是设置监听器 setListeners()。

然后设置 main 方法,mainApplicationClass(),点进 deduceMainApplicationClass() 看一看:

private Class<?> deduceMainApplicationClass() {
    try {StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
        for (StackTraceElement stackTraceElement : stackTrace) {if ("main".equals(stackTraceElement.getMethodName())) {return Class.forName(stackTraceElement.getClassName());
            }
        }
    }
    catch (ClassNotFoundException ex) {// Swallow and continue}
    return null;
}

从方法栈 stackTrace 中,不断读取方法,通过名称,当读到“main”方法的时候,获得这个类实例,return 出去。

到这里,所有初始化工作结束了,也找到了 Main 方法,ruturn 给 run() 方法,进行后续项目的项目启动。

准备好,开始 run 吧

先上代码:

public ConfigurableApplicationContext run(String... args) {StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    configureHeadlessProperty();
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                applicationArguments);
        configureIgnoreBeanInfo(environment);
        Banner printedBanner = printBanner(environment);
        context = createApplicationContext();
        exceptionReporters = getSpringFactoriesInstances(
                SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class}, context);
        prepareContext(context, environment, listeners, applicationArguments,
                printedBanner);
        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;
}

首先开启一个计时器,记录下这次启动时间,咱们项目开启 XXXms statred 就是这么计算来的。
然后是一堆声明,知道 listeners.starting(),这个 starting(),我看了一下源码注释

Called immediately when the run method has first started. Can be used for very
early initialization.

早早初始化,是为了后面使用,看到后面还有一个方法 listeners.started()

Called immediately before the run method finishes, when the application context has been refreshed and all {@link CommandLineRunner CommandLineRunners} and {@link ApplicationRunner ApplicationRunners} have been called.

这会儿应该才是真正的开启完毕,值得一提的是,这里终于看到了引子中的 ApplicationRunner 这个类了,莫名的有点小激动呢。

我们继续进入 try,接下来是读取一些参数 applicationArguments,然后进行 listener 和 environment 的一些绑定。然后打印出 Banner 图,printBanner(), 这个方法里面可以看到把 environment,也存入 Banner 里面了,应该是为了方便打印,如果有日志模式,也打印到日志里面,所以,项目启动的打印日志里面记录了很多东西。

private Banner printBanner(ConfigurableEnvironment environment) {if (this.bannerMode == Banner.Mode.OFF) {return null;}
    ResourceLoader resourceLoader = (this.resourceLoader != null)
            ? this.resourceLoader : new DefaultResourceLoader(getClassLoader());
    SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
    if (this.bannerMode == Mode.LOG) {return bannerPrinter.print(environment, this.mainApplicationClass, logger);
    }
    return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}

接着 生成上下文环境 context = createApplicationContext(); 还记着 webApplicationType 三种类型吗,这边是根据 webApplicationType 类型生成不同的上下文环境类的。

接着开启 exceptionReporters,用来支持启动时的报错。

接着就要准备往上下文中 set 各种东西了,看 prepareContext() 方法:

private void prepareContext(ConfigurableApplicationContext context,
        ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments, Banner printedBanner) {context.setEnvironment(environment);
    postProcessApplicationContext(context);
    applyInitializers(context);
    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);
    }
    // Load the sources
    Set<Object> sources = getAllSources();
    Assert.notEmpty(sources, "Sources must not be empty");
    load(context, sources.toArray(new Object[0]));
    listeners.contextLoaded(context);
}

首先把环境 environment 放进去,然后把 resource 信息也放进去,再让所有的 listeners 知道上下文环境。 这个时候,上下文已经读取 yml 文件了 所以这会儿引子中 yml 创建的参数,上下文读到了配置信息,又有点小激动了!接着看,向 beanFactory 注册单例 bean:一个参数 bean,一个 Bannerbean。

prepareContext() 这个方法大概先这样,然后回到 run 方法中,看看项目启动还干了什么。

refreshContext(context) 接着要刷新上下问环境了,这个比较重要,也比较复杂,今天只看个大概,有机会另外写一篇博客,说说里面的东西,这里面主要是有个 refresh() 方法。看注释可知,这里面进行了 Bean 工厂的创建,激活各种 BeanFactory 处理器,注册 BeanPostProcessor,初始化上下文环境,国际化处理,初始化上下文事件广播器,将所有 bean 的监听器注册到广播器(这样就可以做到 Spring 解耦后 Bean 的通讯了吧)

总之,Bean 的初始化我们已经做好了,他们直接也可以很好的通讯。

接着回到 run 方法,
afterRefresh(context, applicationArguments); 这方法里面没有任何东西,网上查了一下,说这里是个拓展点,有机会研究下。

接着 stopWatch.stop(); 启动就算完成了,因为这边启动时间结束了。
我正要失落的发现没找到我们引子中说到的 ApplicationRunner 这个类, 就在下面看到了最后一个方法,必须贴出来源码:
callRunners(context, applicationArguments),当然这个方法前面还有 listeners.started().

private void callRunners(ApplicationContext context, ApplicationArguments args) {List<Object> runners = new ArrayList<>();
    runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
    runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
    AnnotationAwareOrderComparator.sort(runners);
    for (Object runner : new LinkedHashSet<>(runners)) {if (runner instanceof ApplicationRunner) {callRunner((ApplicationRunner) runner, args);
        }
        if (runner instanceof CommandLineRunner) {callRunner((CommandLineRunner) runner, args);
        }
    }
}

看到没,这会就执行了 ApplicationRunner 方法(至于 CommandLineRunner,和 ApplicationRunner 类似,只是参数类型不同,这边不做过多区分先)。所以可以说 ApplicationRunner 不是启动的一部分,不记录进入 SpringBoot 启动时间内,这也好理解啊,你自己初始化数据的时间凭什么算到我 SpringBoot 身上,你要初始化的时候做了个费时操作,回头又说我 SpringBoot 辣鸡,那我不是亏得慌 …

最后 run 下这个,listeners.running(context); 这会儿用户自定义的事情也会被调用了。

ok,结束了。

小结

今天只是大概看了下 SpringBoot 启动过程。有很多细节,比如 refresh() 都值得再仔细研究一下。SpringBoot 之所以好用,就是帮助我们做了很多配置,省去很多细节(不得不说各种 stater 真实让我们傻瓜式使用了很多东西),但是同样定位 bug 或者通过项目声明周期搞点事情的时候会无从下手。所以,看看 SpringBoot 源码还是听有必要的。

正文完
 0