springboot如何创建并配置环境

简介: springboot如何创建并配置环境

一、介绍

在springboot的启动流程中,启动环境Environment是可以说是除了应用上下文ApplicationContext之外最重要的一个组件了,而且启动环境为应用上下文提供了最基本的前提基础。

在启动环境中,主要保存大量配置信息和当前操作系统的配置信息以及环境变量。

对于它的重要性,我们可以这样理解:启动环境为创建应用上下文提供了基础支持,而应用上下文为我们开发springboot项目提供了基础支持。

本文基于以下版本进行展开:

  • jdk:1.8

  • springboot:2.4.3

二、启动环境Environment的分析

老规矩,在了解一个类之前,我们需要先通过其UML图对该类的功能有一个大致的了解,下面是启动环境Environment的UML图:

三种环境的UML图.png

  • PropertyResolver:顾名思义为属性解析器,提供用来解析并保存形如key=value这样的属性。

  • Environment:在PropertyResolver的基础上添加了对profile的支持,其实profile也是形如key=value的属性配置,只是为了更清晰就把它做成独立的api了。

  • ConfigurablePropertyResolver可配置的属性解析器。在PropertyResolver的基础上添加了类型转换器ConversionService

  • ConfigurableEnvironment可配置的环境,即启动环境。本片文章主要就是围绕它来展开的。它对Environment做出了扩展,允许动态设置profile。并对其内部保存的属性集合进行分类,如:操作系统的属性、操作系统的环境变量。

  • ConfigurableWebEnvironment:在ConfigurableEnvironment的基础上添加了对servlet类型的web环境的支持。

  • ConfigurableReactiveWebEnvironment:在ConfigurableEnvironment的基础上添加了对响应式类型的web环境的支持。

从上图中不难看出,springboot为我们提供了三种启动环境

  • StandardEnvironment标准环境。提供基本的springboot启动环境。
  • StandardServletEnvironmentservlet类型的web环境。在标准环境的基础上,添加了对servlet类型的web环境的环境处理。
  • StandardReactiveWebEnvironment响应式类型的web环境。在标准环境的基础上,添加了对响应式类型的web环境的环境处理。

三、进入源码

在springboot启动流程的源码中,我们不难发现,启动环境的创建和配置是在一个prepareEnvironment()方法中完成的,如下所示:

springboot的run()方法.png

进入该方法查看其实现逻辑:

prepareEnvironment()方法.png

本文主要围绕prepareEnvironment()方法探讨springboot是如何创建运行环境并对其进行配置的。

下面进入正题。

四、创建环境

prepareEnvironment()方法中,getOrCreateEnvironment()方法负责实例化环境对象,并将创建好的环境返回。所以我们需要进入该方法:

getOrCreateEnvironment()方法.png

该方法很简单,就是根据当前应用类型去实例化对应的环境对象

  • 如果是servlet类型的web环境,则实例化一个StandardServletEnvironment对象
  • 如果是响应式类型的web环境,则实例化一个StandardReactiveWebEnvironment对象
  • 如果以上两种web环境都不是,则默认实例化一个标准环境对象StandardEnvironment

但是,springboot是如何知道我们当前应用是哪一种类型呢?即webAppliicationType是如何确定的?

1. 如何确定应用类型

当我们在springboot的主启动类中使用SpringApplicaton.run()启动项目时,其内部其实是先创建一个SpringApplicaton实例,然后对该实例调用其run()方法,如下图所示

调用SpringApplication实例的run()方法.png

在创建SpringApplicaton实例时,该构造方法内部确定当前应用程序类型并将该类型保存到webApplicationType属性中,如下图所示

SpringApplication构造方法.png

从该行代码可以看出,springboot通过调用WebApplicationType的静态方法deduceFromClasspath()推断出当前应用程序类型

我们再进入该静态方法来了解它是如何推断的

WebApplicationType的静态方法deduceFromClasspath().png

从该方法中看到,推断过程无非就是从类路径中判断是否存在指定的类

  • 如果类路径中存在servlet相关的类,那么当前应用程序就是servlet类型的应用程序
  • 如果类路径中仅存在reactive相关的类,那么当前应用程序就是响应式类型的应用程序
  • 如果以上两种类都不存在,那么当前应用程序就什么类型的应用程序都不是了。

判断类路径中判断是否存在指定的类只需要调用ClassUtils的静态方法isPresent()就行了。而在该静态方法中,则是通过对传入的类进行反射去实例化,如果实例化失败并抛出了异常,则说明该类是不存在的。

2. 测试

  • 既不存在reactive相关的类,也不存在servlet相关的类

    在pom中我们仅仅引入springboot的依赖

    仅仅引入springboot的依赖.png

然后启动项目进入断点,查看当前应用程序的类型,确定为NONE类型的应用程序

NONE类型的应用程序.png

  • 仅存在reactive相关的类

    在pom中我们引入springboot的依赖 和 reactive相关的依赖

    引入springboot的依赖和reactive相关的依赖.png

然后启动项目进入断点,查看当前应用程序的类型,确定为响应式类型的应用程序

响应式类型的应用程序.png

  • 存在servlet相关的类

    在pom中我们引入springboot的依赖 和 servlet相关的依赖

    引入springboot的依赖和servlet相关的依赖.png

然后启动项目进入断点,查看当前应用程序的类型,确定为servlet类型的应用程序

servlet类型的应用程序.png

五、配置基础环境

在前面的章节中我们探讨了springboot是如何根据当前应用程序类型去创建对应的环境实例的。接下来探讨如何去配置完善该运行环境。

下面我们以标准环境StandardEnvironment为例进行分析。

1. 配置系统属性和环境变量

首先我们应该判断在创建一个运行环境实例对象时,在构造器内部是否就已经开始对某些配置属性进行处理了。

在创建运行环境实例时,是通过标准环境StandardEnvironment的无参构造方法完成的,进入该构造方法后发现它是一个空方法

标准运行环境无参构造方法.png

由于该类继承于AbstractEnvironment,那么在执行StandardEnvironment的构造方法时会默认先调用父类的无参构造方法,接下来再看父类的无参构造。

抽象环境的构造方法.png

