问题回顾
前天,日常上线了个小迭代。内容是:将接口A切换成了接口B,需求很小,QA也没想着测,就让我自测后走免测上线了。开发完成后,赶紧部署到测验环境验证了下,没啥问题,perfect!可以上线了。
我兴奋地在线上一通构建,程序很快上线了。没一会,发现体系张狂报错。瞅着过错栈里调用的接口url我一看,惊奇地大喊:“怎样线上恳求到测验环境了!”。赶紧回滚代码。所幸,体系在代码回退后报错中止了。但是光回退代码还不行呀,还得找出原因上线呀。我细心端详我的代码,事务逻辑上无懈可击,只要调用下流办法的写法有些差异。
@Value("${rpc.url}")
private String host;
.......
public Boolean customerAuth(Object... objects) {
URIBuilder uriBuilder = new URIBuilder();
uriBuilder.setHost(host);
......
String content;
HttpGet httpget;
URI uri = uriBuilder.build();
httpget = new HttpGet(uri);
LOGGER.info("request:\n {} {} \n", httpget.getMethod(), httpget.getURI());
HttpResponse response = httpClient.execute(httpget);
......
return hasAuth;
}
本来调用下流,我是选用 @Value的办法,将恳求下流服务的url注入进来的。为了更高雅的完成功用(默默拿出了《代码整洁之道》),我改成了选用 @FeignClient注解的办法完成,同时将途径装备到了Apollo里面,从而削减代码量。
@FeignClient(name = "Rpc", contextId = "Rpc", url = "${rpc.url}")
public interface Rpc {
@GetMapping(value = "xxx/xxx/query")
Result<List<Object>> getContractDiscounts(@RequestParam("number") String number);
}
紧接着又细心检查了apollo里自己装备的url途径,确认是线上的无疑。那么此时我就更晕了,“测验环境不是运转的好好的么,怎样一到出产就拉胯了呢?”,直到我看到了applicaiton.yml里的装备:
rpc:
url: http://xxx.test.com
显然,Apollo里装备没收效吧,而application.yml内的装备收效了。为了证明我的猜想,我将applicaiton.yml里的代码删掉了,然后从头发动了下服务,调用了下接口,结果报出了这个过错:
Caused by: java.lang.IllegalArgumentException: Illegal character in authority at index 7: http://${rpc.url}
at java.net.URI.create(URI.java:852)
at feign.RequestTemplate.target(RequestTemplate.java:465)
... 162 common frames omitted
公然我的猜测是没错的,为了优先解决问题,我在applicaiton-test.yml中装备了新的接口途径,从头上线后,体系没有报错,且正常运转起来了。尽管代码正常运转起来了,但是我的脑海不仅有了个疑问:“为什么在切换写法前,Apollo装备可以正常覆盖,但是在切换了写法之后,就不行了呢?”
Spring装备机制简介
为了找到问题产生的原因,首先需求了解装备是如安在SpringBoot项目中收效的。查阅材料后,我知道了在SpringBoot中,存在一个名为Application的变量,其间保存着Spring中发动的一切信息。在这一切的变量中,装备信息首要同变量Environment相关,比如JVM参数、环境变量、Apollo装备等装备用PropertySource封装后,存放在Environment里的。
除了存储装备以外,SpringBoot还规划了propertyResolver用于管控当前的装备信息,并担任对装备进行填充。
至于PropertyResolver和PropertySource的关系,形象点来说,PropertyResolver便是一位翻译官,他会根据现有的词典PropertySource对咱们的言语${xxx.url}做翻译,并最终得到所装备的信息。假使字典中没有对应的信息,那么很天然”翻译官”是无法做出翻译的。
因而,不难分析问题的原因应该是切换写法后,装备产生了加载次序上的变化,使得装备解析先于apollo里装备加载,从而呈现解析失利的状况。
装备加载次序整理
认识到问题原因可能是因为装备加载次序导致的,咱们需求对Apollo、@Value、@FeignClient三者的装备加载次序进行了解。
Apollo加载次序整理
首先咱们来了解Apollo的装备加载次序,结合Apollo的文档中的内容,不难得到apollo装备的加载次序会有三种状况:
apollo.bootstrap.enabled | apollo.bootstrap.eagerLoad.enabled | 对应SpringBoot的运转阶段 |
---|---|---|
True | True | prepareEnvironment |
True | False | prepareContext |
False | False | refreshContext |
这儿简单介绍下这三种状况对应的Springboot运转阶段分别担任的功用是:
- prepareEnvironment,是最早加载装备的当地,bootstrap.yml装备、体系发动参数中的环境变量都会在这个阶段被加载。
- prepareContext,首要对上下文做初始化,如设置bean姓名命名器、设置加载.class文件加载器等。
- refreshContext,该阶段首要担任对bean容器进行加载,包括扫描文件得到BeanDefinition和BeanFactory工厂、Bean工厂出产Bean目标、对Bean目标再进行特点注入等作业。
这三个阶段在现有SpringBoot发动过程中次序如下所示:
prepareEnviroment
在preparenEnvironment阶段,Spring会宣布异步音讯ApplicationEnvironmentPreparedEvent,同时名为ConfigFileApplicationListener目标会监听该音讯,并对完成了EnvironmentPostProcessor
接口的目标进行调用。
在Apollo源码中,ApolloApplicationContextInitializer类也完成了EnvironmentPostProcessor
的接口。其完成办法中进行apollo装备的加载。
prepareContext
在prepareContext的阶段,首要依靠于办法applyInitializers。该办法会对一切完成了ApplicationContextInitializer
接口的目标进行调用。在Apollo中,ApolloApplicationContextInitializer类也完成了该接口,并在办法中进行装备加载。
refreshContext
refreshContext为Apollo的默许加载阶段。在refreshContext中,会调用invokeBeanFactoryPostProcessors办法对完成了BeanFactoryPostProcessor
接口的目标进行调用。在apollo源码中,目标PropertySourcesProcessor就完成了该接口。且该目标在postProcessBeanFactory办法中,进行了对装备信息的加载。
小结
由此整理下来,Apollo三个阶段的加载次序及装备控制逻辑,如下图所示:
@Value 加载次序整理
了解了apollo的加载次序后。咱们要了解下@Value的加载次序,@Value的完成思想很朴实,当你的Bean目标创建好后,我再把特点通过getter、setter办法注入进去,就完成注入的功用。
因而@Value的完成首要在Bean生成后。在refreshContext阶段,会调用finishBeanFactoryInitialization办法对一切单例bean目标做初始化逻辑。其间在AbstractAutowireCapableBeanFactory会有一个办法populateBean,其会对bean特点做填充。同上述相似,这儿也会对一切承继了BeanPostProcessor
接口的目标进行调用。其间包括一个特别的目标AutowiredAnnotationBeanPostProcessor
AutowiredAnnotationBeanPostProcessor会将用@Value注解润饰的目标扫描出来,并从装备中找到对应的装备信息,注入到目标中。结合上述apollo装备加载次序图,咱们可以得到@Value和Apollo的装备优先级大约如下所示:
可以看到,@Value的装备晚于apollo的装备,因而在切换写法前,apollo的装备可以被正常注入。
@FeignClient 加载次序整理
了解完@Value的加载次序后,咱们还需求了解下@FeignClient的装备加载次序。关于FeignClient来说,它通常选用接口做完成,因而需求根据@FeignClient生成新的Bean目标,并注册到容器中。因而,其装备的加载次序在Bean目标生成之前。
类ConfigurationClassPostProcessor承继自接口AutowiredAnnotationBeanPostProcessor
,其postProcessBeanDefinitionRegistry办法会对BeanDefinition做注入处理。(BeanDefinition,简写为BeanDef,是Bean容器未生成的形态,如果将Bean比作一辆汽车,那么BeanDefinition便是汽车的图纸。)
同时,类ConfigurationClassBeanDefinitionReader会调用loadBeanDefinitionsFromRegistrars办法,该办法会将完成了ImportBeanDefinitionRegistrar
接口的目标逐一进行调用。这其间包括一个FeignClientsRegistrar目标,其完成的registerFeignClients办法会扫描一切被@FeignClient注解的目标。
同时,对单个BeanDef目标,还会调用FeignClientsRegistrar下的registerFeignClient办法做处理,将咱们其间的url、path等特点都用propertyResolver做翻译处理,假使此时,装备中不存在相应的特点,就不会更新。这便是造成本次问题的要害点。
关注到加载次序上,@FeignClient注解所依靠的接口为BeanDefinitionRegistryPostProcessor
,而Apollo中默许加载的状况则依靠于BeanFactoryPostProcessor
接口。两者几乎在同一处办法调用内,但BeanDefinitionRegistryPostProcessor
接口执行略微先于BeanFactoryPostProcessor
。因而在加载次序上,@FeignClient会先于默许状况下的Apollo加载。
至此也就不难理解为什么Apollo注解无法收效了。因为在@FeignClient注解的状况下,beanDef注入时,apollo的装备还没有加载,PropertyResolver找不到对应的装备,天然也就无法进行注入了。
总结
在了解了上述装备的效果机制后,我在本来代码中添加了apollo.bootstrap.enabled=true,将Apollo的装备加载提前到了FeignClient加载前,然后从头运转代码,项目公然如幻想中的正常运转起来。
我抱着的《代码整洁之道》,放声大笑。
参考文章
FeignClient装备Apollo动态url不收效问题
apollo 装备提前加载
apollo官方文档 – 运用文档
添加EnvironmentPostProcessor处理,将Apollo装备加载提到初始化日志体系之前
Spring Boot 2.2.6 源码之旅四十七@Value原理详解
注解 @EnableFeignClients 作业原理