druid-spring-boot icon indicating copy to clipboard operation
druid-spring-boot copied to clipboard

提一个建议,关于配置文件绑定顺序的问题

Open lihengming opened this issue 7 years ago • 13 comments

min-evictable-idle-time-millis: 100000
max-evictable-idle-time-millis: 200000

你好,例如有上面这种配置,会导致单元测试失败,因为使用动态绑定会按ASCII的顺序绑定配置属性,也就是说max 是先于 min执行的,这也是Druid官方Starter放弃动态绑定这种便捷方式的原因之一,如果你有更好的解决方案,可以探讨下。

lihengming avatar Jul 20 '17 05:07 lihengming

根据给出的配置编写测试用例,没有复现问题。

单数据源测试用例:DevProfilesTests.java 多数据源测试用例:DynamicDevProfilesTests.java

drtrang avatar Jul 20 '17 07:07 drtrang

@drtrang hi,我又看了下,原来你把@ConfigurationProperties放在了一个继承 DruidDataSource 的包装类上,使用@Bean(initMethod = "init", destroyMethod = "close")调用DruidDataSource.init()来初始化数据源,我发现这样 Spring Boot 绑定就是按照对象成员变量的顺序调用set方法了,这样即解决了该问题同时IDE提示也会覆盖 DruidDataSource 内所有成员变量,为你的这个思路点赞。

@ConfigurationProperties(DRUID_DATA_SOURCE_PREFIX)
public abstract class DruidParentDataSource extends DruidDataSource {
@ConditionalOnMissingBean(DataSource.class)
//如果放在这里,则使用ConfigurationPropertiesBindingPostProcessor来绑定,会出现顺序的问题
@ConfigurationProperties(DRUID_DATA_SOURCE_PREFIX)
@Bean(initMethod = "init", destroyMethod = "close")
public DruidDataSource dataSource() {
    log.debug("druid data-source init...");
    return new DruidParentDataSource() {};
}