在该无参构造中,先实例化一个MutablePropertySources对象,然后再调用了下面的有参构造方法,并对propertySourcespropertyResolver这两个属性进行初始化。

  • MutablePropertySources

    该类内部维护了一个PropertySource集合,该集合中保存着大量以key,value形式的属性配置,且value值可以是任何数据类型。mutable意为可变的,表示允许调用方去修改其内部的属性配置。如下图所示

    MutablePropertySources类.png

看到PropertiySource大家是否还有印象,我们在springboot从命令行读取应用程序参数这篇文章中有介绍过,它其实就是一个以key,value形式的属性配置对象,且value值可以是任何数据类型(用泛型T表示)。

  • propertySources

    该属性指向MutablePropertySources对象。

  • propertyResolver

    属性解析器,提供一些从propertySources中获取所需要的属性的方法。

该运行环境实例的两个重要属性初始化完成后,该实例就具备了利用属性解析器propertyResolver解析配置属性并将解析后的结果保存到propertySources中的功能了。

接下来我们再看customizePropertySources()方法做了什么?从命名上来看,它可以自定义地添加一些属性,查看该方法是一个空方法

抽象父类的customizePropertySources方法.png

那么我们再回到它的子类StandardEnvironment查看该方法实现。

标准环境的customizePropertySources方法.png

在该方法中,我们可以看到,通过调用MutablePropertySourcesaddLast()方法,向其内部的PropertySource集合尾部添加两种属性:PropertiesPropertySourceSystemEnvironmentPropertySource

这两个类都是PropertiySource抽象类的实现类,用于保存不同类型的属性,但他们本质上都是PropertiySource实例。其中

  • PropertiesPropertySource

    该类用于保存系统属性,其中key为systemPropertiesvalue为Map集合,该集合中保存所有系统属性

  • SystemEnvironmentPropertySource

    该类用于保存系统环境变量,其中key为systemEnvironmentvalue为Map集合,该集合中保存所有系统环境变量

另外,PropertiySource还有很多种不同的实现类,用于保存不同类型的属性,如下所示

PropertySource的各种实现.png

下面我们用图示将当前已经保存的属性、以及目前运行环境保存属性的结构表示出来

运行环境中保存的属性1.png

好了,说了这么多我会不会是在瞎说,我们测一下就好了,把断点打在创建标准环境StandardEnvironment实例的下一行,查看此时运行环境中的属性

创建环境实例后的属性.png

  • 系统属性

    从调试中我们看到,springboot找到了64个系统属性

    系统属性.png

  • 系统环境变量

    从调试中我们看到,springboot找到了65个系统环境变量

    环境变量.png

2. 配置自定义属性命令行参数

自定义属性主要是来自命令行参数。在前面的文章springboot读取命令行参数中我们已经详细介绍过springboot是如何读取命令行参数并将其保存在ApplicationArguments对象中的,但这些来自命令行的参数目前并不能作为启动环境信息、,需要将其也保存到启动环境中。

我们继续读prepareEnvironment()方法源码,来到configureEnvironment()这一行,

prepareEnvironment()方法.png

进入configureEnvironment()方法,该方法的逻辑主要分三部分,其中第一部分设置类型转换器

configureEnvironment()方法.png

该方法的处理逻辑主要分三部分

  • 设置类型转换器

    该部分逻辑只是向环境中设置一些springboot内置的类型转换器Convertor,本篇文章不具体展开来讲。

  • 配置自定义的普通参数

    configurePropertySources方法.png

  • 首先将默认属性配置添加到MutablePropertySources的尾部

    默认属性配置的设置方法是通过SpringApplication对象的setDefaultProperties()方法设置的,如下所示

    设置defaultProperties的方法.png

  • 配置命令行参数

    在前面的文章springboot读取命令行参数我们已经详细介绍过springboot如何将命令行参数转为SimpleCommandLinePropertySource,本文就不再赘述。

    在这一步会先判断我们已经保存的属性配置中是否已经存在key=commandLineArgs了,从我们逐行阅读源码的过程中我们可以断定是不存在这样的属性的,因此会执行else代码块,即只需要将SimpleCommandLinePropertySource添加到已经保存的属性配置的首部即可。

    此时我们再次用图示将当前已经保存的属性、以及目前运行环境保存属性的结构表示出来

    运行环境中保存的属性2.png

当然了,如果你想拒绝任何来自命令行的属性参数,可以通过以下方法将`addCommandLineProperties`属性设置为`false`,这样springboo就不会把命令行参数保存到启动环境中了

