SpringBoot 系列(二)自定义Starter以及启动流程

PS: Spring Boot 用的是 2.6.2 版本

自定义Starter

Spring Boot starter机制
SpringBoot是由众多Starter组成(一系列的自动化配置的starter插件),SpringBoot之所以流行也是因为starter。
starter是SpringBoot非常重要的一部分,可以理解为一个可插拔式的插件,正是这些starter使得使用某个功能的开发者不需要关注各种依赖库的处理,不需要具体的配置信息,由Spring Boot自动通过classpath路径下的类发现需要的Bean,并织入相应的Bean。
例如,想使用redis插件,那么可以使用spring-boot-starter-redis。
为什么要自定义starter
开发过程中,经常有一些独立于业务之外的配置模块。如果我们将这些可独立业务代码之外的功能配置模块封装成一个个starter,复用的时候只需要在pom引用依赖即可,SpringBoot为我们完成自动装配。
自定义starter的命名规则
SpringBoot提供的starter以spring-boot-starter-xxx的方式命名的。官方建议自定义的starter使用xx-spring-boot-starter命名规则。以区分SpringBoot生态提供的starter
整个过程分为两部分:

  1. 自定义starter
  2. 使用starter

首先先完成自定义Starter

  1. 新建maven jar工程,工程名为zdy-spring-boot-starter,导入依赖
<dependencies>
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-autoconfigure</artifactId>
 <version>2.2.2.RELEASE</version>
 </dependency>
</dependencies>
  1. 编写javabean
@EnableConfigurationProperties(SimpleBean.class)
@ConfigurationProperties(prefix = "simplebean")
public class SimpleBean {
 private int id;
 private String name;
 public int getId() {
 return id;
 }
 public void setId(int id) {
 this.id = id;
 }
 public String getName() {
 return name;
 }
 public void setName(String name) {
 this.name = name;
 }
}
  1. 编写配置类
@Configuration
//@ConditionalOnClass: 当类路径classpath下有指定的类的情况下进行自动配置
@ConditionalOnClass
public class MyAutoConfiguration {

    static {
        System.out.println("MyAutoConfiguration  init ...");
    }

    @Bean
    public SimpleBean simpleBean() {
        return new SimpleBean();
    }
}
  1. resources下创建/META-INF/spring.factories
    注意:META-INF是自己手动创建的目录,spring.factories也是手动创建的文件,在该文件中配置自己的自动配置类


org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.lagou.config.MyAutoConfiguration

使用自定义starter

  1. 导入自定义starter的依赖
<dependency>
 <groupId>com.lagou</groupId>
 <artifactId>zdy-spring-boot-starter</artifactId>
 <version>1.0-SNAPSHOT</version>
</dependency>
  1. 在全局配置文件中配置属性值
simplebean.id=1
simplebean.name=starter

  1. 编写测试方法
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestBean {
    @Autowired
    SimpleBean simpleBean;
@Test
    public void aa(){
        System.out.println(simpleBean.getName());
    }
}

执行原理

每个SpringBoot项目都有一个主程序启动类,在主程序启动类中有一个启动项目的main()方法,在该方法中通过执行SpringApplication.run()即可启动整个Spring Boot程序。
问题:那么SpringApplication.run()如何做到启动SpringBoot项目?
下面看一下run()方法内部的源码,核心代码如下

  1. 启动类
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
  1. SpringApplication#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.run 方法内部执行了两个操作,分别是SpringApplication实例的初始化创建和调用调用run启动项目,这两个阶段具体说明如下
a. SpringApplication实例的初始化创建
查看SpringApplication实例对象初始化创建的源码信息,核心代码如下:

 public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        this.sources = new LinkedHashSet();
        this.bannerMode = Mode.CONSOLE;
        this.logStartupInfo = true;
        this.addCommandLineProperties = true;
        this.addConversionService = true;
        this.headless = true;
        this.registerShutdownHook = true;
        this.additionalProfiles = Collections.emptySet();
        this.isCustomEnvironment = false;
        this.lazyInitialization = false;
        this.applicationContextFactory = ApplicationContextFactory.DEFAULT;
        this.applicationStartup = ApplicationStartup.DEFAULT;
        this.resourceLoader = resourceLoader;
        Assert.notNull(primarySources, "PrimarySources must not be null");
        this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
//判断当前的webApplicationType
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        this.bootstrapRegistryInitializers = new ArrayList(this.getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
//设置初始化器,最后会调用这些初始化器
        this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
//设置监听器
        this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
// 用于推断并设置项目main方法的主程序启动类
        this.mainApplicationClass = this.deduceMainApplicationClass();
    }

