『深入学习 Spring Boot』(十一) profiles

前言

这一小节,来学习 Spring Profiles。

Profiles 在我们日常工作中,很常用。一般我们用它来区分开发、测试、线上环境。

那么,Spring Boot 是怎么加载它的呢?

Profiles

默认情况

我们最常使用的就是application.yml了,我们知道 Spring Boot 会自动加载它。

其实,除了 application.yml 之外,Spring Boot 还自动会加载application-default.yml

ymlproperties 格式,Spring Boot 都是支持的。但是在相同情况下,properties优先于yml

另外,我们一般会使用active属性去激活某个配置文件,在此情况下,application-default就失效了。

加载过程

我们还是来关注在 SpringApplication#run()方法中,被调用的 prepareEnvironment方法。

    private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments) {
        // Create and configure the environment
        ConfigurableEnvironment environment = getOrCreateEnvironment();
        configureEnvironment(environment, applicationArguments.getSourceArgs());
        ConfigurationPropertySources.attach(environment);
        listeners.environmentPrepared(environment);
        bindToSpringApplication(environment);
        if (!this.isCustomEnvironment) {
            environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
                    deduceEnvironmentClass());
        }
        ConfigurationPropertySources.attach(environment);
        return environment;
    }
复制代码

经过我们 debug,在listeners.environmentPrepared(environment);之前,加载了这些属性源:

image-20210526220725163

发送事件之后的属性源:

image-20210526221316153

破案了破案了,我们的配置文件,就是在事件监听机制中被加载到内存中的。

还是经过 debug,我们找到了监听器 ConfigFileApplicationListener。下面我们主要就看监听器中怎么加载的配置文件。

ConfigFileApplicationListener

// ConfigFileApplicationListener # 172
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
        postProcessors.add(this);
        AnnotationAwareOrderComparator.sort(postProcessors);
        for (EnvironmentPostProcessor postProcessor : postProcessors) {
            postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
        }
    }
复制代码

这里主要就是循环调用postProcessor.postProcessEnvironment()方法:

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        addPropertySources(environment, application.getResourceLoader());
    }
复制代码

继续调用addPropertySources()方法:

    protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
        RandomValuePropertySource.addToEnvironment(environment);
        new Loader(environment, resourceLoader).load();
    }
复制代码

激动了,我们看到了关键字 Loader,我们继续进load()方法:

public void load() {
            this.profiles = new LinkedList<>();
            this.processedProfiles = new LinkedList<>();
            this.activatedProfiles = false;
            this.loaded = new LinkedHashMap<>();
            initializeProfiles();
            while (!this.profiles.isEmpty()) {
                Profile profile = this.profiles.poll();
                if (profile != null && !profile.isDefaultProfile()) {
                    addProfileToEnvironment(profile.getName());
                }
                load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
                this.processedProfiles.add(profile);
            }
            resetEnvironmentProfiles(this.processedProfiles);
            load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
            addLoadedPropertySources();
        }
复制代码

有点东西,估计这个就是加载配置文件的主要逻辑了。

前面几行逻辑都是初始化,我们从initializeProfiles()方法开始:

        private void initializeProfiles() {
            // The default profile for these purposes is represented as null. We add it
            // first so that it is processed first and has lowest priority.
      // 这里先添加一个null,和后面的 this.profiles.size() == 1 呼应。
            this.profiles.add(null);
      // 获取激活的配置属性
            Set<Profile> activatedViaProperty = getProfilesActivatedViaProperty();
            this.profiles.addAll(getOtherActiveProfiles(activatedViaProperty));
            // Any pre-existing active profiles set via property sources (e.g.
            // System properties) take precedence over those added in config files.
            addActiveProfiles(activatedViaProperty);
            if (this.profiles.size() == 1) { // only has null profile
                for (String defaultProfileName : this.environment.getDefaultProfiles()) {
                    Profile defaultProfile = new Profile(defaultProfileName, true);
                    this.profiles.add(defaultProfile);
                }
            }
        }
复制代码

有一点需要注意,当程序进行到这里的时候,我们配置文件还没被加载进来:

image-20210527193209061

所以这里,我们必然会进入到 if (this.profiles.size() == 1)的逻辑中,然后在此,加载了default Property。这就是为什么,Spring Boot 会自动加载 application-default.yml

image-20210527193850834

然后,程序继续执行:

while (!this.profiles.isEmpty()) {
         // 出栈
                Profile profile = this.profiles.poll();
         // null && default,profile.get(0) == null 不会进入这里
                if (profile != null && !profile.isDefaultProfile()) {
                    addProfileToEnvironment(profile.getName());
                }
         // 这里开始是主要逻辑,这里需要注意 addToLoaded(MutablePropertySources::addLast, false) 这个是 Consumer 函数参数。
                load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
                this.processedProfiles.add(profile);
            }
复制代码

我们进入load()

private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
            getSearchLocations().forEach((location) -> {
                boolean isFolder = location.endsWith("/");
                Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
                names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
            });
        }
复制代码

getSearchLocaltion() 就是加载配置文件存放的地点,默认的有四个:classpath:/,classpath:/config/,file:./,file:./config/ ;另外还会加载spring.config.locationspring.config.additional-location的值。

getSearchNames(),则是加载配置文件的前缀,我们使用的都是默认的application。此外还会加载spring.config.name属性的值,覆盖掉默认的application

最后会执行 names.forEach((name) -> load(location, name, profile, filterFactory, consumer)):

        private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,                DocumentConsumer consumer) {            ........            Set<String> processed = new HashSet<>();            for (PropertySourceLoader loader : this.propertySourceLoaders) {                for (String fileExtension : loader.getFileExtensions()) {                    if (processed.add(fileExtension)) {                        loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,                                consumer);                    }                }            }        }
复制代码

this.propertySourceLoaders就有意思了:

image-20210527201430726

这就是propertiesyml文件的加载器。

然后做了一下路径拼接,进入了loadForFileExtension()方法。我们定位到以prefix=classpath:/applicationfileExtension=.yml参数进入此方法体的逻辑:

private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
                Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
        ....
        .....
            // Also try the profile-specific section (if any) of the normal file
      // 我们会进入到这个方法中
            load(loader, prefix + fileExtension, profile, profileFilter, consumer);
        }
复制代码
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
                DocumentConsumer consumer) {
            try {
        // 加载文件,我们的配置文件被加载到内存中了
                Resource resource = this.resourceLoader.getResource(location);
                .......
                .......
         // 把配置文件(application.yml)包装成 Document
                String name = "applicationConfig: [" + location + "]";
                List<Document> documents = loadDocuments(loader, name, resource);
                ......
                List<Document> loaded = new ArrayList<>();
                for (Document document : documents) {
                    if (filter.match(document)) {
             //  加载配置文件中配置的 active、include,添加到profiles中
                        addActiveProfiles(document.getActiveProfiles());
                        addIncludedProfiles(document.getIncludeProfiles());
                        loaded.add(document);
                    }
                }
                Collections.reverse(loaded);
                if (!loaded.isEmpty()) {
           // 最后走到这个方法
                    loaded.forEach((document) -> consumer.accept(profile, document));
                    .......
                }
            }
            ......
        }
复制代码

loaded.forEach((document) -> consumer.accept(profile, document)) 中的 consumer.accept(profile, document),会执行下面这段逻辑:

private DocumentConsumer addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,
                boolean checkForExisting) {
            return (profile, document) -> {
                ......
                // 添加到 loaded (Map<Profile, MutablePropertySources>) 属性源中
                MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
                        (k) -> new MutablePropertySources());
                // 把解析到的属性添加到 MutablePropertySources(属性源) 的最后面
                addMethod.accept(merged, document.getPropertySource());
            };
        }
复制代码

以上就是profiles.get(0)=null时,所做的事情:在指定的位置,加载指定的配置文件,然后读取其中的属性,包括active、include,然后在把active、include的值,添加到 profiles 中,继续遍历。

全部遍历完成后,就是执行addLoadedPropertySources()方法了:

 private void addLoadedPropertySources() {
    MutablePropertySources destination = this.environment.getPropertySources();
    List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
    Collections.reverse(loaded);
    String lastAdded = null;
    Set<String> added = new HashSet<>();
    for (MutablePropertySources sources : loaded) {
        for (PropertySource<?> source : sources) {
            if (added.add(source.getName())) {
                addLoadedPropertySource(destination, lastAdded, source);
                lastAdded = source.getName();
            }
        }
    }
}
复制代码

这个方法就比较好理解了,把我们加载的配置文件属性源,都添加都属性源集合中:

image-20210527211230738

如此一来,我们配置的激活某环境下的配置文件,就生效了。

总结

这一小节,学习了我们日常开发中 profiles 的加载原理。

基本上每个项目我们都会用到 profiles ,但是却从来没有探究过。也算了小小地解惑了一次。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享