![关闭命令行参数功能.png](https://ucc.alicdn.com/pic/developer-ecology/een7m2naevh4a_6c40c02a8a754a21a7d1ce016d2576fb.png)
  • 配置来自命令行的profile

    该方法为空方法,保留该方法方面日后扩展。

    configureProfiles()方法.png

3. 作为应用配置信息

在处理完自定义的属性后,下一步springboot需要对当前启动环境中的属性配置适配ConfigurationPropertySources的支持,如下图所示

添加ConfigurationPropertySources的支持.png

进入attach()方法,如下所示

attach()方法.png

该方法执行后的结果就是将当前启动环境中已保存的属性配置source封装到一个ConfigurationPropertySourcesPropertySource对象中,并且对应的key=configurationProperties,此时启动环境中保存的属性配置如下所示

运行环境中保存的属性3.png

至于为什么这么做,我们可以看一下方法注释:

Attach a ConfigurationPropertySource support to the specified Environment. Adapts each PropertySource managed by the environment to a ConfigurationPropertySource and allows classic PropertySourcesPropertyResolver calls to resolve using configuration property names.
The attached resolver will dynamically track any additions or removals from the underlying Environment property sources.

从注释中得知,该方法的目的就是真正的把当前保存在启动环境中的这些propertySources作为配置属性,就是说这些属性在此之前springboot并没有把他们作为配置来看,如今它们成为了配置属性

六、配置扩展属性

这里所说的扩展属性,即表示来自不同位置的配置属性,如:classpath路径下的application.ymlbootstrap.yml使用@PropertySource注解指定的文件、以及来自项目外部的配置文件等。

1. 配置文件application.yml

我们在项目的resources目录下新建配置文件application.yml,并添加如下配置

配置文件1.png

然后进入断点调试。

当代码执行到下面这一行时,我们查看此时环境实例中保存的配置属性都是前面我们讲过的,来自配置文件中的属性还没有被保存。

还没有加载配置文件.png

当我们进行到下一行时,此时来自配置文件中的属性就已经被保存到环境中来了

已经加载配置文件.png

很明显,springboot通过观察者模式发布一个环境准备就绪事件,由监听该事件的监听器处理不同的逻辑,即以下代码

// 通过观察者模式发布一个环境准备就绪事件,由监听该事件的监听器处理不同的逻辑
listeners.environmentPrepared(bootstrapContext, environment);

我们进入environmentPrepared()方法一探究竟,该方法源码如下所示,大致意思就是向this.listeners传入Consumer对象,由this.listeners中的监听器来执行这个Consumer对象。

spring运行监听器的environmentPrepared()方法.png

插入一嘴,this.listenersSpringApplicationRunListener运行时监听器的集合,如下所示

RunListener集合.png

该集合是进入run()方法后执行的第二个关键步骤,第一个关键步骤就是创建BootstrapContext上下文。

getRunListeners()方法的调用.png

而对SpringApplicationRunListener运行时监听器的集合的获取则是从META-INF目录下的spring.factories文件中获取的。

getRunListeners()方法.png

运行时监听器的集合中内置的该监听器只有一种,即EventPublishingRunListener事件发布监听器,该监听器专门用来发布事件。而我们当前正要发布一个环境准备就绪事件

而且我们在调试代码过程中也看到了,此时需要发布的是环境准备就绪事件,调用的是监听器的environmentPrepared()方法,所以我们进入EventPublishingRunListener监听器的这个方法

发布事件的RunListener.png

此时才真正的创建了环境准备就绪事件的实例ApplicationEnvironmentPreparedEvent,并通过this.initialMulticaster广播该事件。其实这里涉及到的只是springboot对观察者模式的实现。但是为了进一步摸清整个环境配置过程的来龙去脉,也无所谓了。

下面我们进入multicastEvent()方法来看看是如何广播的。

广播环境准备就绪事件.png

该方法通过getApplicationListeners()方法获取项目中监听当前事件的所有监听器,并对其进行遍历,然后不同的监听器针对该事件进行不同的逻辑处理。

针对环境准备就绪事件的监听器.png

从上面的截图中我们发现,针对环境准备就绪事件的监听器有6个,他们监听到该事件后处理不同的逻辑。

  • EnvironmentPostProcessorApplicationListener:使用环境后处理器对环境进行配置
  • AnsiOutputApplicationListener:控制日志的颜色,决定输出的日志文本是否具有颜色
  • LoggingApplicationListener:对日志进行配置
  • BackgroundPreinitializer:提前初始化耗时任务的后台线程
  • DelegatingApplicationListener:将监听到的事件再次发布,由指定的监听器执行
  • FileEncodingApplicationListener:如果系统文件编码与环境中设置的期望值不匹配,则立刻停止应用程序的启动

从上面的说明来看,我们当前关注的是如何将配置文件中的配置信息加载到环境中,因此我们只需要关注EnvironmentPostProcessorApplicationListener在监听到环境准备就绪事件后执行什么处理逻辑即可。

下面我们进入监听器EnvironmentPostProcessorApplicationListener,在springboot中,监听器通过onApplicationEvent()方法监听事件,该方法如下所示

环境后处理监听器对事件的处理方法.png

前面讲过,当前程序发布的事件为环境准备就绪事件ApplicationEnvironmentPreparedEvent,所以显然我们将目光放在onApplicationEnvironmentPreparedEvent()方法上来处理该事件。

在该处理方法中,根据当前事件中的bootstrapContext属性(也就是启动程序上下文)获取到对应的环境后处理器

我们简单看一下如何获取环境后处理器的,进入getEnvironmentPostProcessors()方法

getEnvironmentPostProcessors()方法.png

从该方法中可以看到,它的实现是通过工厂模式从环境后处理器工厂获取到环境后处理器的。但是我们这一路走来,并不知道该工厂中有哪些后处理器,甚至该工厂是在什么时候实例化的都不知道。

首先我们看一下当前监听器EnvironmentPostProcessorApplicationListener的构造方法,从构造方法中寻找答案

环境后处理器的spring应用监听器.png

答案已经揭晓,环境后处理器工厂对象的实例化是在此监听器的构造方法中完成的,它通过环境后处理器工厂EnvironmentPostProcessorsFactory的静态方法fromSpringFactories()实例化。

从spring.factories文件中获取环境后处理器.png

另外,环境后处理器工厂EnvironmentPostProcessorsFactory在这里的实现类使用的是ReflectionEnvironmentPostProcessorsFactory,该实现类通过classNames属性保存着spring.factories文件中的所有环境后处理器的类路径,当需要从该工厂中获取环境后处理器时,该工厂通过反射获取环境后处理器的实例

回到正题,我们需要知道根据当前事件中的bootstrapContext属性(也就是启动程序上下文)获取到对应的环境后处理器有哪些,打断点进行代码调试,如下

获取到的环境后处理器集合.png

由此可见,对启动环境的处理可不止是从配置文件中获取配置这么简单,springboot对环境的处理又细分为这么多种:

  • RandomValuePropertySourceEnvironmentPostProcessor:在环境中添加随机数的配置信息

  • SystemEnvironmentPropertySourceEnvironmentPostProcessor:将环境中以保存的系统环境变量相关的属性进行替换,将原本保存环境变量的SystemEnvironmentPropertySource实例替换成其子类OriginAwareSystemEnvironmentPropertySource

  • SpringApplicationJsonEnvironmentPostProcessor:将当前环境中已经保存的属性集合中出现的key为spring.application.jsonSPRING_APPLICATION_JSONvalue为json字符串的属性转换成map形式

  • CloudFoundryVcapEnvironmentPostProcessor:与远程配置中心相关。我们可以理解为从远程配置中心读取配置

  • ConfigDataEnvironmentPostProcessor:与配置数据相关。该处理器专门负责读取各个位置的配置文件中的配置信息。其实在springboot中,有另一个处理器ConfigFileApplicationListener(配置文件监听器),两者的作用相同,但是后者被springboot打上了@Deprecated,说明被启用了,想必从命名上来看后者是一个监听器,相比之下前者更适合。以下为后者配置文件监听器的注释

    // 从springboot2.4.0版本开始,
    // 弃用ConfigFileApplicationListener,
    // 使用ConfigDataEnvironmentPostProcessor
    Deprecated since 2.4.0 in favor of ConfigDataEnvironmentPostProcessor
    

接下来我们在来看ConfigDataEnvironmentPostProcessor(配置数据环境后处理器)postProcessEnvironment()方法实现

ConfigData环境后处理器的postProcessEnvironment()方法.png

从该方法的源码中,我们发现它也没做什么特别重要的事,也没有对配置文件做出什么动作。

其实就只有三件事:

  • 初始化一个资源加载器。很明显,它用来加载resources目录下的配置文件资源。
  • 创建一个ConfigDataEnvironment对象
  • 调用ConfigDataEnvironment对象的processAndApply()方法

所以,我们把目光再次转向ConfigDataEnvironment类。该类有几个非常熟悉的常量,如下所示

ConfigDataEnvironment的配置文件地址常量.png

从这几个常量中我们可以肯定,ConfigDataEnvironment类就是负责读取配置文件中的配置信息的类了。应该是重中之重了吧。

所以我们应当分两步分析此类:①分析构造函数。②分析processAndApply()方法

  • 构造函数

    ConfigDataEnvironment构造器.png

该构造方法对一大堆的属性进行了初始化(如上图所示),我们对其中两个属性的初始化做一个简单了解

  • binder属性:包含环境中的所有配置信息

    在上图中大致介绍了该属性的作用,下面我们看一下Binder.get()方法的源码

    实例化Binder对象.png

从上面源码中,我们可以看到,这里的binder对象是通过Binder中的静态方法get()以当前环境为参数去创建Binder实例的。Binder实例中包含了当前环境中key为configurationProperties的属性(其实就是所有的属性),以及解析以${}placeholder的属性的解析器。

所以,在这里我们初步知道PropertySourcesPlaceholdersResolver是一个用来处理下图中firstName配置的解析器

配置文件中的placeholder.png

因此,binder属性中包含了以下成分:

① 当前环境中的所有配置信息

② 处理配置文件中placeholder解析器

  • resolvers属性:配置文件位置解析器集合

    在初始化该属性时,我们看到是通过调用createConfigDataLocationResolvers()方法完成的。从方法名也可以知道,resolvers属性是各种配置文件位置的解析器(用来解析文件位置)。看一下该方法的实现:

    实例化配置文件位置解析器的过程.png

下面我们就来看一下通过该方法获得的配置文件位置解析器的实例有哪些

实例化的配置文件位置解析器.png

这里我们简单介绍一下标准配置文件解析器StandardConfigDataLocationResolver

该解析器内部维护了两个属性:

    static final String CONFIG_NAME_PROPERTY = "spring.config.name";
    private static final String[] DEFAULT_CONFIG_NAMES = {
   
    "application" };
    private final List<PropertySourceLoader> propertySourceLoaders;
  • spring.config.name:表示我们指定配置文件的名称

  • application默认的配置文件名称

  • propertySourceLoaders:配置属性加载器。有两种加载器:PropertiesPropertySourceLoaderYamlPropertySourceLoader,分别从properties和xmlyml和yaml两种类型的配置文件中读取配置属性。

处理properties或yml类型文件的配置属性加载器.png

该解析器的功能就是根据传入的路径加上配置文件名称,并结合其配置属性加载器。得到确定的配置文件资源。如:传入路径classpath:/,该解析器将返回四个对应的配置文件资源classpath:/application.peropertiesclasspath:/application.xmlclasspath:/application.yamlclasspath:/application.yml

  • loaders属性:配置文件解析器集合

    配置文件解析器通过直接创建ConfigDataLoaders实例完成初始化,在该类的构造方法中,与上面resolvers属性的初始化逻辑相同,也是从META-INF/spring.factories文件中获取配置文件解析器的集合,然后对其进行实例化。

    初始化配置文件加载器的过程.png

那我们看一下配置文件加载器都有哪些实现类:

实例化的配置文件加载器集合.png

看到这里我们应该对resolvers属性和loaders属性之间的关系有个了解了:

① resolvers属性用来主要用来解析配置文件所在的目录位置。解析目录获取配置文件

② loaders属性用来加载从目录中获取到的配置文件。即从配置文件中加载配置信息

③ 两种解析器与两种加载器是存在对应关系的。树形解析器对应树形加载器标准解析器对应标准加载器

  • contributors属性:配置信息贡献者集合,每个贡献者提供不同的信息,可能是已收集的配置信息如环境变量、也可能是配置文件的信息。

    该属性是通过createContributors()方法进行初始化的。该方法将当前环境中的配置属性指定的配置文件路径封装到ConfigDataEnvironmentContributors对象中

    封装配置属性和配置文件路径集合.png

由于springboot对ConfigDataEnvironmentContributors的封装和该方法的实现逻辑过于复杂,为了弄清楚该实现,我们将对其进行细化的分析。

1、首先,第一行代码我们无需过多分析,就是获取当前启动环境中已经收集到的配置信息。如系统属性、环境变量、随机变量。这些在前面分析过了。

2、然后看DefaultPropertiesPropertySource类的静态方法hasMatchingName()。在遍历配置信息时将其中key为defaultProperties默认属性找出来,对其进行单独处理。

hasMatchingName()方法判断当前配置信息的key为defaultProperties.png

3、ConfigDataEnvironmentContributor类的静态方法ofExisting()

通过提供静态方法ofExisting()来创建一个ConfigDataEnvironmentContributor类的实例,并且该实例被标记为EXISTING。在此过程中,还会将传入的propertySource对象转为configurationPropertySource对象

将propertySource转为标记为existing类型的contributor对象.png

在看一下ConfigDataEnvironmentContributor类的构造方法如下

被标记为existing的contributor实例.png

4、通过for循环遍历下来,经过我们的分析,不难发现该循环的目的是通过遍历propertySource数组,将该数组转化为contributor数组

将propertySource数组转化为contributor数组的for循环.png

转换过程的示意图如下所示

将propertySource数组转化为contributor数组的过程.png

至此,springboot将最初获取到的配置信息(如:系统属性、命令行参数等)添加到contributor中去了。

5、将配置文件基本信息添加到contributor中。

createContributors()方法中,还有一个很重要的逻辑,即将配置文件的基本信息添加到contributor中,配置文件的基本信息其实指的就是配置文件的路径配置。例如我们常用的classpath:/classpath:/config/file:./config/等配置。

getInitialImportConttributors()方法.png

springboot对上面的常量定义如下:

    static final String IMPORT_PROPERTY = "spring.config.import";
    private static final ConfigDataLocation[] EMPTY_LOCATIONS = new ConfigDataLocation[0];

    static final String ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";

    static final String LOCATION_PROPERTY = "spring.config.location";
    static final ConfigDataLocation[] DEFAULT_SEARCH_LOCATIONS;
    static {
   
   
        List<ConfigDataLocation> locations = new ArrayList<>();
        locations.add(ConfigDataLocation.of("optional:classpath:/"));
        locations.add(ConfigDataLocation.of("optional:classpath:/config/"));
        locations.add(ConfigDataLocation.of("optional:file:./"));
        locations.add(ConfigDataLocation.of("optional:file:./config/"));
        locations.add(ConfigDataLocation.of("optional:file:./config/*/"));
        DEFAULT_SEARCH_LOCATIONS = locations.toArray(new ConfigDataLocation[0]);
    }

在上面截图的方法中,我们看到,getInitialImportContributors()方法调用了三次bindLocations()方法,并将每一次bindLocations()方法的返回值添加到initialContributors集合中。

我们通过断点查看这三个bindLocations()方法的调用返回给我们什么东西

第一次调用该方法的结果如下所示。

bindLocations()方法调用1.png

我们将变量进行替换后,该方法的调用其实是下面这样的

    bindLocations(binder, "spring.config.import", new ConfigDataLocation[0])

所以我们猜测它的目的就是将spring.config.import配置的值转化为ConfigDataLocation类型的数组。而我们的演示没有对其进行配置,所以使用new ConfigDataLocation[0]作为兜底进行返回,得到的结果是一个ConfigDataLocation类型的空数组

第二次调用该方法的结果如下所示。

bindLocations()方法调用2.png

我们将变量进行替换后,该方法的调用其实是下面这样的

    bindLocations(binder, "spring.config.additional-location", new ConfigDataLocation[0])

所以我们猜测它的目的就是将spring.config.additional-location配置的值转化为ConfigDataLocation类型的数组。而我们的演示没有对其进行配置,所以使用new ConfigDataLocation[0]作为兜底进行返回,得到的结果同样也是一个ConfigDataLocation类型的空数组

第三次调用该方法的结果如下所示。

bindLocations()方法调用3.png

我们将变量进行替换后,该方法的调用其实是下面这样的

    bindLocations(binder, "spring.config.location", DEFAULT_SEARCH_LOCATIONS);

    //DEFAULT_SEARCH_LOCATIONS表示默认查找位置,如果没有配置spring.config.location,就使用 DEFAULT_SEARCH_LOCATIONS 作为兜底

通过上面三次对bindLocations()方法的调用,我们得到了三个ConfigDataLocation类型的数组,然后在将这三个数组逐个添加到contributor集合中

将配置文件路径封装到contributor并标记为initail_import.png

前面我们分析过,将系统变量封装为contributor实例时是通过ConfigDataEnvironmentContributor类的静态方法ofExisting()标记为EXISTING的,即表示已存在的属性。那么在这里针对配置文件路径的contributor,则是通过另一个静态方法ofInitialImport()标记为INITIAL_IMPORT的,即表示初始导入的属性,它只能表示配置文件的位置,我们后面还需要通过该位置去找到对应的配置文件并读取其中的配置。

所以,又分析了这么大一堆,我们明白了getInitialImportContributors()方法的作用,就是将配置文件的位置转换成对应的contributor集合。如下图所示

配置文件位置的contributor集合.png

再回到createContributors()方法中,该方法将我们根据系统变量转化的contributor集合根据配置文件位置转化的contributor集合进行合并

createContributors()方法合并两种contributor集合.png

现在我们得到的contributor集合如下所示

合并后的contributor集合.png

6、在得到contributor集合后,springboot还对该集合进行再次封装,将该集合封装到ConfigDataEnvironmentContributors对象中,并通过rootchildren将其封装成一个树形的结构。

将contributor集合封装到树形结构中.png

首先我们看到在静态方法of()中,将枚举BEFORE_PROFILE_ACTIVATION作为key,参数contributor集合作为value封装到一个map对象中,然后将该map对象作为children属性传递到ConfigDataEnvironmentContributor()构造方法中,此时得到了一个新的被标记为ROOT的contributor对象中。该对象的结构如下所示

contributor集合作为children属性,并被标记为ROOT的contributor对象.png

然后将该contributor对象作为root属性保存到ConfigDataEnvironmentContributors对象中,在这里注意区分两个类的区别:

  • ConfigDataEnvironmentContributor:保存配置信息或配置文件位置的contributor类
  • ConfigDataEnvironmentContributors:保存ConfigDataEnvironmentContributor的集合

此时ConfigDataEnvironmentContributors对象的结构如下所示

contributors结构.png

综上所述,我们对ConfigDataEnvironment构造方法做一个小总结

1、binder属性:当前环境中的所有配置信息, 处理配置文件中placeholder解析器

2、resolvers属性:配置文件位置解析器集合

3、loaders属性:配置文件解析器集合

4、contributors属性:保存了profile生效前(BEFORE_PROFILE_ACTIVATION)的属性配置(如:系统属性、环境变量、配置文件位置)。

  • processAndApply()方法

    该方法是从配置文件中读取配置的核心方法,在前面环境后处理器部分中,调用的就是ConfigDataEnvironmentprocessAndApply()方法。

    processAndApply()方法.png

  void processAndApply() {
   
   
      // 创建配置数据导入器,该导入器中的加载器loaders用来读取配置文件中的数据,然后由导入器将数据保存到contributors中
      ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers,
                                                           this.loaders);

      registerBootstrapBinder(this.contributors, null, DENY_INACTIVE_BINDING);

      // 加载默认指定配置目录中的文件名为application的配置文件中的配置信息,保存到当前contributor的children中,并标记为bound_import
      ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);

      // 创建profile上下文,用来保存当前项目激活的profile
      ConfigDataActivationContext activationContext = createActivationContext(
          contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE));

      // 在不考虑profile的情况下,将第二步未处理的contributor进行处理,同样保存到contributor的children中,并标记为bound_import
      contributors = processWithoutProfiles(contributors, importer, activationContext);

      // 确定当前激活的profile,并保存到profile上下文中
      activationContext = withProfiles(contributors, activationContext);

      // 根据已确定的profile,加载默认指定配置目录中对应当前profile的配置文件中的配置信息,保存到当前contributor的children中,并标记为bound_import
      contributors = processWithProfiles(contributors, importer, activationContext);

      // 将contributor中全部标记为bound_import的配置属性保存到运行环境environment中
      applyToEnvironment(contributors, activationContext);
  }

