Spring Boot 2.x 启动全过程源码分析(上)入口类剖析

Spring Boot 的应用教程我们已经分享过很多了,今天来通过源码来分析下它的启动过程,探究下 Spring Boot 为什么这么简便的奥秘。

本篇基于 Spring Boot 2.0.3 版本进行分析,阅读本文需要有一些 Java 和 Spring 框架基础,如果还不知道 Spring Boot 是什么,建议先看下我们的 Spring Boot 教程

Spring Boot 的入口类

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

做过 Spring Boot 项目的都知道,上面是 Spring Boot 最简单通用的入口类。入口类的要求是最顶层包下面第一个含有 main 方法的类,使用注解 @SpringBootApplication 来启用 Spring Boot 特性,使用 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);}

第一个参数 primarySource:加载的主要资源类

第二个参数 args:传递给应用的应用参数

先用主要资源类来实例化一个 SpringApplication 对象,再调用这个对象的 run 方法,所以我们分两步来分析这个启动源码。

SpringApplication 的实例化过程

接着上面的 SpringApplication 构造方法进入以下源码:

public SpringApplication(Class<?>... primarySources) {    this(null, primarySources);}public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {    // 1、资源初始化资源加载器为 null    this.resourceLoader = resourceLoader;    // 2、断言主要加载资源类不能为 null,否则报错    Assert.notNull(primarySources, "PrimarySources must not be null");    // 3、初始化主要加载资源类集合并去重    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));    // 4、推断当前 WEB 应用类型    this.webApplicationType = deduceWebApplicationType();    // 5、设置应用上线文初始化器    setInitializers((Collection) getSpringFactoriesInstances(            ApplicationContextInitializer.class));          // 6、设置监听器                setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));    // 7、推断主入口应用类    this.mainApplicationClass = deduceMainApplicationClass();}

可知这个构造器类的初始化包括以下 7 个过程。

1、资源初始化资源加载器为 null

this.resourceLoader = resourceLoader;

2、断言主要加载资源类不能为 null,否则报错

Assert.notNull(primarySources, "PrimarySources must not be null");

3、初始化主要加载资源类集合并去重

this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));

4、推断当前 WEB 应用类型

this.webApplicationType = deduceWebApplicationType();

来看下 deduceWebApplicationType 方法和相关的源码:

private WebApplicationType deduceWebApplicationType() {    if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null)            && !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)) {        return WebApplicationType.REACTIVE;    }    for (String className : WEB_ENVIRONMENT_CLASSES) {        if (!ClassUtils.isPresent(className, null)) {            return WebApplicationType.NONE;        }    }    return WebApplicationType.SERVLET;}private static final String REACTIVE_WEB_ENVIRONMENT_CLASS = "org.springframework."        + "web.reactive.DispatcherHandler";private static final String MVC_WEB_ENVIRONMENT_CLASS = "org.springframework."        + "web.servlet.DispatcherServlet";private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet",            "org.springframework.web.context.ConfigurableWebApplicationContext" };      public enum WebApplicationType {    /**     * 非 WEB 项目     */    NONE,    /**     * SERVLET WEB 项目     */    SERVLET,    /**     * 响应式 WEB 项目     */    REACTIVE}

这个就是根据类路径下是否有对应项目类型的类推断出不同的应用类型。

5、设置应用上线文初始化器

setInitializers((Collection) getSpringFactoriesInstances(            ApplicationContextInitializer.class));

ApplicationContextInitializer 的作用是什么?源码如下。

public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {    /**     * Initialize the given application context.     * @param applicationContext the application to configure     */    void initialize(C applicationContext);}

用来初始化指定的 Spring 应用上下文,如注册属性资源、激活 Profiles 等。

来看下 setInitializers 方法源码,其实就是初始化一个 ApplicationContextInitializer 应用上下文初始化器实例的集合。

public void setInitializers(        Collection<? extends ApplicationContextInitializer<?>> initializers) {    this.initializers = new ArrayList<>();    this.initializers.addAll(initializers);}

再来看下这个初始化 getSpringFactoriesInstances 方法和相关的源码:

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {    return getSpringFactoriesInstances(type, new Class<?>[] {});}private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,        Class<?>[] parameterTypes, Object... args) {    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();    // 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;}

设置应用上下文初始化器可分为以下 5 个步骤。

5.1)获取当前线程上下文类加载器

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

5.2)获取 ApplicationContextInitializer 的实例名称集合并去重

Set<String> names = new LinkedHashSet<>(                SpringFactoriesLoader.loadFactoryNames(type, classLoader));

loadFactoryNames 方法相关的源码如下:

public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {    String factoryClassName = factoryClass.getName();    return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());}public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {    MultiValueMap<String, String> result = cache.get(classLoader);    if (result != null) {        return result;    }    try {        Enumeration<URL> urls = (classLoader != null ?                classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));        result = new LinkedMultiValueMap<>();        while (urls.hasMoreElements()) {            URL url = urls.nextElement();            UrlResource resource = new UrlResource(url);            Properties properties = PropertiesLoaderUtils.loadProperties(resource);            for (Map.Entry<?, ?> entry : properties.entrySet()) {                List<String> factoryClassNames = Arrays.asList(                        StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));                result.addAll((String) entry.getKey(), factoryClassNames);            }        }        cache.put(classLoader, result);        return result;    }    catch (IOException ex) {        throw new IllegalArgumentException("Unable to load factories from location [" +                FACTORIES_RESOURCE_LOCATION + "]", ex);    }}

