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并通过它们完成不同格式的文件加载,这也是一个扩展点,我们可以自定义加载其他格式的文件。

相关推荐
丶小鱼丶2 分钟前
并发编程之【优雅地结束线程的执行】
java
市场部需要一个软件开发岗位7 分钟前
JAVA开发常见安全问题:Cookie 中明文存储用户名、密码
android·java·安全
忆~遂愿11 分钟前
GE 引擎进阶:依赖图的原子性管理与异构算子协作调度
java·开发语言·人工智能
MZ_ZXD00116 分钟前
springboot旅游信息管理系统-计算机毕业设计源码21675
java·c++·vue.js·spring boot·python·django·php
PP东18 分钟前
Flowable学习(二)——Flowable概念学习
java·后端·学习·flowable
ManThink Technology23 分钟前
如何使用EBHelper 简化EdgeBus的代码编写?
java·前端·网络
invicinble27 分钟前
springboot的核心实现机制原理
java·spring boot·后端
人道领域36 分钟前
SSM框架从入门到入土(AOP面向切面编程)
java·开发语言
大模型玩家七七1 小时前
梯度累积真的省显存吗?它换走的是什么成本
java·javascript·数据库·人工智能·深度学习
CodeToGym1 小时前
【Java 办公自动化】Apache POI 入门:手把手教你实现 Excel 导入与导出
java·apache·excel