在该方法中,主要有以下部分逻辑,

  • 从默认指定的配置文件中读取配置信息并保存到contributors中。通过调用processInitial()方法完成。

    默认的配置文件路径前面ConfigDataEnvironment类中已经介绍过,分别是:classpath:/classpath:/config/file:./file:./config/file:./config/*/。以此5个默认的路径、默认的配置文件名称application和默认的配置文件格式properties、xml、yaml、yml为参数,分别获取其对应的配置文件资源。并从中读取配置属性。

    然后将读取到的配置属性封装为contributor对象并标记为BEFORE_PROFILE_ACTIVATION,再将该contributor对象保存到其父contributor对象的children集合中。

    以下图的contributors的结构为例:当处理右边第一个以optional:classpath:/为属性的contributor对象时,我们发现该对象的children属性为空,此时从classpath:/路径下以application命名的配置文件中读取配置并将配置信息封装为contributor对象,再将该对象标记为BEFORE_PROFILE_ACTIVATION并保存到上一层的children属性中。

    由此看出,springboot非常巧妙地利用父子层级的关系来分别表示以配置文件路径为属性的contributor封装着从配置文件中获取的配置信息的contributor

    contributors结构.png

  • 初始化环境激活上下文即ConfigDataActivationContext。通过调用createActivationContext()方法完成。

  • 在忽略环境激活上下文的情况下处理当前已收集的contributors。通过调用processWithoutProfiles()方法完成。

  • 确定当前运行环境激活的profile。通过调用withProfiles()方法完成。

    在前面springboot已经把所有位置上以application为名称的配置文件中的配置属性读取并保存到contributors中了。此时只需要再从中获取spring.profiles.active对应的属性,并将获取到的profiles属性保存到环境激活上下文中。

  • 根据已确定的环境激活上下文读取对应的配置信息到contributors中。通过调用processWithProfiles()方法完成。

    再一次根据默认的配置文件路径classpath:/classpath:/config/file:./file:./config/file:./config/*/。以5个默认的路径、默认的配置文件名称application后拼接-和激活的profiles,以及默认的配置文件格式properties、xml、yaml、yml为参数,再次分别获取其对应的配置文件资源。并从中读取配置属性。例如默认配置文件中spring.profiles.active = dev,则此时再次从application-dev.yml中读取配置。

    然后将读取到的配置属性封装为contributor对象并标记为AFTER_PROFILE_ACTIVATION(因为该对象是根据激活的profiles得到的),再将该contributor对象保存到其父contributor对象的children集合中。

    最后再以父子层级的关系将该contributor对象保存到其父级的children属性中。

  • 将contributors中保存的所有配置属性应用到当前运行环境Environment中。

