Spring Boot 源码分析(三):配置文件加载

Spring Boot项目的主类main方法执行了run方法后,整个项目就运行起来了,运行需要的配置肯定也是在run方法执行中加载使用的。Spring Boot官方文档 指出按下图位置去寻找配置文件application.properties或application.yml,序号越小优先级越高,优先级高的文件中的配置会覆盖优先级低的。

当然我们也可以在命令行参数中指定配置文件的位置和名称(不一定非要application):


properties 复制代码
 java -jar myproject.jar --spring.config.name=myproject
arduino 复制代码
 java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties

1. 启动流程中的事件监听

下面我们分析一下Spring Boot加载配置文件的源码。SpringApplicatino对象创建时会通过SPI加载事件监听器,我们研究的Spring Boot版本中有一个监听器ConfigFileApplicationListener,它就是负责加载配置文件的。前面说过SpringApplicatino对象执行run方法时,在创建容器前的准备阶段会获先取SpringApplicationRunListener,然后创建并配置环境。SpringApplicationRunListener的接口方法会贯穿run方法执行全流程,在创建并配置环境后,SpringApplicationRunListener的environmentPrepared方法执行:

java 复制代码
    private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {
        ConfigurableEnvironment environment = this.getOrCreateEnvironment();
        this.configureEnvironment((ConfigurableEnvironment)environment, applicationArguments.getSourceArgs());
        ConfigurationPropertySources.attach((Environment)environment);
        //配置文件加载发生在这里
        listeners.environmentPrepared((ConfigurableEnvironment)environment);
        this.bindToSpringApplication((ConfigurableEnvironment)environment);
        //...
    }

SpringApplicationRunListener的唯一实现类EventPublishingRunListener执行environmentPrepared时,会构造这个方法对应的事件ApplicationEnvironmentPreparedEvent,然后用它组合的广播器广播:

java 复制代码
public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
   
    private final SimpleApplicationEventMulticaster initialMulticaster;
	//...
    public void environmentPrepared(ConfigurableEnvironment environment) {
        this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
    }

广播方法multicastEvent:

java 复制代码
public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
    
    //...
    @Override
	public void multicastEvent(ApplicationEvent event) {
		multicastEvent(event, resolveDefaultEventType(event));
	}

	@Override
	public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
		ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
		Executor executor = getTaskExecutor();
		for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
			if (executor != null) {
				executor.execute(() -> invokeListener(listener, event));
			}
			else {
				invokeListener(listener, event);
			}
		}
	}
    
    //...
}

重载的multicastEvent方法会调用getApplicationListeners,这个方法会根据事件对象和事件类型去搜索对应的所有事件监听器:

获取到监听器后遍历,分别执行invokeListener方法,invokeListener会再调doInvokeListener:

2. 加载配置文件的监听器:ConfigFileApplicationListener

ConfigFileApplicationListener负责加载配置文件,遍历到它时会进入它的事件监听方法onApplicationEvent:

2.1 事件监听方法

当前事件是ApplicationEnvironmentPreparedEvent,所以会继续调用onApplicationEnvironmentPreparedEvent那个分支:

java 复制代码
public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
    
    private static final Set<String> LOAD_FILTERED_PROPERTY;
   
    //...
    private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        //SPI加载所有EnvironmentPostProcessor
        List<EnvironmentPostProcessor> postProcessors = this.loadPostProcessors();
        //ConfigFileApplicationListener也是EnvironmentPostProcessor的实现类
        postProcessors.add(this);
        AnnotationAwareOrderComparator.sort(postProcessors);
        Iterator var3 = postProcessors.iterator();

        while(var3.hasNext()) {
            EnvironmentPostProcessor postProcessor = (EnvironmentPostProcessor)var3.next();
            //调用每个EnvironmentPostProcessor后置处理方法
            postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
        }

    }

    List<EnvironmentPostProcessor> loadPostProcessors() {
        return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, this.getClass().getClassLoader());
    }
    
}    

onApplicationEnvironmentPreparedEvent会获取所有的环境后置处理器然后逐个调用对应的后置处理方法。先用SPI加载META-INF/spring.factories中配置的,再把ConfigFileApplicationListene本身也加入,因为它也实现了EnvironmentPostProcessor。

具体调用ConfigFileApplicationListener的postProcessEnvironment:

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

    protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
        RandomValuePropertySource.addToEnvironment(environment);
        (new ConfigFileApplicationListener.Loader(environment, resourceLoader)).load();
    }

