【自定义外部配置】记SpringBoot读取配置文件的坑(自定义元数据)

记SpringBoot读取配置文件的坑(自定义元数据)

使用的注解@ConfigurationProperties,@ConditionalOnProperty
在一次日常写代码中,按着度娘写的代码去读取配置文件中的数据,发现 @ConditionalOnProperty 这个注解总是验证不通过(导致bean注册不上去)。仔细查找一下资料,发现写法并无错误,但就是验证不通过,发现配置文件中不为on就能通过,即使是false也能通过(这里是指字符串)。于是开始一下午的排坑之旅。

先说结论

SpringBoot中读取配置文件,会先给字段打上标识符Tag来识别他是某一个类型,例如有str、seq、map、时间戳等。不同的类型会有不同的默认值,而value为on时,会识别成bool类型,默认值是true,这样和注解@ConditionalOnProperty(havingValue="on")中的on自然不相等,也就会一直校验失败。

  • 那么SpringBoot是根据什么来给字段打上标签的呢?
    经过一下午的查询找到在SpringBoot中有一个叫 Resolver的类,他的一个字段yamlImplicitResolvers里面就存放了字段的校验规则。

首先先看一下校验的源码:

public Tag resolve(NodeId kind, String value, boolean implicit) {
    if (kind == NodeId.scalar && implicit) {
        final List<ResolverTuple> resolvers;
        /*
        这里我们可以看到,yamlImplicitResolvers会去取字段的第一个字母做校验
        */
        if (value.length() == 0) {
            resolvers = yamlImplicitResolvers.get('\0');
        } else {
            resolvers = yamlImplicitResolvers.get(value.charAt(0));
        }
        // yamlImplicitResolvers中分null和非null,这里是非null的情况
        if (resolvers != null) {
            for (ResolverTuple v : resolvers) {
                Tag tag = v.getTag();
                Pattern regexp = v.getRegexp();
                // 这里会进行一个正则匹配,只有匹配上了才能返回
                if (regexp.matcher(value).matches()) {
                    return tag;
                }
            }
        }
        if (yamlImplicitResolvers.containsKey(null)) {
            for (ResolverTuple v : yamlImplicitResolvers.get(null)) {
                Tag tag = v.getTag();
                Pattern regexp = v.getRegexp();
                // 同上
                if (regexp.matcher(value).matches()) {
                    return tag;
                }
            }
        }
    }
    // 如果在yamlImplicitResolvers没有查询到结果,那么就返回传进来的kind(一般是str)
    switch (kind) {
    case scalar:
        return Tag.STR;
    case sequence:
        return Tag.SEQ;
    default:
        return Tag.MAP;
    }
}

根据上面的代码,我们不难得出 yamlImplicitResolvers就是一个Map,里面的key为一个字符,用于匹配字段名,value为正则表达式,只有同时匹配key和value才返回完整的Tag

  • 注意这里key和value指的是yamlImplicitResolvers对象的key和value,而不是配置文件中的key和value。

列举一些yamlImplicitResolvers中的值:

  • null->[^(?:~|null|Null|NULL| )$,^$]
  • F->[^(?:yes|Yes|YES|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)$]
  • N->[^(?:yes|Yes|YES|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)$, ^(?:~|null|Null|NULL| )$]
  • (万恶之源)o->[^(?:yes|Yes|YES|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)$]

注意,配置文件中只有value需要注意写法,如果在@ConfigurationProperties标识的类中定义了字段类型,那么就不会自行转换了(可能)。

