SpringBoot读取application.properties配置原理以及中文乱码问题

12 篇文章 1 订阅

SpringBoot读取application.properties原理

在SpringApplication.run方法中打断点。
在这里插入图片描述

ApplicationEnvironmentPreparedEvent事件

spring主要在环境准备阶段广播了ApplicationEnvironmentPreparedEvent事件,然后相应的监听器进行处理。

进入到org.springframework.boot.SpringApplication#prepareEnvironment方法

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

org.springframework.boot.context.event.EventPublishingRunListener#environmentPrepared方法广播事件

	@Override
	public void environmentPrepared(ConfigurableEnvironment environment) {
		this.initialMulticaster
				.multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
	}

org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType)执行事件监听器

	@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);
			}
		}
	}

在这里插入图片描述

ConfigFileApplicationListener监听器

查看ConfigFileApplicationListener监听器的实现。里面对配置文件进行了解析。

里面使用了PropertySourceLoader的两个实现类PropertiesPropertySourceLoader和YamlPropertySourceLoader对文件进行解析。

	/*
		监听器的调用方法
	*/
	@Override
	public void onApplicationEvent(ApplicationEvent event) {
		// 如果事件是ApplicationEnvironmentPreparedEvent则去完成解析
		if (event instanceof ApplicationEnvironmentPreparedEvent) {
			onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
		}
		if (event instanceof ApplicationPreparedEvent) {
			onApplicationPreparedEvent(event);
		}
	}
	
	/*
	如果是onApplicationEnvironmentPreparedEvent事件则进入这个方法
	*/
	private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
		List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
		postProcessors.add(this);
		AnnotationAwareOrderComparator.sort(postProcessors);
		// 调用所有的环境后置处理器,ConfigFileApplicationListener也是一个环境后置处理器
		for (EnvironmentPostProcessor postProcessor : postProcessors) {
			postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
		}
	}

	/*
	执行后置处理器的实现方法
	*/
	@Override
	public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
		addPropertySources(environment, application.getResourceLoader());
	}

	/*
		添加属性源
	*/
	protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
		RandomValuePropertySource.addToEnvironment(environment);
		// 调用内部类Loader的load方法
		new Loader(environment, resourceLoader).load();
	}
	
	private class Loader {

		/*
		被addPropertySources方法调用的load方法
		*/
		void load() {
			FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
					(defaultProperties) -> {
						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 (isDefaultProfile(profile)) {
								addProfileToEnvironment(profile.getName());
							}
							// 加载配件,第三个参数为consumer接口实现,用于将加载的配置保存出来
							load(profile, this::getPositiveProfileFilter,
									addToLoaded(MutablePropertySources::addLast, false));
							this.processedProfiles.add(profile);
						}
						load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
						addLoadedPropertySources();
						applyActiveProfiles(defaultProperties);
					});
		}
		
		/*
		遍历目录,然后加载
		*/
		private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
			getSearchLocations().forEach((location) -> {
				boolean isDirectory = location.endsWith("/");
				Set<String> names = isDirectory ? getSearchNames() : NO_SEARCH_NAMES;
				names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
			});
		}

		/*
		加载指定的name,比如bootstrap,application等
		*/
		private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
				DocumentConsumer consumer) {
			if (!StringUtils.hasText(name)) {
				for (PropertySourceLoader loader : this.propertySourceLoaders) {
					if (canLoadFileExtension(loader, location)) {
						load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
						return;
					}
				}
				throw new IllegalStateException("File extension of config file location '" + location
						+ "' is not known to any PropertySourceLoader. If the location is meant to reference "
						+ "a directory, it must end in '/'");
			}
			// 用来保存当前已经解析过的后缀名
			Set<String> processed = new HashSet<>();
			// 遍历属性源加载器,默认有两个:PropertiesPropertySourceLoader,YamlPropertySourceLoader
			for (PropertySourceLoader loader : this.propertySourceLoaders) {
				for (String fileExtension : loader.getFileExtensions()) {
					// 保证同一个后缀名只有一个加载器加载过一次
					if (processed.add(fileExtension)) {
						loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
								consumer);
					}
				}
			}
		}

		/*
		加载指定的前缀和文件扩展名
		*/
		private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
				Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
			DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
			DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
			if (profile != null) {
				// Try profile-specific file & profile section in profile file (gh-340)
				String profileSpecificFile = prefix + "-" + profile + fileExtension;
				load(loader, profileSpecificFile, profile, defaultFilter, consumer);
				load(loader, profileSpecificFile, profile, profileFilter, consumer);
				// Try profile specific sections in files we've already processed
				for (Profile processedProfile : this.processedProfiles) {
					if (processedProfile != null) {
						String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
						load(loader, previouslyLoaded, profile, profileFilter, consumer);
					}
				}
			}
			// Also try the profile-specific section (if any) of the normal file
			load(loader, prefix + fileExtension, profile, profileFilter, consumer);
		}

		/*
		加载指定配置文件,location为文件路径,内部调用了PropertySourceLoader的load方法
		*/
		private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
				DocumentConsumer consumer) {
			Resource[] resources = getResources(location);
			for (Resource resource : resources) {
				try {
					if (resource == null || !resource.exists()) {
						if (this.logger.isTraceEnabled()) {
							StringBuilder description = getDescription("Skipped missing config ", location, resource,
									profile);
							this.logger.trace(description);
						}
						continue;
					}
					if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
						if (this.logger.isTraceEnabled()) {
							StringBuilder description = getDescription("Skipped empty config extension ", location,
									resource, profile);
							this.logger.trace(description);
						}
						continue;
					}
					String name = "applicationConfig: [" + getLocationName(location, resource) + "]";
					// 内部调用PropertySourceLoader的load方法
					List<Document> documents = loadDocuments(loader, name, resource);
					if (CollectionUtils.isEmpty(documents)) {
						if (this.logger.isTraceEnabled()) {
							StringBuilder description = getDescription("Skipped unloaded config ", location, resource,
									profile);
							this.logger.trace(description);
						}
						continue;
					}
					List<Document> loaded = new ArrayList<>();
					for (Document document : documents) {
						if (filter.match(document)) {
							addActiveProfiles(document.getActiveProfiles());
							addIncludedProfiles(document.getIncludeProfiles());
							loaded.add(document);
						}
					}
					Collections.reverse(loaded);
					if (!loaded.isEmpty()) {
						// 将解析后的配置到consumer中回调
						loaded.forEach((document) -> consumer.accept(profile, document));
						if (this.logger.isDebugEnabled()) {
							StringBuilder description = getDescription("Loaded config file ", location, resource,
									profile);
							this.logger.debug(description);
						}
					}
				}
				catch (Exception ex) {
					StringBuilder description = getDescription("Failed to load property source from ", location,
							resource, profile);
					throw new IllegalStateException(description.toString(), ex);
				}
			}
		}
	}