最后创建ConfigFileApplicationListener的内部类Loader对象并调用它的load方法,这里就是加载配置文件具体逻辑。

2.2 加载配置文件

Loader类:

java 复制代码
private class Loader {
		Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
            //...
            //构造器中用SPI加载了配置的PropertySourceLoader
            this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, this.getClass().getClassLoader());
        }
    
        void load() {
            FilteredPropertySource.apply(this.environment, "defaultProperties", ConfigFileApplicationListener.LOAD_FILTERED_PROPERTY, (defaultProperties) -> {
                this.profiles = new LinkedList();
                this.processedProfiles = new LinkedList();
                this.activatedProfiles = false;
                this.loaded = new LinkedHashMap();
                this.initializeProfiles();

                while(!this.profiles.isEmpty()) {
                    ConfigFileApplicationListener.Profile profile = (ConfigFileApplicationListener.Profile)this.profiles.poll();
                    if (this.isDefaultProfile(profile)) {
                        this.addProfileToEnvironment(profile.getName());
                    }

                    this.load(profile, this::getPositiveProfileFilter, this.addToLoaded(MutablePropertySources::addLast, false));
                    this.processedProfiles.add(profile);
                }

                this.load((ConfigFileApplicationListener.Profile)null, this::getNegativeProfileFilter, this.addToLoaded(MutablePropertySources::addFirst, true));
                this.addLoadedPropertySources();
                this.applyActiveProfiles(defaultProperties);
            });
        }
    
    //...
2.2.1 FilteredPropertySource的apply方法

内部类Loader的load方法调用FilteredPropertySource的apply方法。apply方法的参数分别是从前面事件对象获取的environment、propertySource名称"defaultProperties"、Set集合LOAD_FILTERED_PROPERTY和函数式接口Consumer<PropertySource<?>>,Consumer接受一个参数进行处理返回void。apply方法是对环境中的某属性进行处理,如果能获取到则做替换然后执行Consumer的逻辑,否则直接执行Consumer。

java 复制代码
class FilteredPropertySource extends PropertySource<PropertySource<?>> {
    //...
    
    static void apply(ConfigurableEnvironment environment, String propertySourceName, Set<String> filteredProperties, Consumer<PropertySource<?>> operation) {
        MutablePropertySources propertySources = environment.getPropertySources();
        PropertySource<?> original = propertySources.get(propertySourceName);
        if (original == null) {
            operation.accept((Object)null);
        } else {
            propertySources.replace(propertySourceName, new FilteredPropertySource(original, filteredProperties));

            try {
                operation.accept(original);
            } finally {
                propertySources.replace(propertySourceName, original);
            }

        }
    }
}   

debug发现:

从环境中获取属性defaultProperties,没有获取到,就不做额外处理,直接把第四个参数那个函数式接口的逻辑拿来执行。

2.2.2 load方法的参数

在这里,FilteredPropertySource的apply方法会直接执行Consumer接口的逻辑,会执行到下面的地方:

重点看重载的load方法:

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

第一个参数Profile和环境有关。第二个参数DocumentFilterFactory是个接口,唯一的接口方法会根据profile返回DocumentFilter:

java 复制代码
    @FunctionalInterface
    private interface DocumentFilterFactory {
        ConfigFileApplicationListener.DocumentFilter getDocumentFilter(ConfigFileApplicationListener.Profile profile);
    }

具体传参时要传入接口实现类对象,这里采用匿名内部类创建对象,创建对象时必须覆盖接口方法getDocumentFilter。而覆盖的getDocumentFilter的方法体如果只有一行return语句,可以使用方法引用。可以举例复习一下这种语法:

java 复制代码
public interface I {
    Integer getVal(String str);
}

public class Client {
    public static void main(String[] args) {
        I i = new I() {
            @Override
            public Integer getVal(String str) {
                return Integer.valueOf(str);
            }
        };

        I i2 = Integer::valueOf;

        System.out.println("i.getVal(\"123\") = " + i.getVal("123"));
        System.out.println("i2.getVal(\"12\") = " + i2.getVal("12"));
    }
}

本地运行是可以的。

源码采用this::getPositiveProfileFilter:

java 复制代码
        private ConfigFileApplicationListener.DocumentFilter getPositiveProfileFilter(ConfigFileApplicationListener.Profile profile) {
            return (document) -> {
                if (profile == null) {
                    return ObjectUtils.isEmpty(document.getProfiles());
                } else {
                    return ObjectUtils.containsElement(document.getProfiles(), profile.getName()) && this.environment.acceptsProfiles(Profiles.of(document.getProfiles()));
                }
            };
        }

getPositiveProfileFilter接受profile并返回DocumentFilter,而DocumentFilter也是一个接口:

java 复制代码
    @FunctionalInterface
    private interface DocumentFilter {
        boolean match(ConfigFileApplicationListener.Document document);
    }

所以getPositiveProfileFilter返回时也要创建DocumentFilter实现类对象,也是匿名内部类对象,然后要覆盖match方法,由于这里的逻辑不只是一行语句,所以用了 <math xmlns="http://www.w3.org/1998/Math/MathML"> l a m b d a lambda </math>lambda表达式。这里的逻辑是过滤文档Document是否符合某种属性,文档是对配置文件的一种抽象:

java 复制代码
 private static class Document {
        private final PropertySource<?> propertySource;//和配置有关
        private String[] profiles;
        private final Set<ConfigFileApplicationListener.Profile> activeProfiles;
        private final Set<ConfigFileApplicationListener.Profile> includeProfiles;
     //...
 }

再来看load方法的第三个参数DocumentConsumer,它也是一个接口,只是要处理profile和document,不返回参数:

java 复制代码
    @FunctionalInterface
    private interface DocumentConsumer {
        void accept(ConfigFileApplicationListener.Profile profile, ConfigFileApplicationListener.Document document);
    }

源码用了this.addToLoaded(MutablePropertySources::addLast, false),这个方法也是同样的逻辑,返回一个 <math xmlns="http://www.w3.org/1998/Math/MathML"> l a m b d a lambda </math>lambda 表达式:

java 复制代码
private Map<ConfigFileApplicationListener.Profile, MutablePropertySources> loaded;

private ConfigFileApplicationListener.DocumentConsumer addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>> addMethod, boolean checkForExisting) {
            return (profile, document) -> {
                if (checkForExisting) {
                    Iterator var5 = this.loaded.values().iterator();

                    while(var5.hasNext()) {
                        MutablePropertySources merged = (MutablePropertySources)var5.next();
                        if (merged.contains(document.getPropertySource().getName())) {
                            return;
                        }
                    }
                }

                MutablePropertySources mergedx = (MutablePropertySources)this.loaded.computeIfAbsent(profile, (k) -> {
                    return new MutablePropertySources();
                });
                addMethod.accept(mergedx, document.getPropertySource());
            };
        }

先来看看addToLoaded的第一个参数BiConsumer,它是函数式接口,还是同样的套路!匿名内部类创建的对象要覆盖BiConsumer的accept方法,对MutablePropertySources类型参数和 PropertySource<?>类型参数加逻辑而不返回参数,又是一个可以采用方法引用的地方,所以源码用了MutablePropertySources::addLast,只对PropertySource类型参数处理了,MutablePropertySources类型参数弃之不理:

java 复制代码
public class MutablePropertySources implements PropertySources {
    private final List<PropertySource<?>> propertySourceList;
    
    //...
        public void addLast(PropertySource<?> propertySource) {
        synchronized(this.propertySourceList) {
            this.removeIfPresent(propertySource);
            this.propertySourceList.add(propertySource);
        }
    }
    //...
}

回头看this.addToLoaded(MutablePropertySources::addLast, false),checkForExisting是false,所以先根据profile获取一个属性源MutablePropertySources,然后从document获取propertySource,最后把这两个参数交给BiConsumer处理。从MutablePropertySources::addLast可知,这个addToLoaded方法其实就是要把从document得到的propertySource重新加入到属性列表propertySourceList中去,也就是从配置文件读配置然后维护到属性集中去。有点绕,花点时间debug一下就明白了。

2.2.3 load方法

返回看load的具体逻辑

java 复制代码
        private void load(ConfigFileApplicationListener.Profile profile, ConfigFileApplicationListener.DocumentFilterFactory filterFactory, ConfigFileApplicationListener.DocumentConsumer consumer) {
            //getSearchLocations调用后,再执行两重循环
            this.getSearchLocations().forEach((location) -> {
                boolean isDirectory = location.endsWith("/");
                Set<String> names = isDirectory ? this.getSearchNames() : ConfigFileApplicationListener.NO_SEARCH_NAMES;
                names.forEach((name) -> {
                    this.load(location, name, profile, filterFactory, consumer);
                });
            });
        }