我们从上述的介绍中挑选几个主要部分进行分析。

2. 对contributors中的配置属性进行处理

上面对processAndApply()方法的分析中,概括来讲就是分四步:①在确定profiles前处理contributors中的配置属性。②确定profiles。③在确定profiles后处理contributors中的配置属性。④将contributors中的配置属性应用到当前运行环境Environment中。

因此下面我们来分析以下逻辑

  • 处理contributors中的配置属性
  • 确定profiles
  • 将配置属性应用到当前运行环境Environment中。

3. 处理contributors中的配置属性

该逻辑通过三个方法完成,分别是processInitial()processWithoutProfiles()processWithProfiles()。而这三个方法其实内部实现都是通过调用contributors对象的withProcessedImports()方法完成的,他们之间的区别就是是否传入确定的profiles

处理contributors对象中的配置属性.png

因此我们主要对withProcessedImports()方法进行分析。

先来看一下该方法的源码如下:

ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter importer,
            ConfigDataActivationContext activationContext) {
   
   
    // 根据环境激活上下文获取导入阶段,所谓导入阶段为profiles激活前和profiles激活后两个阶段
    ImportPhase importPhase = ImportPhase.get(activationContext);
    this.logger.trace(LogMessage.format("Processing imports for phase %s. %s", importPhase,
                                        (activationContext != null) ? activationContext : "no activation context"));
    // this表示contributors表示的对象,将该对象赋值给result,本质上仍然是保存contributor集合的contributors对象
    ConfigDataEnvironmentContributors result = this;
    int processed = 0;
    while (true) {
   
   
        // 从contributor集合中获取下一个将要处理的contributor
        ConfigDataEnvironmentContributor contributor = getNextToProcess(result, activationContext, importPhase);
        if (contributor == null) {
   
   
            // 如果contributor集合中没有要处理的元素,则返回该集合
            this.logger.trace(LogMessage.format("Processed imports for of %d contributors", processed));
            return result;
        }
        if (contributor.getKind() == Kind.UNBOUND_IMPORT) {
   
   
            // 如果contributor的类型为UNBOUND_IMPORT(未绑定导入)

            // 从当前正处理的contributor对象中获取ConfigurationPropertySource
            Iterable<ConfigurationPropertySource> sources = Collections
                .singleton(contributor.getConfigurationPropertySource());
            // 创建placeholder解析器,用来解析${}
            PlaceholdersResolver placeholdersResolver = new ConfigDataEnvironmentContributorPlaceholdersResolver(
                result, activationContext, true);
            // 创建binder对象,binder中包含了ConfigurationPropertySource解析器
            Binder binder = new Binder(sources, placeholdersResolver, null, null, null);
            // 将当前contributor复制给一个新的对象,并将类型修改为BOUND_IMPORT(已绑定导入)
            ConfigDataEnvironmentContributor bound = contributor.withBoundProperties(binder);
            // 根据原contributor集合重新创建一个Contributors对象,并将当前正处理的contributor对象进行替换
            result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
                                                           result.getRoot().withReplacement(contributor, bound));
            continue;
        }
        // 创建位置解析器上下文
        ConfigDataLocationResolverContext locationResolverContext = new ContributorConfigDataLocationResolverContext(
            result, contributor, activationContext);
        // 创建配置数据加载器上下文
        ConfigDataLoaderContext loaderContext = new ContributorDataLoaderContext(this);
        // 从contributor中获取imports,imports中包含了当前contributor对象要处理的配置文件路径
        List<ConfigDataLocation> imports = contributor.getImports();
        this.logger.trace(LogMessage.format("Processing imports %s", imports));
        // 调用importer的resolveAndLoad()方法来解析并读取配置数据
        // 在importer中已经包含了位置解析器、配置数据加载器。
        // 返回值是一个map对象,其中key中包含了配置文件的路径及其资源,value中包含的是配置文件中的配置数据
        Map<ConfigDataResolutionResult, ConfigData> imported = importer.resolveAndLoad(activationContext,
                                                                                          locationResolverContext, 
                                                                                       loaderContext, 
                                                                                       imports);
        this.logger.trace(LogMessage.of(() -> getImportedMessage(imported.keySet())));
        // 将读取到的配置通过父子关系,设置为当前正处理的contributor的children,
        // asContributors()方法将读取到的配置封装成contributor对象并设置其类型为UNBOUND_IMPORT(未绑定导入)
        ConfigDataEnvironmentContributor contributorAndChildren = contributor.withChildren(importPhase,
                                                                                           asContributors(imported));
        // 根据原contributor集合重新创建一个Contributors对象,并将当前正处理的contributor对象进行替换
        result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
                                                       result.getRoot().withReplacement(contributor, contributorAndChildren));
        processed++;
    }
}