PropertiesPropertySourceLoader配置加载器

该加载器主要对properties和xml文件进行解析。

	/*
	实现PropertySourceLoader接口的load方法
	*/
	@Override
	public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
		Map<String, ?> properties = loadProperties(resource);
		if (properties.isEmpty()) {
			return Collections.emptyList();
		}
		return Collections
				.singletonList(new OriginTrackedMapPropertySource(name, Collections.unmodifiableMap(properties), true));
	}

	/*
	从指定的Resource中加载配置
	*/
	private Map<String, ?> loadProperties(Resource resource) throws IOException {
		String filename = resource.getFilename();
		if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) {
			// 加载xml配置文件
			return (Map) PropertiesLoaderUtils.loadProperties(resource);
		}
		// 加载properties配置文件
		return new OriginTrackedPropertiesLoader(resource).load();
	}
OriginTrackedPropertiesLoader配置加载器

对properties配置文件进行解析。

同时也是乱码出现的地方。

	/*
	PropertiesPropertySourceLoader.loadProperties调用的方法
	*/
	Map<String, OriginTrackedValue> load() throws IOException {
		return load(true);
	}

	/*
	具体加载配置实现方法
	*/
	Map<String, OriginTrackedValue> load(boolean expandLists) throws IOException {
		// CharacterReader 默认采用了ISO_8859_1编码造成了乱码
		try (CharacterReader reader = new CharacterReader(this.resource)) {
			Map<String, OriginTrackedValue> result = new LinkedHashMap<>();
			StringBuilder buffer = new StringBuilder();
			while (reader.read()) {
				String key = loadKey(buffer, reader).trim();
				if (expandLists && key.endsWith("[]")) {
					key = key.substring(0, key.length() - 2);
					int index = 0;
					do {
						OriginTrackedValue value = loadValue(buffer, reader, true);
						put(result, key + "[" + (index++) + "]", value);
						if (!reader.isEndOfLine()) {
							reader.read();
						}
					}
					while (!reader.isEndOfLine());
				}
				else {
					OriginTrackedValue value = loadValue(buffer, reader, false);
					put(result, key, value);
				}
			}
			return result;
		}
	}

	// 静态内部类
	private static class CharacterReader implements Closeable {
		CharacterReader(Resource resource) throws IOException {
			// 默认采用了ISO_8859_1编码
			this.reader = new LineNumberReader(
					new InputStreamReader(resource.getInputStream(), StandardCharsets.ISO_8859_1));
		}
	}