 datasource:
    druid:
      min-evictable-idle-time-millis: 100000
      max-evictable-idle-time-millis: 200000
      one:
        name: one
      two:
        name: two
  @Bean
    @ConfigurationProperties("spring.datasource.druid.one")
    public DruidDataSource firstDataSource() {
        log.debug("druid first-data-source init...");
        return new DruidMultiDataSource();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.two")
    public DruidDataSource secondDataSource() {
        log.debug("druid second-data-source init...");
        return new DruidMultiDataSource();
    }

但是,多数据源就不行了,它们只能把@ConfigurationProperties放在 Bean 上面,这样绑定的时候不会按照对象成员变量的顺序调用set方法,而是按照ASCII的顺序来调用set方法,不过,yml是不存在这个问题的,我想这也是Spring Boot ConfigurationPropertiesBindingPostProcessor的一个BUG吧,像下面这种配置(application-dynamic-dev.properties)就会报错,之所以你提供那个多数据源测试用例不会报错那是因为都是使用的单数据源的配置,两个数据源配置一模一样,并且使用的是yml而不是properties

spring.autoconfigure.exclude=- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
spring.datasource.druid.one.min-evictable-idle-time-millis=100000
spring.datasource.druid.one.max-evictable-idle-time-millis=200000
spring.datasource.druid.two.min-evictable-idle-time-millis=100001
spring.datasource.druid.two.max-evictable-idle-time-millis=200002

lihengming avatar Jul 20 '17 08:07 lihengming

@lihengming

直接上结论,导致 https://github.com/alibaba/druid/issues/1796 的原因在于 Spring Boot 在读取 .yml 时是有序的,而读取 .properties 文件时是无序的。所以如果需要依赖于配置顺序,建议使用 YAML 方式。

YamlPropertySourceLoader#load(String name, Resource resource, String profile)

// 用 LinkedHashMap 解析
public Map<String, Object> process() {
    final Map<String, Object> result = new LinkedHashMap<String, Object>();
    process(new MatchCallback() {
        @Override
        public void process(Properties properties, Map<String, Object> map) {
            result.putAll(getFlattenedMap(map));
        }
    });
    return result;
}

PropertiesPropertySourceLoader#load(String name, Resource resource, String profile)

// 用 HashTable 解析
public PropertySource<?> load(String name, Resource resource, String profile) throws IOException {
    if (profile == null) {
        Properties properties = PropertiesLoaderUtils.loadProperties(resource);
        if (!properties.isEmpty()) {
            return new PropertiesPropertySource(name, properties);
        }
    }
    return null;
}

RelaxedDataBinder 在进行数据绑定的时候,会依据 PropertySource 的顺序赋值,而 Properties 继承自 HashTable,本身是无序的,那么出现问题就不稀奇了。

所以问题不在于单数据源或多数据源,也不在于 @ConfigurationProperties 注解是标注在 DruidDataSource 还是 DruidParentDataSource ,而是因为 Spring 对不同文件类型的处理方式不同。

drtrang avatar Jul 21 '17 07:07 drtrang

DruidParentDataSource 类存在的目的是为了注入 spring.datasource.druid 的配置。 基于 Spring4 的特性,DruidMultiDataSource 在继承 DruidParentDataSource 的同时,也会继承这些配置,由此解决多数据源场景下相同配置重复定义的问题。

也就是说,以下两种方式是等价的:

spring:
  datasource:
    druid:
      min-evictable-idle-time-millis: 1800000
      max-evictable-idle-time-millis: 25200000
      one:
        name: one
      two:
        name: two
---
spring:
  datasource:
    druid:
      one:
        name: one
        min-evictable-idle-time-millis: 1800000
        max-evictable-idle-time-millis: 25200000
      two:
        name: two
        min-evictable-idle-time-millis: 1800000
        max-evictable-idle-time-millis: 25200000

若子数据源有相同的配置时,会覆盖掉父数据源的值:

spring:
  datasource:
    druid:
      max-active: 20
      one:
        name: one
        max-active: 50
      two:
        name: two

drtrang avatar Jul 21 '17 08:07 drtrang

@drtrang thx,那也就是说,要解决该问题要么放弃使用RelaxedDataBinder ,要么放弃使用 .properties使用.yml,显然为了兼容 .properties我采取了前者。

我个人还是比较看好.yml,优美、简洁,我想最终也会替代 .properties的,就像JSON替代XML一样。

幸运的是,Spring Boot官方也意识到了该问题,在Spring Boot 2.0 移除了RelaxedDataBinder,并重构了PropertiesPropertySourceLoader和相关属性绑定的代码,如果是Map类型的原因,这个BUG应该被修复了。

PropertiesPropertySourceLoader#load(String name, Resource resource, String profile)

public PropertySource<?> load(String name, Resource resource, String profile)
		throws IOException {
	if (profile == null) {
		Map<String, ?> properties = loadProperties(resource);
		if (!properties.isEmpty()) {
			return new OriginTrackedMapPropertySource(name, properties);
		}
	}
	return null;
}

OriginTrackedPropertiesLoader# load(boolean expandLists)

/**
 * Load {@code .properties} data and return a map of {@code String} ->
 * {@link OriginTrackedValue}.
 * @param expandLists if list {@code name[]=a,b,c} shortcuts should be expanded
 * @return the loaded properties
 * @throws IOException on read error
 */
public 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;
	}
}

PropertiesPropertySourceLoaderTests 单元测试我运行了下,已经是按照声明的顺序来存储了。

lihengming avatar Jul 21 '17 09:07 lihengming

期待 2.0 发布 : )

drtrang avatar Jul 21 '17 09:07 drtrang

: ) 正式版应该快了,这是一次愉快的技术探讨,感谢。

lihengming avatar Jul 21 '17 09:07 lihengming

表示spring boot2.0 没有修复这个问题,最近遇到这个坑,项目不经常出现启动失败, 重启又好了

itmyhome avatar Dec 04 '18 12:12 itmyhome