其中,在contributor类型为UNBOUND_IMPORT的if代码块中,主要是把该contributor的类型修改为BOUND_IMPORT,并作为一个新的contributor对象将原contributor对象进行替换。如下所示

将contributor对象的类型进行修改.png

下面,我们分析withProcessedImports()方法剩余部分逻辑:

withProcessedImports方法的主要逻辑.png

其中,我们先进入importer.resolveAndLoad()方法,该方法返回一个map对象(key为配置文件资源对象,value为从配置文件资源中加载的配置属性),其内部逻辑分两部分:①解析配置文件位置和资源,②加载配置属性

resolveAndLoad()方法.png

1) 解析配置文件位置和资源

我们进入resolve()方法查看如何解析配置文件的位置和资源。

对resolve()方法的调用.png

从上面源码中可以看到,resolve()方法对其locations参数(配置文件位置)进行遍历,对每一个配置文件位置再调用重载的resolve()方法进行解析。真正的解析过程是通过调用配置文件位置解析器resolversresolve()方法实现的。

调用配置文件位置解析器的resolve()方法.png

前面讲过,springboot提供了两个配置文件解析器:①ConfigTreeConfigDataLocationResolver;②StandardConfigDataLocationResolver。我们从这两个解析器的isResolvable()方法便可以判断出区别:前者用于解析带有前缀configtree:的配置文件路径;后者解析任意配置文件路径。

