Spring无法使用@PropertySource配合@ConfigurationProperties注入对象属性问题解析

文章目录

        • 一、问题背景
        • 二、重现问题
          • 2.1 编写`datasource.yml`文件
          • 2.2 编写`DataSourceProperties`
          • 2.3 编写`DynamicDataSourceProperties`
          • 2.4 无法注入原因解析
        • 三、解决方案
          • 3.1 自定义`PropertySourceFactory`
          • 3.2 使用`@PropertySource`注解

一、问题背景

因为项目中需要用到多数据源,所以想自己写一个datasource.yml的配置文件单独用来配置数据源,采用spring的常用解析方法,并没有成功。

二、重现问题

2.1 编写datasource.yml文件
dynamic:
  datasource:
    druids:
      write:
        driverClassName: com.p6spy.engine.spy.P6SpyDriver
        url: jdbc:p6spy:mysql://118.89.104.67:3306/saas_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
        username: root
        password: mysql!@root##123
        filters: stat
      read:
        driverClassName: com.p6spy.engine.spy.P6SpyDriver
        url: jdbc:p6spy:mysql://localhost:3306/saas_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
        username: root
        password: 1234
        filters: stat
2.2 编写DataSourceProperties
@Data
public class DataSourceProperties {
    private String driverClassName;
    private String url;
    private String username;
    private String password;
}
2.3 编写DynamicDataSourceProperties

使用spring提供的@ConfigurationProperties配合@PropertySource注解来将上面的datasource.yml解析到DynamicDataSourcePropertiesdruidsMap中

@Data
@PropertySource(value = {"classpath:datasource.yml"})
@ConfigurationProperties(prefix = "dynamic.datasource")
public class DynamicDataSourceProperties {
	// 多数据源配置属性转Map
    private Map<String, DataSourceProperties> druids = new LinkedHashMap<>();
}

但是结果很不理想,启动springboot后发现driuds是一个空的集合。

2.4 无法注入原因解析

通过追踪@PropertySource的源码发现他的解析位置是从ConfigurationClassParser开始的,通过doProcessConfigurationClass方法来解析

protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)

继续观察他的源码

// Process any @PropertySource annotations
		for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
				sourceClass.getMetadata(), PropertySources.class,
				org.springframework.context.annotation.PropertySource.class)) {
			if (this.environment instanceof ConfigurableEnvironment) {
				processPropertySource(propertySource);
			}
			else {
				logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
						"]. Reason: Environment must implement ConfigurableEnvironment");
			}
		}

这里用了一个processPropertySource的方法,继续往下追踪

	/**
	 * Process the given @PropertySource annotation metadata.
	 * @param propertySource metadata for the @PropertySource annotation found
	 * @throws IOException if loading a property source failed
	 */
	private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
		String name = propertySource.getString("name");
		if (!StringUtils.hasLength(name)) {
			name = null;
		}
		String encoding = propertySource.getString("encoding");
		if (!StringUtils.hasLength(encoding)) {
			encoding = null;
		}
		String[] locations = propertySource.getStringArray("value");
		Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
		boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");

		Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
		PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
				DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));

		for (String location : locations) {
			try {
				String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
				Resource resource = this.resourceLoader.getResource(resolvedLocation);
				addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
			}
			catch (IllegalArgumentException | FileNotFoundException | UnknownHostException ex) {
				// Placeholders not resolvable or resource not found when trying to open it
				if (ignoreResourceNotFound) {
					if (logger.isInfoEnabled()) {
						logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());
					}
				}
				else {
					throw ex;
				}
			}
		}
	}

可以看到for循环里有这样一个函数addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));

里面具体解析属性文件的函数是由PropertySourceFactory factory 来执行的,跟踪下去,可以看到Spring只提供一个默认的DefaultPropertySourceFactory,观察这个默认的工厂不难发现他是用的PropertiesLoaderUtils.loadProperties(resource)来加载属性文件,跟踪到最后发现,在Properties这个类中读取属性

    public synchronized void load(InputStream inStream) throws IOException {
        load0(new LineReader(inStream));
    }

由此可以发现Spring提供的@PropertySource注解他并没有兼容yml文件的解析,他会一行一行的读取,这意味着

city:
	name: 海南
	area: 200

上面的yml会被解析为

city = ""
name = "海南"
area = "200"

而我们想要的是

city.name = "海南"
city.area = "200"

三、解决方案

3.1 自定义PropertySourceFactory
public class YamlPropertySourceFactory implements PropertySourceFactory {
    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
        YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
        List<PropertySource<?>> load = loader.load(name, resource.getResource());
        if (load!=null && !load.isEmpty()) {
            return load.get(0);
        }
        return null;
    }
}

3.2 使用@PropertySource注解

使用的时候指定解析的PropertySourceFactory


@PropertySource(value = {"classpath:datasource.yml"}, 
							 factory = YamlPropertySourceFactory.class)

你可能感兴趣的:(Spring)