前天,每天都会推出一个小迭代。内容是:将接口A切换到接口B,需求很小,QA不想测试,所以让我自测后不测试就上线。开发完成后,我很快将其部署到测试环境进行验证。没有任何问题,完美!已经准备好上线了。
我兴奋地搭建在网上,程序很快就上线了。过了一会儿,我发现系统疯了,报错。当我查看错误堆栈中调用的接口URL时,我惊讶地喊道:“我为什么要在线请求测试环境!”。
赶紧回滚代码吧。幸运的是,代码回滚后系统不再报错。但仅仅回滚代码是不够的。你必须找出原因并上网。我仔细看了一下自己的代码,发现除了调用下游方法的写法之外,业务逻辑没有任何缺陷。
@Value("${rpc.url}")私有字符串主机;......公共 布尔值 customerAuth(对象...对象) { URIBuilder uriBuilder = new URIBuilder(); uriBuilder.setHost(主机); ... 字符串内容; HttpGet httpget; URI uri = www.sychzs.cn(); httpget = new HttpGet(uri); www.sychzs.cn("请求:n {} {} n", httpget.getMethod(), httpget.getURI()); HttpResponse 响应 = httpClient.execute (httpget); ... u结果; }
原来在调用下游时,我使用@Value注入请求下游服务的url。为了更优雅的实现功能(我默默拿出了《代码整洁之道》),我改成了使用@FeignClient注解来实现,同时配置了进入Apollo的路径,以减少代码量。
@Value
@FeignClient
@FeignClient(名称 = "Rpc", contextId = "Rpc", url = "${rpc.url }") 公共 接口 Rpc { @GetMapping(值 = "xxx/xxx/查询")结果> getContractDiscounts(@RequestParam("数字"); }
然后我仔细检查了我在apollo中配置的url路径,确认它在线。然后我这个时候就更疑惑了,“测试环境不是跑得好好的吗?怎么到了生产环境就这么乱?”,直到我看到了applicaiton.yml中的配置:
rpc: url: http://www.sychzs.cn
显然Apollo中的配置没有生效,而是application.yml中的配置生效了。为了证实我的猜想,我删除了applicaiton.yml中的代码,然后重启服务并调用接口。结果报了这个错误:
原因:java.lang.IllegalArgumentException:非法字符in索引7处的权限:http://${rpc.url} 在 java .net.URI.create(www.sychzs.cn:852) at www.sychzs.cn(www.sychzs.cn:465) ... 省略 162 个常见帧
果然,我的猜测是正确的。为了先解决问题,我在applicaiton-test.yml中配置了新的接口路径。再次上线后,系统没有报错,运行正常。虽然代码运行正常,但我心里不仅有一个疑问:“为什么切换写法之前Apollo配置可以正常覆盖,而切换写法之后就不行了?”
为了找到问题的原因,首先需要了解springboot项目中的配置是如何生效的。查资料得知,在SpringBoot中,有一个名为Application的变量,里面保存了Spring中启动的所有信息。
在所有变量中,配置信息主要与变量环境有关。 JVM参数、环境变量、Apollo配置等配置都用PropertySource封装,并存储在Environment中。
环境
PropertySource
Environment
除了存储配置之外,SpringBoot还设计了propertyResolver来管理当前的配置信息,并负责填充配置。
propertyResolver
至于PropertyResolver和PropertySource的关系,从视觉上看,PropertyResolver 是一位会使用现有词典的翻译者 PropertySource 翻译我们的语言${xxx.url},最终获取配置信息。如果字典里没有对应的信息,那么“翻译者”自然就无法翻译。
PropertyResolver
PropertyResolver 是一位会使用现有词典的翻译者 PropertySource 翻译我们的语言${xxx.url},最终获取配置信息。如果字典里没有对应的信息,那么“翻译者”自然就无法翻译。
${xxx.url}
所以不难分析,问题的原因应该是切换写入方式后,配置的加载顺序改变了,导致配置解析先于apollo中的配置加载,导致解析失败失败。
认识到问题的原因可能是由于配置加载顺序造成的,我们需要更正Apollo、@Value、@ FeignClientThe三者的配置加载顺序 要了解。
Apollo
@ FeignClient
首先我们来了解一下Apollo配置的加载顺序。结合Apollo文档中的内容,不难得到Apollo配置的加载顺序的三种情况:
这里简单介绍一下这三种情况对应的Springboot运行阶段负责的功能:
prepareEnvironment
prepareContext
refreshContext
BeanDefinition
BeanFactory
现有SpringBoot启动流程中这三个阶段的顺序如下:
在preparenEnvironment阶段,Spring会发出异步消息ApplicationEnvironmentPreparedEvent,也称为Conf igFileApplicationListener对象将监听消息并实现EnvironmentPostProcessor 调用接口的对象。
preparenEnvironment
ApplicationEnvironmentPreparedEvent
Conf igFileApplicationListener
EnvironmentPostProcessor 调用
Apollo源码中,ApolloApplicationContextInitializer类还实现了EnvironmentPostProcessor的接口。 apollo 配置在其实现方法中加载。
ApolloApplicationContextInitializer
EnvironmentPostProcessor
在prepareContext阶段,主要依赖于applyInitializers这个方法。该方法将在所有实现 ApplicationContextInitializer 接口的对象上调用。在 Apollo 中,ApolloApplicationContextInitializer 类也实现了这个接口,并在方法中加载配置。
applyInitializers
ApplicationContextInitializer
refreshContext是Apollo的默认加载阶段。在refreshContext中,调用invokeBeanFactoryPostProcessors方法来调用实现BeanFactoryPostProcessor接口的对象。在apollo源代码中,对象PropertySourcesProcessor实现了这个接口。并且该对象在 postProcessBeanFactory 方法中加载配置信息。
invokeBeanFactoryPostProcessors
BeanFactoryPostProcessor
PropertySourcesProcessor
postProcessBeanFactory
由此,Apollo三个阶段的加载顺序和配置控制逻辑如下图所示:
了解了apollo的加载顺序后。我们需要了解@Value的加载顺序。 @Value的实现思想非常纯粹。你的Bean对象创建完成后,我会通过getter和setter方法将属性注入到其中,实现注入功能。
所以@Value的实现主要是在Bean生成之后。在 refreshContext 阶段,会调用 finishBeanFactoryInitialization 方法来初始化所有单例 bean 对象。其中AbstractAutowireCapableBeanFactory中有一个方法populateBean,它会填充bean属性。与上面类似,所有继承BeanPostProcessor接口的对象也会在这里被调用。它包含一个特殊对象AutowiredAnnotationBeanPostProcessor
finishBeanFactoryInitialization
AbstractAutowireCapableBeanFactory
BeanPostProcessor
AutowiredAnnotationBeanPostProcessor
AutowiredAnnotationBeanPostProcessor会扫描出被@Value注解修改的对象,从配置中找到对应的配置信息,注入到对象中。结合上面的apollo配置加载时序图,我们可以得到@Value和Apollo的配置优先级如下:
可以看到,@Value的配置晚于apollo的配置,所以在切换写入方式之前,apollo的配置是可以正常注入的。
了解了@Value的加载顺序后,我们还需要了解@FeignClient的配置加载顺序。对于FeignClient来说,通常是使用接口实现的,所以需要根据@FeignClient生成一个新的Bean对象,并注册到容器中。因此,它的配置是在生成Bean对象之前按顺序加载的。
类ConfigurationClassPostProcessor继承自接口AutowiredAnnotationBeanPostProcessor,其中postProcessBeanDefinitionRe要点ry方法将注入BeanDefinition。 (BeanDefinition,缩写为BeanDef,是Bean容器未生成的形式。如果把Bean比作一辆汽车,那么BeanDefinition就是汽车的图纸。)
ConfigurationClassPostProcessor
postProcessBeanDefinitionRe要点ry
同时,类ConfigurationClassBeanDefinitionReader将调用loadBeanDefinitionsFromRegistrars方法,该方法将实现Im portBeanDefinitionRegistrar 接口的对象通过以下方式调用一。其中包含一个 FeignClientsRegistrar 对象,其实现的 registerFeignClients 方法会扫描所有使用 @FeignClient 注释的对象。
ConfigurationClassBeanDefinitionReader
loadBeanDefinitionsFromRegistrars
Im portBeanDefinitionRegistrar
FeignClientsRegistrar
registerFeignClients
同时,对于单个BeanDef对象,也会调用FeignClientsRegistrar下的registerFeignClient方法进行处理,我们所有的url、path等属性都会被调用被使用propertyResolver进行翻译处理。如果此时配置中不存在对应的属性,则不会更新。这是造成这个问题的关键点。
registerFeignClient
重点关注加载顺序,@FeignClient注解依赖的接口是BeanDefinitionRegistryPostProcessor,Apollo中默认加载依赖于 BeanFactoryPostProcessor接口。两者几乎在同一个方法调用中,但BeanDefinitionRegistryPostProcessor接口的执行稍早于BeanFactoryPostProcessor。因此,在加载顺序上,@FeignClient默认会先于Apollo加载。
BeanDefinitionRegistryPostProcessor
至此就不难理解为什么Apollo注解无法生效了。因为在@FeignClient注解的情况下,注入beanDef时apollo的配置还没有加载,而PropertyResolver找不到对应的配置,自然也无法注入。
了解了上述配置的工作机制后,我在原代码中添加了apollo.bootstrap.enabled=true,将Apollo的配置加载提前到FeignClient加载之前,然后重新运行代码。 项目按预期正常运行。
apollo.bootstrap.enabled=true