下面我们以StandardConfigDataLocationResolver为例,分析如何解析配置文件位置。

其中resolve()方法和resolveProfileSpecific()方法逻辑大致相同,只是后者携带有效的profile参数

因此我们分析其resolve()方法,该方法先获取配置文件资源的引用再根据该文件引用,获取该文件资源

获取配置文件资源的引用1.png

下面我们看如何获取配置文件资源的引用,以目录为例,查看getReferencesForDirectory()方法

获取配置文件资源的引用.png

进一步查看配置文件引用的构造方法

配置文件资源引用的构造方法.png

由此我们便可以知道,springboot是如何根据spring.profiles.active属性确定profiles对应的配置文件资源

当我们得到配置文件资源的引用后,通过该引用获取对应的配置文件资源

解析配置文件资源的引用得到文件资源.png

我们再回到配置文件位置解析器resolversresolve()方法。

将配置文件资源封装到解析结果中.png

下面我们回到resolveAndLoad()方法,其中resolved集合中包含了默认指定的以及指定profile对应的配置文件资源。然后在调用load()方法,从配置文件资源中加载配置属性即可。

resolveAndLoad()方法.png

2) 加载配置属性

这里我们关注resolveAndLoad()方法对load()方法的调用。

该方法以配置文件资源分析结果集合为参数,返回一个Map集合,其中key为配置文件资源分析结果,value为配置文件资源中的配置属性。

根据配置文件资源分析结果加载配置.png

进入加载器loadersload()方法,该方法用于加载指定配置文件资源中并返回该配置文件中的配置属性

调用配置属性加载器的load方法.png

从该方法中看到,配置属性加载器有两种,分别是ConfigTreeConfigDataLoaderStandardConfigDataLoader,还记得前面我们分析的配置文件位置解析器也有两个分别是ConfigTreeConfigDataLocationResolverStandardConfigDataLocationResolver,他们是一一对应的。

我们以StandardConfigDataLocationResolver为例,查看它的load()方法。

调用加载yaml或properties的load方法.png

从该方法中我们看到,对配置文件中配置属性的加载是通过配置属性加载器中的load()方法实现的,而该加载器又分为propertiesyaml两种。

load()方法将我们在配置文件中定义的配置属性进行加载,并转化为propertySource集合。再将该集合封装到ConfigData对象中并返回。

而该加载器中对配置文件资源中的配置属性的加载过程我们这里就不做分析了,请有兴趣的读者自查。

最后再回到withProcessedImports()方法

withProcessedImports方法的主要逻辑.png

resolveAndLoad()方法我们就分析结束了,该方法返回的imported对象为map集合,其中key为配置文件资源分析结果,value为配置文件资源中的配置属性。最后通过withChildren()方法将该map集合转为contributor对象并保存到children属性中,再通过withReplacement()方法将contributor对象更新

至此我们在配置文件中定义的所有配置属性均已保存到contributors对象中并返回。此时contributors对象的结构如下

contributors最终结构.png

4. 确定当前运行环境激活的profile

此过程由processAndApply()方法中调用withProfiles()方法完成

processAndApply方法调用withProfiles.png

下面我们进入该方法源码查看

withProfiles方法.png

1) 获取附加的addtionalProfiles

从源码上看,addtionalProfiles属性是在ConfigDataEnvironment类的成员变量中直接定义的,且该属性是通过该类的构造方法设置的

class ConfigDataEnvironment {
   
   
    // ...
    private final Collection<String> additionalProfiles;    
    // ...

    ConfigDataEnvironment(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
            ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles,
            ConfigDataEnvironmentUpdateListener environmentUpdateListener) {
   
   
        // ...
        this.additionalProfiles = additionalProfiles;
        // ...
    }
}

