问题回顾

​ 前天,日常上线了个小迭代。内容是:将接口A切换成了接口B,需求很小,QA也没想着测,就让我自测后走免测上线了。开发完成后,赶紧部署到测验环境验证了下,没啥问题,perfect!可以上线了。

一个Springboot配置顺序问题,让我直接回滚代码了

​ 我兴奋地在线上一通构建,程序很快上线了。没一会,发现体系张狂报错。瞅着过错栈里调用的接口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
一个Springboot配置顺序问题,让我直接回滚代码了

显然,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用于管控当前的装备信息,并担任对装备进行填充。

一个Springboot配置顺序问题,让我直接回滚代码了

​ 至于PropertyResolverPropertySource的关系,形象点来说,PropertyResolver便是一位翻译官,他会根据现有的词典PropertySource对咱们的言语${xxx.url}做翻译,并最终得到所装备的信息。假使字典中没有对应的信息,那么很天然”翻译官”是无法做出翻译的。

一个Springboot配置顺序问题,让我直接回滚代码了
​ 因而,不难分析问题的原因应该是切换写法后,装备产生了加载次序上的变化,使得装备解析先于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运转阶段分别担任的功用是:

  1. prepareEnvironment,是最早加载装备的当地,bootstrap.yml装备体系发动参数中的环境变量都会在这个阶段被加载。
  2. prepareContext,首要对上下文初始化,如设置bean姓名命名器、设置加载.class文件加载器等。
  3. refreshContext,该阶段首要担任对bean容器进行加载,包括扫描文件得到BeanDefinition和BeanFactory工厂、Bean工厂出产Bean目标、对Bean目标再进行特点注入等作业。

​ 这三个阶段在现有SpringBoot发动过程中次序如下所示:

一个Springboot配置顺序问题,让我直接回滚代码了

prepareEnviroment

​ 在preparenEnvironment阶段,Spring会宣布异步音讯ApplicationEnvironmentPreparedEvent,同时名为ConfigFileApplicationListener目标会监听该音讯,并对完成了EnvironmentPostProcessor接口的目标进行调用。

一个Springboot配置顺序问题,让我直接回滚代码了

​ 在Apollo源码中,ApolloApplicationContextInitializer类也完成了EnvironmentPostProcessor的接口。其完成办法中进行apollo装备的加载。

一个Springboot配置顺序问题,让我直接回滚代码了

prepareContext

​ 在prepareContext的阶段,首要依靠于办法applyInitializers。该办法会对一切完成了ApplicationContextInitializer接口的目标进行调用。在Apollo中,ApolloApplicationContextInitializer类也完成了该接口,并在办法中进行装备加载。

一个Springboot配置顺序问题,让我直接回滚代码了

refreshContext

refreshContext为Apollo的默许加载阶段。在refreshContext中,会调用invokeBeanFactoryPostProcessors办法对完成了BeanFactoryPostProcessor接口的目标进行调用。在apollo源码中,目标PropertySourcesProcessor就完成了该接口。且该目标在postProcessBeanFactory办法中,进行了对装备信息的加载。

一个Springboot配置顺序问题,让我直接回滚代码了

小结

由此整理下来,Apollo三个阶段的加载次序及装备控制逻辑,如下图所示:

一个Springboot配置顺序问题,让我直接回滚代码了

@Value 加载次序整理

​ 了解了apollo的加载次序后。咱们要了解下@Value的加载次序,@Value的完成思想很朴实,当你的Bean目标创建好后,我再把特点通过getter、setter办法注入进去,就完成注入的功用。

​ 因而@Value的完成首要在Bean生成后。在refreshContext阶段,会调用finishBeanFactoryInitialization办法对一切单例bean目标做初始化逻辑。其间在AbstractAutowireCapableBeanFactory会有一个办法populateBean,其会对bean特点做填充。同上述相似,这儿也会对一切承继了BeanPostProcessor接口的目标进行调用。其间包括一个特别的目标AutowiredAnnotationBeanPostProcessor

一个Springboot配置顺序问题,让我直接回滚代码了

AutowiredAnnotationBeanPostProcessor会将用@Value注解润饰的目标扫描出来,并从装备中找到对应的装备信息,注入到目标中。结合上述apollo装备加载次序图,咱们可以得到@Value和Apollo的装备优先级大约如下所示:

一个Springboot配置顺序问题,让我直接回滚代码了

​ 可以看到,@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注解的目标。

一个Springboot配置顺序问题,让我直接回滚代码了
​ 同时,对单个BeanDef目标,还会调用FeignClientsRegistrar下的registerFeignClient办法做处理,将咱们其间的url、path等特点都用propertyResolver做翻译处理,假使此时,装备中不存在相应的特点,就不会更新。这便是造成本次问题的要害点。

一个Springboot配置顺序问题,让我直接回滚代码了

​ 关注到加载次序上,@FeignClient注解所依靠的接口为BeanDefinitionRegistryPostProcessor,而Apollo中默许加载的状况则依靠于BeanFactoryPostProcessor接口。两者几乎在同一处办法调用内,但BeanDefinitionRegistryPostProcessor接口执行略微先于BeanFactoryPostProcessor。因而在加载次序上,@FeignClient会先于默许状况下的Apollo加载

一个Springboot配置顺序问题,让我直接回滚代码了

​ 至此也就不难理解为什么Apollo注解无法收效了。因为在@FeignClient注解的状况下,beanDef注入时,apollo的装备还没有加载,PropertyResolver找不到对应的装备,天然也就无法进行注入了。

总结

​ 在了解了上述装备的效果机制后,我在本来代码中添加了apollo.bootstrap.enabled=true,将Apollo的装备加载提前到了FeignClient加载前,然后从头运转代码,项目公然如幻想中的正常运转起来。

一个Springboot配置顺序问题,让我直接回滚代码了

​ 我抱着的《代码整洁之道》,放声大笑。

参考文章

FeignClient装备Apollo动态url不收效问题

apollo 装备提前加载

apollo官方文档 – 运用文档

添加EnvironmentPostProcessor处理,将Apollo装备加载提到初始化日志体系之前

Spring Boot 2.2.6 源码之旅四十七@Value原理详解

注解 @EnableFeignClients 作业原理