application.properties中文出现乱码

因为spring中默认读取配置的编码格式为 ISO_8859_1 。
所以在properties中的中文配置会变成乱码。

解决方案1

在这里插入图片描述
在IDEA中勾选如上选项。
勾选的意思是将properties文件都转成ASCII码,但是在IDEA中显示为正常意思,如果用其它编辑器打开,就会变成Unicode。
并且在实际开发的时候,勾选这个选项后,自己本地查看是正常的,但其他人没有勾选这个选项,那么就会出现Unicode。

解决方案2

自定义实现PropertySourceLoader接口。
这个接口是spring用于解析配置文件的接口,查看spring的PropertiesPropertySourceLoader发现其中使用的编码为ISO_8859_1;所以才出现乱码问题。
自定义实现一个UTF-8的配置文件解析器。

1、直接将spring的PropertiesPropertySourceLoader的实现逻辑拿过来,修改其中的编码格式即可。

package com.yiweituo.fix.infrastructure.loader;

import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.boot.origin.OriginTrackedValue;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * 自定义 properties文件后缀加载器
 * 因为spring默认的PropertiesPropertySourceLoader采用ISO_8859_1编码读取
 *
 * @author wenei
 * @date 2021-04-10 10:56
 */
public class CustomPropertiesPropertySourceLoader implements PropertySourceLoader {

    @Override
    public String[] getFileExtensions() {
        return new String[]{"properties"};
    }

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

    private Map<String, ?> loadProperties(Resource resource) throws IOException {
    	// 将spring的OriginTrackedPropertiesLoader拉下来重写,修改里面的编码即可
        return new OriginTrackedPropertiesLoader(resource).load();
    }
}

自定义OriginTrackedPropertiesLoader类,将编码格式修改。

	Map<String, OriginTrackedValue> load(boolean expandLists) throws IOException {
        try (CharacterReader reader = new CharacterReader(this.resource)) {
            Map<String, OriginTrackedValue> result = new LinkedHashMap<>();
            StringBuilder buffer = new StringBuilder();
            while (reader.read()) {
                String key = loadKey(buffer, reader).trim();
                if (expandLists && key.endsWith("[]")) {
                    key = key.substring(0, key.length() - 2);
                    int index = 0;
                    do {
                        OriginTrackedValue value = loadValue(buffer, reader, true);
                        put(result, key + "[" + (index++) + "]", value);
                        if (!reader.isEndOfLine()) {
                            reader.read();
                        }
                    }
                    while (!reader.isEndOfLine());
                }
                else {
                    OriginTrackedValue value = loadValue(buffer, reader, false);
                    put(result, key, value);
                }
            }
            return result;
        }
    }

		CharacterReader(Resource resource) throws IOException {
            this.reader = new LineNumberReader(
                    new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8));
        }

2、在项目中添加META-INF/spring.factories文件即可

org.springframework.boot.env.PropertySourceLoader=\
    com.infrastructure.loader.CustomPropertiesPropertySourceLoader
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

肥牛火锅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值