通过该构造方法的调用链我们可以发现,该构造方法的调用如下所示

ConfigDataEnvironment构造方法的调用链.png

由此可知,附加profiles是从SpringApplication类中获取的,那么是否也是由SpringApplication类设置的呢?答案是肯定的。

SpringApplication类中有对应的方法定义

SpringApplication设置addtionalProfiles.png

因此我们可以在springboot的主启动方法中通过以下方式设置

设置addtionalProfiles示例.png

2) 获取spring.profiles.include定义的配置

进入getIncludedProfiles()方法

获取include指定的profiles.png

从该方法中看到,对contributors中的contributor进行遍历,从中获取key为spring.profiles.include的配置属性,将其添加到集合中并返回。

3) 获取spring.profiles.active定义的配置

进入Profiles的构造方法查看,

Profiles的构造方法.png

该构造方法中定义了三种profiles,分别是spring.profiles.group定义的profiles、spring.profiles.active定义的profiles和spring.profiles.default定义的profiles。我们逐个查看

  • spring.profiles.group定义的profiles

    Profiles的构造方法得知,springboot通过spring.profiles.group定义profiles分组,且定义方式为Map集合。我们通过下面示例说明

    配置文件中定义分组profiles.png

调式源码

配置文件设置分组profiles调式结果.png

  • spring.profiles.active定义的profiles

    Profiles的构造方法得知,springboot通过getActivatedProfiles()方法获取spring.profiles.active定义的profiles

    获取active定义的profiles.png

getActivatedProfiles()方法中,springboot获取spring.profiles.active定义的profiles,并将前面获取的additionalProfiles一同添加到集合中并返回,作为Profiles实例的activeProfiles属性。

  • spring.profiles.default定义的profiles

    Profiles的构造方法得知,springboot通过getDefaultProfiles()方法获取spring.profiles.default定义的profiles

    获取default定义的profiles.png

在`getDefaultProfiles()`方法中,springboot获取`spring.profiles.default`定义的profiles(默认为`default`),并将其添加到集合中并返回,作为`Profiles`实例的`defaultProfiles`属性。

最后,在将所有定义的profiles封装到Profiles实例后,通过activationContext.withProfiles()方法将该Profiles实例添加到profiles激活上下文中。

withProfiles方法.png

5. 将contributors中保存的配置信息应用到当前运行环境中

下面我们便到达processAndApply()方法的最后一步,将contributors中保存的配置信息应用到当前运行环境

processAndApply方法调用applyToEnvironment.png

下面进入applyToEnvironment方法的源码

将配置属性应用到当前运行环境中.png

从源码可见,该方法虽然较长,但逻辑比较简单,就是将contributor集合中来自配置文件的配置属性添加到当前运行环境的配置属性集合中,然后对当前运行环境设置profiles。

此时,运行环境中的所有配置属性均已设置完毕,包含来自系统的配置属性以及来自配置文件的配置属性等,如下所示

运行环境中保存的属性4.png




至此,我们对springboot创建并配置运行环境的整个过程就分析结束了,其过程虽然繁琐,但如果认真梳理,其处理逻辑并不复杂,只需我们在阅读源码时耐得住寂寞沉得住气即可。

七、总结

  • 在启动环境中主要保存配置信息和当前操作系统的配置信息以及环境变量
  • 针对不同的应用程序类型,springboot创建对应的运行环境实例,如StandardEnvironmentStandardServletEnvironmentStandardReactiveWebEnvironment
  • 在创建运行环境实例时,其构造器内部就已经首先将系统属性环境变量保存到其内部属性中了。
  • 通过观察者模式发布环境准备就绪事件,由监听该事件的各种监听器针对该事件进行不同的逻辑处理。
  • 涉及到的设计模式
    • 观察者模式:发布环境准备就绪事件,由对应的监听器执行逻辑
    • 工厂模式:环境后处理器工厂
  • 通过contributor对象临时保存所有配置文件中的配置属性
  • 配置文件的格式有多种,propertiesxmlyaml、以及yml
  • 配置文件的位置有多种,classpath:/classpath:/config/file:./file:./config/file:./config/*/、以及指定的位置spring.config.location
  • 最后将contributor对象集合中的配置属性再应用到运行环境中。




纸上得来终觉浅,绝知此事要躬行。

————————我是万万岁,我们下期再见————————

相关文章
|
5月前
|
Arthas Java 测试技术
Docker 环境中 Spring Boot 应用的 Arthas 故障排查与性能优化实战
Docker 环境中 Spring Boot 应用的 Arthas 故障排查与性能优化实战
|
2月前
|
XML 设计模式 Java
springboot创建并配置环境3 - 配置扩展属性(下)
springboot创建并配置环境3 - 配置扩展属性(下)
springboot创建并配置环境3 - 配置扩展属性(下)
|
2月前
|
XML JSON Java
springboot如何创建并配置环境3 - 配置扩展属性(上)
springboot如何创建并配置环境3 - 配置扩展属性(上)
springboot如何创建并配置环境3 - 配置扩展属性(上)
|
2月前
|
Java
springboot创建并配置环境2 - 配置基础环境
springboot创建并配置环境2 - 配置基础环境
springboot创建并配置环境2 - 配置基础环境
|
2月前
|
Java API uml
springboot创建并配置环境1 - 创建环境
springboot创建并配置环境1 - 创建环境
springboot创建并配置环境1 - 创建环境
|
2月前
|
Java Maven
idea中如何创建SpringBoot项目
idea中如何创建SpringBoot项目
|
3月前
|
Java Docker 容器
docker-compose部署一个springboot项目(包含环境)
docker-compose部署一个springboot项目(包含环境)
59 0
|
4月前
|
Java 数据处理 调度
论如何让Spring Boot在高压力环境下依然与众不同
论如何让Spring Boot在高压力环境下依然与众不同
|
5月前
|
Java 调度 Docker
Docker【应用 01】Spring Boot 项目部署在Linux环境下的Docker容器内举例(任务调度系统 xxl-job 任务调度中心)(手动版)
Docker【应用 01】Spring Boot 项目部署在Linux环境下的Docker容器内举例(任务调度系统 xxl-job 任务调度中心)(手动版)
80 0
|
5月前
|
XML 监控 Java
使用IDEA社区版如何创建SpringBoot项目?
使用IDEA社区版如何创建SpringBoot项目?