先调getSearchLocations确定配置文件位置

java 复制代码
        private Set<String> getSearchLocations() {
            //从environment中获取spring.config.additional-location
            //这是配置文件的默认加载路径
            Set<String> locations = this.getSearchLocations("spring.config.additional-location");
            //如果指定了spring.config.location就添加它指定的路径
            if (this.environment.containsProperty("spring.config.location")) {
                locations.addAll(this.getSearchLocations("spring.config.location"));
            } else {
                //如果没有就添加这几个写死的路径返回
                locations.addAll(this.asResolvedSet(ConfigFileApplicationListener.this.searchLocations, "classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/"));
            }

            return locations;
        }

	//用逗号分割字符串形成集合并排序
      private Set<String> asResolvedSet(String value, String fallback) {
            List<String> list = Arrays.asList(StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(value != null ? this.environment.resolvePlaceholders(value) : fallback)));
            Collections.reverse(list);
            return new LinkedHashSet(list);
        }

获取文件位置后对每一个位置的文件处理,这里有双重循环,第一层确定配置文件名称:

java 复制代码
        private Set<String> getSearchNames() {
            //如果参数配置了spring.config.name就使用,否则用默认的"application"
            if (this.environment.containsProperty("spring.config.name")) {
                String property = this.environment.getProperty("spring.config.name");
                Set<String> names = this.asResolvedSet(property, (String)null);
                names.forEach(this::assertValidConfigName);
                return names;
            } else {
                return this.asResolvedSet(ConfigFileApplicationListener.this.names, "application");
            }
        }

第二重会遍历每一个配置文件,调用重载的load方法,参数location是目录名,name是文件名,其余几个都是之前传来的参数:

java 复制代码
private void load(String location, String name, ConfigFileApplicationListener.Profile profile, ConfigFileApplicationListener.DocumentFilterFactory filterFactory, ConfigFileApplicationListener.DocumentConsumer consumer) {
    		//if一般不走,除非配了spring.config.name但配的是空
            if (!StringUtils.hasText(name)) {
                Iterator var13 = this.propertySourceLoaders.iterator();

                PropertySourceLoader loader;
                do {
                    //...
                    loader = (PropertySourceLoader)var13.next();
                } while(!this.canLoadFileExtension(loader, location));

                this.load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
            } else {
                Set<String> processed = new HashSet();
                //获取SPI获取的所有PropertySourceLoader
                Iterator var7 = this.propertySourceLoaders.iterator();

                 //遍历所有的PropertySourceLoader
                while(var7.hasNext()) {
                    PropertySourceLoader loaderx = (PropertySourceLoader)var7.next();
                    String[] var9 = loaderx.getFileExtensions();
                    int var10 = var9.length;

                    //每一个PropertySourceLoader指定的配置文件扩展名继续遍历
                    for(int var11 = 0; var11 < var10; ++var11) {
                        String fileExtension = var9[var11];
                        if (processed.add(fileExtension)) {
                            //处理文件名称,(location + name) 会生成像 classpath:/application
                            this.loadForFileExtension(loaderx, location + name, "." + fileExtension, profile, filterFactory, consumer);
                        }
                    }
                }

            }
        }

用SPI加载所有PropertySourceLoader然后遍历,而PropertySourceLoader会指定文件扩展名,并真正加载解析这种文件。默认SPI获取PropertiesPropertySourceLoader和YamlPropertySourceLoader,PropertiesPropertySourceLoader可以加载扩展名是propertiesxml的文件,YamlPropertySourceLoader可以加载扩展名yml和yaml的文件。如果想加载json格式文件自定义PropertySourceLoader实现加载json文件的逻辑,然后配置到项目META-INF/spring.factories文件中去即可。

这里有处理文件名称的方法loadForFileExtension,它会得到配置文件全名然后继续调重载load方法:

java 复制代码
 private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension, 					ConfigFileApplicationListener.Profile profile,
	ConfigFileApplicationListener.DocumentFilterFactory filterFactory, 	
	ConfigFileApplicationListener.DocumentConsumer consumer) {
     		//文档过滤器工厂中取出过滤器
            ConfigFileApplicationListener.DocumentFilter defaultFilter = filterFactory.getDocumentFilter((ConfigFileApplicationListener.Profile)null);
            ConfigFileApplicationListener.DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
     //加载不带profile的配置文件 
     if (profile != null) {
         		//具体文件名,像classpath:/application-dev.properties这样
                String profileSpecificFile = prefix + "-" + profile + fileExtension;
                this.load(loader, profileSpecificFile, profile, defaultFilter, consumer);
                this.load(loader, profileSpecificFile, profile, profileFilter, consumer);
                Iterator var10 = this.processedProfiles.iterator();

         		//加载所有profile对应的配置文件
                while(var10.hasNext()) {
                    ConfigFileApplicationListener.Profile processedProfile = (ConfigFileApplicationListener.Profile)var10.next();
                    if (processedProfile != null) {
                        String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
                        this.load(loader, previouslyLoaded, profile, profileFilter, consumer);
                    }
                }
            }

     		//加载不带profile的配置文件
            this.load(loader, prefix + fileExtension, profile, profileFilter, consumer);
        }

最终每个文件都会调这个终级的load重载方法

java 复制代码
private void load(PropertySourceLoader loader, String location, 
	ConfigFileApplicationListener.Profile profile, 
    ConfigFileApplicationListener.DocumentFilter filter,
    ConfigFileApplicationListener.DocumentConsumer consumer) {
    		//调用Resource类到指定路径
            Resource[] resources = this.getResources(location);
            Resource[] var7 = resources;
            int var8 = resources.length;

            for(int var9 = 0; var9 < var8; ++var9) {
                Resource resource = var7[var9];

                try {
                    StringBuilder description;
                    if (resource != null && resource.exists()) {
                        if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
                           //...
                        } else if (resource.isFile() && this.isPatternLocation(location) && this.hasHiddenPathElement(resource)) {
                           //...
                        } else {
                            String name = "applicationConfig: [" + this.getLocationName(location, resource) + "]";
                            //读取配置文件内容,封装成文档类
                            List<ConfigFileApplicationListener.Document> documents = this.loadDocuments(loader, name, resource);
                            if (CollectionUtils.isEmpty(documents)) {
                                //...
                            } else {
                                List<ConfigFileApplicationListener.Document> loaded = new ArrayList();
                                Iterator var14 = documents.iterator();

                                while(var14.hasNext()) {
                                    ConfigFileApplicationListener.Document document = (ConfigFileApplicationListener.Document)var14.next();
                                    //filter参数用来过滤获取的文档是否满足profile
                                    if (filter.match(document)) {
                                        //将配置文件中配置的spring.profiles.active和
                        				//spring.profiles.include的值写入集合profiles中,
                        				//上层调用方法会读取profiles集合中的值,并读取对应的配置文件
                                        this.addActiveProfiles(document.getActiveProfiles());
                                        this.addIncludedProfiles(document.getIncludeProfiles());
                                        loaded.add(document);
                                    }
                                }

                                Collections.reverse(loaded);
                                if (!loaded.isEmpty()) {
                                    loaded.forEach((documentx) -> {
                                        //consumer接口处理
                                        consumer.accept(profile, documentx);
                                    });
                                    //...
                                }
                            }
                        }
                    } 
                    //...
            }

        }

读取配置文件内容,封装成文档类,如果文档满足profile条件就加入集合loaded。这里回头看第一个说到的getPositiveProfileFilter方法,它是用profile做条件的:

java 复制代码
        private ConfigFileApplicationListener.DocumentFilter getPositiveProfileFilter(ConfigFileApplicationListener.Profile profile) {
            return (document) -> {
                if (profile == null) {
                    return ObjectUtils.isEmpty(document.getProfiles());
                } else {
                    return ObjectUtils.containsElement(document.getProfiles(), profile.getName()) && this.environment.acceptsProfiles(Profiles.of(document.getProfiles()));
                }
            };
        }

loadDocuments读取配置文件内容,是用前面SPI加载的PropertySourceLoader实现的:

java 复制代码
        private List<ConfigFileApplicationListener.Document> loadDocuments(PropertySourceLoader loader, String name, Resource resource) throws IOException {
            ConfigFileApplicationListener.DocumentsCacheKey cacheKey = new ConfigFileApplicationListener.DocumentsCacheKey(loader, resource);
            List<ConfigFileApplicationListener.Document> documents = (List)this.loadDocumentsCache.get(cacheKey);
            if (documents == null) {
                //据不同的PropertySourceLoader调用相应的load方法
                List<PropertySource<?>> loaded = loader.load(name, resource);
                documents = this.asDocuments(loaded);
                this.loadDocumentsCache.put(cacheKey, documents);
            }

            return documents;
        }

