资讯专栏INFORMATION COLUMN

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

MobService / 572人阅读

摘要:设置应用上线文初始化器的作用是什么源码如下。来看下方法源码,其实就是初始化一个应用上下文初始化器实例的集合。设置监听器和设置初始化器调用的方法是一样的,只是传入的类型不一样,设置监听器的接口类型为,对应的文件配置内容请见下方。

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

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

Spring Boot 的入口类
@SpringBootApplication
public 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 {

    /**
     * Initialize the given application context.
     * @param applicationContext the application to configure
     */
    void initialize(C applicationContext);

}

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

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

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

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

private  Collection getSpringFactoriesInstances(Class type) {
    return getSpringFactoriesInstances(type, new Class[] {});
}

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

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

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

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

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

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

loadFactoryNames 方法相关的源码如下:

public static List 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> loadSpringFactories(@Nullable ClassLoader classLoader) {
    MultiValueMap result = cache.get(classLoader);
    if (result != null) {
        return result;
    }

    try {
        Enumeration 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 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 的初始化器相关配置内容如下:

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

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

List instances = createSpringFactoriesInstances(type, parameterTypes,
                classLoader, args, names);

private  List createSpringFactoriesInstances(Class type,
        Class[] parameterTypes, ClassLoader classLoader, Object[] args,
        Set names) {
    List 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 的作用是什么?源码如下。

@FunctionalInterface
public interface ApplicationListener 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 Listeners
org.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 的栈帧,从来得到入口类的名字再返回该类。

总结

源码分析内容有点多,也很麻烦,本章暂时分析到 SpringApplication 构造方法的初始化流程,下章再继续分析其 run 方法,作者很快写完过两天就发布,扫码关注下面的公众号 "Java技术栈" 即可获取推送更新。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76638.html

相关文章

  • 涨姿势:Spring Boot 2.x 启动过程源码分析

    摘要:参考创建所有运行监听器并发布应用启动事件来看下创建运行监听器相关的源码创建逻辑和之前实例化初始化器和监听器的一样,一样调用的是方法来获取配置的监听器名称并实例化所有的类。 上篇《Spring Boot 2.x 启动全过程源码分析(一)入口类剖析》我们分析了 Spring Boot 入口类 SpringApplication 的源码,并知道了其构造原理,这篇我们继续往下面分析其核心 ru...

    suemi 评论0 收藏0
  • spring boot - 收藏集 - 掘金

    摘要:引入了新的环境和概要信息,是一种更揭秘与实战六消息队列篇掘金本文,讲解如何集成,实现消息队列。博客地址揭秘与实战二数据缓存篇掘金本文,讲解如何集成,实现缓存。 Spring Boot 揭秘与实战(九) 应用监控篇 - HTTP 健康监控 - 掘金Health 信息是从 ApplicationContext 中所有的 HealthIndicator 的 Bean 中收集的, Spring...

    rollback 评论0 收藏0
  • SpringBoot源码分析系列(一)--核心注解

    摘要:用于主类上最最最核心的注解,表示这是一个项目,用于开启的各项能力。下面我们来分析一下这个注解的组成以及作用通过上面的代码我们可以看出来是一个组合注解,主要由和这三个注解组成的。通过源码可以看出也是一个组合注解。   SpringBoot项目一般都会有Application的入口类,入口类中会有main方法,这是一个标准的java应用程序的入口方法。@SpringBootApplicat...

    seanlook 评论0 收藏0
  • Spring Boot 最核心的 3 个注解详解

    摘要:核心注解讲解最大的特点是无需配置文件,能自动扫描包路径装载并注入对象,并能做到根据下的包自动配置。所以最核心的个注解就是这是添加的一个注解,用来代替配置文件,所有这个配置文件里面能做到的事情都可以通过这个注解所在类来进行注册。 最近面试一些 Java 开发者,他们其中有些在公司实际用过 Spring Boot, 有些是自己兴趣爱好在业余自己学习过。然而,当我问他们 Spring Boo...

    hzx 评论0 收藏0
  • Spring Boot 面试,一个问题就干趴下了!

    摘要:我又问微服务和有什么关系不用行不行然后对方就吱吱唔唔了可以打包部署,内部集成了。为什么说是自动配置的开启注解是,其实它就是由下面三个注解组成的上面三个注解,前面两个都是自带的,和无关,所以说上面的回答的不是在点上。 最近栈长面试了不少人,其中不乏说对 Spring Boot 非常熟悉的,然后当我问到一些 Spring Boot 核心功能和原理的时候,没人能说得上来,或者说不到点上,可以...

    junbaor 评论0 收藏0

发表评论

0条评论

MobService

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<