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

问题回顾

前天,日常上线了个小迭代。内容是:将接口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配置的加载顺序会有三种情况:

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

这里简单介绍下这三种情况对应的Springboot运行阶段分别负责的功能是:

  • prepareEnvironment,是最早加载配置的地方,bootstrap.yml配置、系统启动参数中的环境变量都会在这个阶段被加载。
  • prepareContext,主要对上下文做初始化,如设置bean名字命名器、设置加载.class文件加载器等。
  • refreshContext,该阶段主要负责对bean容器进行加载,包括扫描文件得到BeanDefinitionBeanFactory工厂、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配置加载顺序图,我们可以得到@ValueApollo的配置优先级大概如下所示:

一个 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加载前,然后重新运行代码,项目果然如想象中的正常运转起来。

来源:juejin.cn/post/7157687494274711589

后端专属技术群

构建高质量的技术交流社群,欢迎从事编程开发、技术招聘HR进群,也欢迎大家分享自己公司的内推信息,相互帮助,一起进步!

文明发言,以交流技术职位内推行业探讨为主

广告人士勿入,切勿轻信私聊,防止被骗

一个 SpringBoot 配置顺序问题,让我直接回滚了代码。。。
加我好友,拉你进群

原文始发于微信公众号(Java知音):一个 SpringBoot 配置顺序问题,让我直接回滚了代码。。。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/147018.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!