以PropertySourceLoader为例:

java 复制代码
public class PropertiesPropertySourceLoader implements PropertySourceLoader {
    private static final String XML_FILE_EXTENSION = ".xml";

    public String[] getFileExtensions() {
        return new String[]{"properties", "xml"};
    }

    public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
        Map<String, ?> properties = this.loadProperties(resource);
        return properties.isEmpty() ? Collections.emptyList() : Collections.singletonList(new OriginTrackedMapPropertySource(name, Collections.unmodifiableMap(properties), true));
    }

    private Map<String, ?> loadProperties(Resource resource) throws IOException {
        String filename = resource.getFilename();
        //OriginTrackedPropertiesLoader的load方法会循环读取properties文件每行,每行用"="分割获取属性名和属性值
        return (Map)(filename != null && filename.endsWith(".xml") ? PropertiesLoaderUtils.loadProperties(resource) : (new OriginTrackedPropertiesLoader(resource)).load());
    }
}

PropertySourceLoader加载好配置文件后再设置回文档类,文档会交给consumer接口处理,就就是会回到前面说到l的this.addToLoaded(MutablePropertySources::addLast, false)中执行,逻辑闭环了。

3 加载JSON格式配置文件

分析完源码,我们可以来实现一下加载json配置文件,只要自定义PropertySourceLoader来实现加载json文件的逻辑,然后配到spring.factories文件。

1.定义普通类

java 复制代码
public class Developer {

    @Value("${dev.skill}")
    private String skill;

    public String getSkill() {
        return skill;
    }

    public void setSkill(String skill) {
        this.skill = skill;
    }
}

2.定义application.json,并注释掉application.properties文件中相同配置

json 复制代码
{
  "server.port": 9001,
  "dev.skill": "Java"
}

3.定义PropertySourceLoader实现类

java 复制代码
package org.cosmos.springboot.induction.prop;

public class JsonPropertySourceLoader implements PropertySourceLoader {
    @Override
    public String[] getFileExtensions() {
        return new String[]{"json"};
    }

    @Override
    public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
        ReadableByteChannel channel = resource.readableChannel();
        ByteBuffer buffer = ByteBuffer.allocate((int) resource.contentLength());
        channel.read(buffer);

        String content = new String(buffer.array());
        JSONObject jsonObject = com.alibaba.fastjson.JSON.parseObject(content);

        Map<String, Object> map = new HashMap<>(jsonObject.size());
        for (String key : jsonObject.keySet()) {
            map.put(key, jsonObject.getString(key));
        }
        return Collections.singletonList(new MapPropertySource("jsonSource", map));
    }
}

4.META-INF/spring.factories中添加


org.springframework.boot.env.PropertySourceLoader=org.cosmos.springboot.induction.prop.JsonPropertySourceLoader


5.定义主启动类并运行

java 复制代码
@SpringBootApplication
public class PropSpringApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext ctx = SpringApplication.run(PropSpringApplication.class, args);
        Developer developer = ctx.getBean(Developer.class);
        System.out.println("获取到Bean为:" + developer + ",属性skill值:" + developer.getSkill());
    }

    @Bean
    public Developer develop(){
        return new Developer();
    }
}

结果:

4. 总结

Spring Boot在启动流程中加载事件监听器ConfigFileApplicationListener并调用它的监听方法,监听方法会用SPI查找所有PropertySourceLoader并通过它们完成不同格式的文件加载,这也是一个扩展点,我们可以自定义加载其他格式的文件。

相关推荐
一个不秃头的 程序员17 分钟前
代码加入SFTP JAVA ---(小白篇3)
java·python·github
丁总学Java29 分钟前
--spring.profiles.active=prod
java·spring
上等猿36 分钟前
集合stream
java
java1234_小锋40 分钟前
MyBatis如何处理延迟加载?
java·开发语言
菠萝咕噜肉i41 分钟前
MyBatis是什么?为什么有全自动ORM框架还是MyBatis比较受欢迎?
java·mybatis·框架·半自动
林的快手1 小时前
209.长度最小的子数组
java·数据结构·数据库·python·算法·leetcode
向阳12181 小时前
mybatis 缓存
java·缓存·mybatis
上等猿2 小时前
函数式编程&Lambda表达式
java
蓝染-惣右介2 小时前
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
java·设计模式