痛苦记忆
  1. 最开始以为是写法问题,属性值写错了之类的。点进去@ConditionalOnProperty里看了很多遍,很多方法都试过了,发现没有问题。然后试了下官方文档的实例,发现都可以,甚至false都可以,说明这个字段应该是字符串类型的,然后发现只有用on不行,于是猜想是不是SpringBoot自带什么奇怪的转化问题。
  2. 其实在一开始是报的找不到bean这个错,一顿排查找到了带@ConditionalOnProperty的这个方法,猜想打了断点发现根本没创建bean,猜想是不是没通过校验。😂
  3. 然后准备去找,是不是@ConditionalOnProperty里面做了什么奇怪的操作把属性havingValue的值给改掉了,但自己对Spring不算很熟练,只知道SpringAop会对方法代理,执行一个代理方法,这么快想到是因为之前自己写过AOP日志。于是想着去找Spring的方法增强里面找,看看是不是把方法注解给我改东西了。
  4. 首先找到打印日志的类NoSuchBeanDefinitionFailureAnalyzer 通过debug 观察到抛出的异常,但是这个异常仅仅是找不到 bean 所抛出的异常,对我们要找的问题没有什么帮助。在搜索ConditionalOnProperty观察后,在Spring的上下文的ConditionContext中找到了创建 bean 的方法,观察发现在该context中注解的值是正常的为**“on”**。(在OnPropertyCondition的校验类中的getMatchOutcome方法里面可以找到,并且debug发现把匹配结果加入了noMatch集合中,也就是为匹配,说明配置文件读取的有问题。)
  5. 继续观察上下文对象,发现了配置文件中的值为true,这就很神奇了,明明配置文件中写的是on,为什么会变成true呢?(这里的value点进去是Boolean类型)
    自己定义的字段和值

在context对象中有一个environment对象,里面的propertySources属性里记录了外部数据的详细信息,其中的OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.yaml]' via location 'optional:classpath:/''}对象就是读取我们自己写的配置文件的对象,再去里面找到source属性就可以找到我们自己定义的所有字段包括值

根本原因

到这里,问题似乎一目了然了,显然是SpringBoot在读取配置文件时,根据字段或者值在转成java对应的类型时转出了问题。
那么SpringBoot是怎么读取配置文件的呢,度娘了一下,找到了这个类YamlPropertySourceLoader是用于读取yaml文件的,(顺带提一句,这里可以看到yml和yaml是一个文件类型)。随后在核心方法里可以找到调用的OriginTrackedYamlLoader这货的load方法

public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
	if (!ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", getClass().getClassLoader())) {
		throw new IllegalStateException(
				"Attempted to load " + name + " but snakeyaml was not found on the classpath");
	}
	// 就在这里
	List<Map<String, Object>> loaded = new OriginTrackedYamlLoader(resource).load();
	if (loaded.isEmpty()) {
		return Collections.emptyList();
	}
	List<PropertySource<?>> propertySources = new ArrayList<>(loaded.size());
	for (int i = 0; i < loaded.size(); i++) {
		String documentNumber = (loaded.size() != 1) ? " (document #" + i + ")" : "";
		propertySources.add(new OriginTrackedMapPropertySource(name + documentNumber,
				Collections.unmodifiableMap(loaded.get(i)), true));
	}
	return propertySources;
}

在load里面继续调用了YamlProcessor类的process方法

private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
	int count = 0;
	try {
		if (logger.isDebugEnabled()) {
			logger.debug("Loading from YAML: " + resource);
		}
		try (Reader reader = new UnicodeReader(resource.getInputStream())) {
			// 在这里才会加载yaml
			for (Object object : yaml.loadAll(reader)) {
				if (object != null && process(asMap(object), callback)) {
					count++;
					if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) {
						break;
					}
				}
			}
			if (logger.isDebugEnabled()) {
				logger.debug("Loaded " + count + " document" + (count > 1 ? "s" : "") +
						" from YAML resource: " + resource);
			}
		}
	}
	catch (IOException ex) {
		handleProcessError(resource, ex);
	}
	return (count > 0);
}

点进加载方法loadAll看一下,发现这是一个迭代器(还发现配置文件用文件流读取),里面有一个
全局对象constructor,暂时不去关心怎么来的,去重写的迭代器里面的next方法发现调用的是constructor对象的getData()方法。在点进去就是本文开头的内容了,这里就不再复述了。

总结

到此,我们明白了SpringBoot读取配置文件的机制,并且了解了配置文件有一些特殊的语义需要注意。这次观察SpringBoot源码告一段落,其实在一开始就大概猜到了是哪里出了问题,但是不清楚为什么会这样,阅读完源码想明白了很多,感觉又有成长了。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值