从上述源码可以看出,SpringApplication的初始化过程主要包括4部分,具体说明如下:

  • this.webApplicationType = WebApplicationType.deduceFromClasspath();
    判断当前webApplicationType应用的类型。deduceFromClasspath方法用于查看Classpath类路径是否存在某个特征类,从而判断当前webApplicationType类型是SERVLET应用(Spring5之前的传统MVC应用)还是REACTIVE(Spring5开始出现的WebFlux交互式应用)

  • this.bootstrapRegistryInitializers = new ArrayList(this.getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
    用于SpringApplication应用的初始化器配置。在初始化器设置过程中,会使用Spring类加载器SpringFactoriesLoader从META-INF/spring.factories类路径下的spriing.factories文件中获取所有可用的应用初始化器类ApplicationContextInitalizer。

  • this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
    用于SpringApplication应用的监听器设置。监听器设置的过程和上一步初始化器设置的过程基本一样,也是使用SpringFactoriesLoader从META-INF/spring.factories类路径下的META-INF下spring.factories中获取所有可用的监听器类ApplicationListener。

  • this.mainApplicationClass = this.deduceMainApplicationClass();
    用于推断并设置项目main方法的主程序启动类

b. 项目的初始化启动
分析(new SpringApplication(primarySources)).run(args)源码前一部分SpringApplication实例对象的初始化创建后,查看run(args)方法执行的项目初始化启动过程,核心代码如下:

public ConfigurableApplicationContext run(String... args) {
        long startTime = System.nanoTime();
        DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
        ConfigurableApplicationContext context = null;
        this.configureHeadlessProperty();
//1. 获取并启动监听器
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        listeners.starting(bootstrapContext, this.mainApplicationClass);

        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//2. 根据SpringApplicationRunListeners以及参数来准备环境
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
            this.configureIgnoreBeanInfo(environment);
// 准备Banner 打印器(就是启动SpringBoot 打印在控制台的ASCII艺术字体
            Banner printedBanner = this.printBanner(environment);
//3. 创建Spring容器
            context = this.createApplicationContext();
            context.setApplicationStartup(this.applicationStartup);
//4. Spring 容器前置处理
            this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
//5. 刷新容器
            this.refreshContext(context);
//6.  Spring 容器后置处理
            this.afterRefresh(context, applicationArguments);
            Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), timeTakenToStartup);
            }

//7. 发出结束执行的事件
            listeners.started(context, timeTakenToStartup);
//返回容器
            this.callRunners(context, applicationArguments);
        } catch (Throwable var12) {
            this.handleRunFailure(context, var12, listeners);
            throw new IllegalStateException(var12);
        }

        try {
            Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
            listeners.ready(context, timeTakenToReady);
            return context;
        } catch (Throwable var11) {
            this.handleRunFailure(context, var11, (SpringApplicationRunListeners)null);
            throw new IllegalStateException(var11);
        }
    }

从上述源码可以看除,项目初始化启动过程大致包括以下部分:

  1. 获取并启动监听器

SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
主要获取SpringApplication实例初始化过程中初始化的SpringApplicationRunListener监听器并运行

  1. 根据SpringApplicationRunListeners以及参数来准备环境

this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);方法主要用于对项目运行环境进行预设置,同时通过this.configureIgnoreBeanInfo(environment);方法排除一些不需要的运行环境

  1. 创建Spring容器

根据webApplicationType进行判断,确定容器类型,如果该类型为SERVLET类型,会反射装载对应的字节码,也就是AnnotationConfigServletWebServerApplicationContext,接着使用之前初始化设置的context(应用上下文环境),environment(项目运行环境),listeners(运行监听器),applicationArguments(项目参数)和printedBanner(项目图标信息)进行应用上下文的组装配置,并刷新配置。

  1. Spring 容器前置处理

这一步主要是容器刷新之前的准备动作。设置容器环境,包括各种变量等等,其中包括一个非常关键的操作,将启动类注入容器,为后续开启自动化配置奠定基础。

  1. 刷新容器

开启刷新spring容器,通过refresh方法对整个IOC容器的初始化(包括bean资源的定位,解析注册等等),同时向JVM运行时注册一个关机钩子,在JVM关机时会关闭这个上下文,除非当时它已经关闭

  1. Spring 容器后置处理

扩展接口,设计模式中的模板方法,默认为空实现。如果有自定义需求,可以重写该方法。比如打印一些启动结束log,或者一些其他的后置处理。

  1. 发出结束执行的事件

获取EventPublishingRunListener监听器,并执行其started方法,并且将创建的Spring容器传进去了,创建一个ApplicationStartedEvent方法,也就是说这里是在Spring容器中发布事件,并不是在SpringApplication中发布事件,和前面的starting是不同的,前面的starting是直接向SpringApplication中的监听器发布启动事件。

  1. 执行Runners

用于调用项目自定义执行器XxxRunner类,使得项目启动完成后立即执行一些特定程序。其中Sprng Boot提供的执行器接口有ApplicationRunner和CommandLineRunner两种,在使用时只需要自定义一个执行器类实现其中一个接口并重写对应的run方法接口,然后Spring Boot项目启动后会立即执行这些特定程序。

这边通过一个Spring Boot执行流程图,让大家更清晰的知道Spring Boot的整体执行流程和主要启动阶段:

Spring Boot执行流程图

fang

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容