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

Java技术前线

共 6173字,需浏览 13分钟

 ·

2023-06-16 17:28

问题回顾

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

ba75dfad0e6d0a752eed4703c6e8c4cf.webp

我兴奋地在线上一通构建,程序很快上线了。没一会,发现系统疯狂报错。瞅着错误栈里调用的接口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
e0db99da94c08f54a44b1cf54db57ed4.webp

显然,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用于管控当前的配置信息,并负责对配置进行填充。

151911284b01118d873a54ee8b6dbc5a.webp

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

952a05070f5f7ea96812e92eef83fc85.webp

因此,不难分析问题的原因应该是切换写法后,配置发生了加载顺序上的变化,使得配置解析先于apollo里配置加载,从而出现解析失败的情况。

配置加载顺序梳理

认识到问题原因可能是由于配置加载顺序导致的,我们需要对Apollo@Value@FeignClient三者的配置加载顺序进行了解。

Apollo加载顺序梳理

首先我们来了解Apollo的配置加载顺序,结合Apollo的文档中的内容,不难得到apollo配置的加载顺序会有三种情况:

7df77c6410453bf00ed1a158246b368b.webp

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

  • prepareEnvironment,是最早加载配置的地方,bootstrap.yml配置、系统启动参数中的环境变量都会在这个阶段被加载。
  • prepareContext,主要对上下文做初始化,如设置bean名字命名器、设置加载.class文件加载器等。
  • refreshContext,该阶段主要负责对bean容器进行加载,包括扫描文件得到BeanDefinitionBeanFactory工厂、Bean工厂生产Bean对象、对Bean对象再进行属性注入等工作。

这三个阶段在现有SpringBoot启动过程中顺序如下所示:

5af87df07e4dd6ed65cff2551bf2010c.webp
prepareEnviroment

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

49c93787c2fe966a6e55f9767e07125d.webp

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

7bd8430f3da7bab6a2083ee28e981537.webp
prepareContext

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

97d2edafe4694f0aaaac3cba394dd051.webp
refreshContext

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

e153b0845143330877c927cab66ee8ac.webp
小结

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

27aafe973cbe2c6167ef3a80d282b0c9.webp

@Value 加载顺序梳理

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

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

77f8014a43ea332000e93498e9798a3b.webp

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

87608d8c198024dd9206e44652034124.webp

可以看到,@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注解的对象。

b19cfa9a7dad5bc6397ba3cfa59abc32.webp

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

6a0a910756b40455d7946cea824e8091.webp

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

8c3091bb40d2b00d96d4de017cabdba9.webp

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

总结

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

来源:juejin.cn/post/7157687494274711589

    

c3303e2742b0cad1106180825e17eb2f.webp

      


简单、漂亮、容易上手的开源 SAAS 多租户快速开发平台,已开源

你见过哪些目瞪口呆的 Java 代码技巧?

从3s到25ms!看看京东的接口优化技巧,确实很优雅!!

12个超好用的免费在线工具,大大提高生产力,建议收藏!

        

最近面试BAT,整理一份面试资料 Java面试BATJ通关手册 ,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。

获取方式:点“ 在看 ”,关注公众号并回复  Java  领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。

谢谢支持哟

浏览 30
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报