当前位置 : 主页 > 编程语言 > java >

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

来源:互联网 收集:自由互联 发布时间:2022-10-26
问题回顾 前天,日常上线了个小迭代。内容是:将接口A切换成了接口B,需求很小,QA也没想着测,就让我自测后走免测上线了。开发完成后,赶紧部署到测试环境验证了下,没啥问题

问题回顾

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

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

我兴奋地在线上一通构建,程序很快上线了。没一会,发现系统疯狂报错。瞅着错误栈里调用的接口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用于管控当前的配置信息,并负责对配置进行填充。

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

至于PropertyResolver和PropertySource的关系,形象点来说,PropertyResolver就是一位翻译官,他会根据现有的词典PropertySource对我们的语言${xxx.url}做翻译,并最终得到所配置的信息。倘若字典中没有对应的信息,那么很自然"翻译官"是无法做出翻译的。

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

因此,不难分析问题的原因应该是切换写法后,配置发生了加载顺序上的变化,使得配置解析先于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启动过程中顺序如下所示:

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

    prepareEnviroment

    在preparenEnvironment阶段,Spring会发出异步消息ApplicationEnvironmentPreparedEvent,同时名为ConfigFileApplicationListener对象会监听该消息,并对实现了​​EnvironmentPostProcessor​​接口的对象进行调用。

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

    在Apollo源码中,ApolloApplicationContextInitializer类也实现了​​EnvironmentPostProcessor​​的接口。其实现方法中进行apollo配置的加载。

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

    prepareContext

    在prepareContext的阶段,主要依赖于方法applyInitializers。该方法会对所有实现了​​ApplicationContextInitializer​​接口的对象进行调用。在Apollo中,ApolloApplicationContextInitializer类也实现了该接口,并在方法中进行配置加载。

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

    refreshContext

    refreshContext为Apollo的默认加载阶段。在refreshContext中,会调用invokeBeanFactoryPostProcessors方法对实现了​​BeanFactoryPostProcessor​​接口的对象进行调用。在apollo源码中,对象PropertySourcesProcessor就实现了该接口。且该对象在postProcessBeanFactory方法中,进行了对配置信息的加载。

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

    小结

    由此梳理下来,Apollo三个阶段的加载顺序及配置控制逻辑,如下图所示:

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

    @Value 加载顺序梳理

    了解了apollo的加载顺序后。我们要了解下@Value的加载顺序,@Value的实现思想很纯粹,当你的Bean对象创建好后,我再把属性通过getter、setter方法注入进去,就实现注入的功能。

    因此@Value的实现主要在Bean生成后。在refreshContext阶段,会调用finishBeanFactoryInitialization方法对所有单例bean对象做初始化逻辑。其中在AbstractAutowireCapableBeanFactory会有一个方法populateBean,其会对bean属性做填充。同上述类似,这里也会对所有继承了​​BeanPostProcessor​​接口的对象进行调用。其中包含一个特殊的对象AutowiredAnnotationBeanPostProcessor

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

    AutowiredAnnotationBeanPostProcessor会将用@Value注解修饰的对象扫描出来,并从配置中找到对应的配置信息,注入到对象中。结合上述apollo配置加载顺序图,我们可以得到@Value和Apollo的配置优先级大概如下所示:

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

    可以看到,@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配置顺序问题,让我直接回滚代码了_加载_12

    同时,对单个BeanDef对象,还会调用FeignClientsRegistrar下的registerFeignClient方法做处理,将我们其中的url、path等属性都用propertyResolver做翻译处理,倘若此时,配置中不存在相应的属性,就不会更新。这就是造成本次问题的关键点。

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

    ​ 关注到加载顺序上,@FeignClient注解所依赖的接口为​​BeanDefinitionRegistryPostProcessor​​,而Apollo中默认加载的情况则依赖于​​BeanFactoryPostProcessor​​接口。两者几乎在同一处方法调用内,但​​BeanDefinitionRegistryPostProcessor​​接口执行稍微先于​​BeanFactoryPostProcessor​​。因此在加载顺序上,@FeignClient会先于默认情况下的Apollo加载。

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

    至此也就不难理解为什么Apollo注解没法生效了。因为在@FeignClient注解的情况下,beanDef注入时,apollo的配置还没有加载,PropertyResolver找不到对应的配置,自然也就无法进行注入了。

    总结

    在了解了上述配置的作用机制后,我在原本代码中添加了apollo.bootstrap.enabled=true,将Apollo的配置加载提前到了FeignClient加载前,然后重新运行代码,项目果然如想象中的正常运转起来。

    【本文由:高防服务器ip http://www.558idc.com/gfip.html 复制请保留原URL】
    网友评论