@itmyhome 使用的 spring boot 版本、starter 版本是多少?堆栈是否方便贴一下?

drtrang avatar Dec 05 '18 03:12 drtrang

@itmyhome 使用的 spring boot 版本、starter 版本是多少?堆栈是否方便贴一下? 我也遇到了这个问题: spring-boot版本:2.1.2.RELEASE druid版本:1.1.10 yaml配置: min-evictable-idle-time-millis: 300000 max-evictable-idle-time-millis: 600000 启动项目或者Junit 测试的时候也会时不时的报。请问该如何解决?

Caused by: org.springframework.boot.context.properties.ConfigurationPropertiesBindException: Error creating bean with name 'dataSource': Could not bind properties to 'DruidDataSourceWrapper' : prefix=spring.datasource.druid, ignoreInvalidFields=false, ignoreUnknownFields=true; nested exception is org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'spring.datasource.druid' to javax.sql.DataSource at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.bind(ConfigurationPropertiesBindingPostProcessor.java:110) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:93) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:419) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1737) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:576) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:498) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE] at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:277) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE] at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1244) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE] at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1164) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE] at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:857) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE] at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:760) ~[spring-beans-5.1.4.RELEASE.jar:5.1.4.RELEASE] ... 66 common frames omitted Caused by: org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'spring.datasource.druid' to javax.sql.DataSource at org.springframework.boot.context.properties.bind.Binder.handleBindError(Binder.java:249) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:225) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:208) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:190) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.bind(ConfigurationPropertiesBinder.java:83) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.bind(ConfigurationPropertiesBindingPostProcessor.java:107) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] ... 80 common frames omitted Caused by: java.lang.IllegalStateException: Unable to set value for property max-evictable-idle-time-millis at org.springframework.boot.context.properties.bind.JavaBeanBinder$BeanProperty.setValue(JavaBeanBinder.java:333) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:89) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:72) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:54) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.boot.context.properties.bind.Binder.lambda$null$4(Binder.java:344) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) ~[na:1.8.0_202] at java.util.ArrayList$ArrayListSpliterator.tryAdvance(ArrayList.java:1359) ~[na:1.8.0_202] at java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:126) ~[na:1.8.0_202] at java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:498) ~[na:1.8.0_202] at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:485) ~[na:1.8.0_202] at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) ~[na:1.8.0_202] at java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:152) ~[na:1.8.0_202] at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:1.8.0_202] at java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:464) ~[na:1.8.0_202] at org.springframework.boot.context.properties.bind.Binder.lambda$bindBean$5(Binder.java:345) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:448) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.boot.context.properties.bind.Binder$Context.withBean(Binder.java:434) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.boot.context.properties.bind.Binder$Context.access$400(Binder.java:388) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.boot.context.properties.bind.Binder.bindBean(Binder.java:342) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:277) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:220) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] ... 84 common frames omitted Caused by: java.lang.reflect.InvocationTargetException: null at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_202] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_202] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_202] at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_202] at org.springframework.boot.context.properties.bind.JavaBeanBinder$BeanProperty.setValue(JavaBeanBinder.java:330) ~[spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE] ... 104 common frames omitted Caused by: java.lang.IllegalArgumentException: maxEvictableIdleTimeMillis must be grater than minEvictableIdleTimeMillis at com.alibaba.druid.pool.DruidAbstractDataSource.setMaxEvictableIdleTimeMillis(DruidAbstractDataSource.java:767) ~[druid-1.1.10.jar:1.1.10] ... 109 common frames omitted

HiAscend avatar Jan 20 '19 03:01 HiAscend

2.0.9 用的yml 有同样的问题

zhangyu-yxff avatar Jun 17 '19 07:06 zhangyu-yxff

我用的2.1.5 yml读取map也是无序的

JiancongLee avatar Oct 12 '19 09:10 JiancongLee

@zhangyu-yxff @JiancongLee 有可复现的 test case 么?我这边暂时没有复现

drtrang avatar Oct 29 '19 12:10 drtrang