根据类路径下的 META-INF/spring.factories 文件解析并获取 ApplicationContextInitializer 接口的所有配置的类路径名称。

spring-boot-autoconfigure-2.0.3.RELEASE.jar!/META-INF/spring.factories 的初始化器相关配置内容如下:

# Initializersorg.springframework.context.ApplicationContextInitializer=\org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

5.3)根据以上类路径创建初始化器实例列表

List<T> instances = createSpringFactoriesInstances(type, parameterTypes,                classLoader, args, names);private <T> List<T> createSpringFactoriesInstances(Class<T> type,        Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args,        Set<String> names) {    List<T> instances = new ArrayList<>(names.size());    for (String name : names) {        try {            Class<?> instanceClass = ClassUtils.forName(name, classLoader);            Assert.isAssignable(type, instanceClass);            Constructor<?> constructor = instanceClass                    .getDeclaredConstructor(parameterTypes);            T instance = (T) BeanUtils.instantiateClass(constructor, args);            instances.add(instance);        }        catch (Throwable ex) {            throw new IllegalArgumentException(                    "Cannot instantiate " + type + " : " + name, ex);        }    }    return instances;}

5.4)初始化器实例列表排序

AnnotationAwareOrderComparator.sort(instances);

5.5)返回初始化器实例列表

return instances;

6、设置监听器

setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

ApplicationListener 的作用是什么?源码如下。

@FunctionalInterfacepublic interface ApplicationListener<E extends ApplicationEvent> extends EventListener {    /**     * Handle an application event.     * @param event the event to respond to     */    void onApplicationEvent(E event);}

看源码,这个接口继承了 JDK 的 java.util.EventListener 接口,实现了观察者模式,它一般用来定义感兴趣的事件类型,事件类型限定于 ApplicationEvent 的子类,这同样继承了 JDK 的 java.util.EventObject 接口。

设置监听器和设置初始化器调用的方法是一样的,只是传入的类型不一样,设置监听器的接口类型为: getSpringFactoriesInstances,对应的 spring-boot-autoconfigure-2.0.3.RELEASE.jar!/META-INF/spring.factories 文件配置内容请见下方。

# Application Listenersorg.springframework.context.ApplicationListener=\org.springframework.boot.autoconfigure.BackgroundPreinitializer

可以看出目前只有一个 BackgroundPreinitializer 监听器。

7、推断主入口应用类

this.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;}

这个推断入口应用类的方式有点特别,通过构造一个运行时异常,再遍历异常栈中的方法名,获取方法名为 main 的栈帧,从来得到入口类的名字再返回该类。

原文发布于微信公众号 - Java技术栈(javastack)

原文发表时间:2018-08-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Hongten

FreeMarker_模板引擎_代码自动生成器_源码下载

你可以到freemarker的官网上去,那里有很详细的介绍:http://freemarker.org/

2381
来自专栏精讲JAVA

深入 Spring Boot :怎样排查 java.lang.ArrayStoreException

这个demo来说明怎样排查一个spring boot 1应用升级到spring boot 2时可能出现的java.lang.ArrayStoreExceptio...

3464
来自专栏编程心路

一文读懂SpringMVC中的数据绑定

Struts2 和 SpringMVC 都是 Web 开发中视图层的框架,两者都实现了数据的自动绑定,都不需要我们手动获取参数然后关联到对应的属性上,下面就谈谈...

1353
来自专栏刘望舒

自定义注解和解析器实现ButterKnife

相信绝大部分的Android开发者都曾使用过ButterKnife, 利用ButterKnife开发者可以快速的实现实体view与xml的绑定,此外还能绑定各种...

3746
来自专栏Ryan Miao

SpringMVC参数校验(针对`@RequestBody`返回`400`)

SpringMVC参数校验(针对@RequestBody返回400) From https://ryan-miao.github.io/2017/05/20/s...

5174
来自专栏GopherCoder

『Go 内置库第一季:json』

在日常开发过程中,使用最频繁的当然是内置库,无数的开源项目,无不是在内置库的基础之上进行衍生、开发,所以其实是有很大的必要进行梳理学习。

1072
来自专栏光变

Java使用slf4j输出日志

801
来自专栏青青天空树

springboot+security整合3

  这篇讲解如何自定义鉴权过程,实现根据数据库查询出的url和method是否匹配当前请求的url和method来决定有没有权限。security鉴权过程如下:...

3922
来自专栏osc同步分享

springmvc 其他

<!-- 配置直接转发的页面 --> <!-- 可以直接相应转发的页面, 而无需再经过 Handler 的方法. --> <mvc:view-contr...

3568
来自专栏别先生

Spring的核心之IoC容器创建对象

Spring的Ioc容器,是Spring的核心内容:   作用:对象的创建和处理对象的依赖关系。 Spring容器创建对象有以下几种方式:   1:调...

2185

扫码关注云+社区

领取腾讯云代金券