揭秘Ribbon配置类为什么不能在主应用程序上下文的@ComponentScan中

SpringCloud 的中文分享还是比较少的,例如在自定义Ribbon配置中,网上很多文章都会说明 Ribbon配置类不能在主应用程序上下文的@ComponentScan中 , 否则就会被声明为 共享 , 但是为什么呢? 却少有人谈. 本文结合实际代码揭秘.

写之前先抛出如下问题

  • 一个web应用可以用过多少个 Spring Context(Spring上下文环境)

前情提要

1、写过 SpringBoot 应用的同学都知道,SpringBoot 会自动加载自己包及其子包下边的 class, 如果有其他的 Bean 不在这个里边,那么就需要通过 @Configuration 或者 增加 @ComponentScan 的扫描区域.

2、在通常的web应用中,会存在2个 Spring Context , 分别是

  • org.springframework.web.servlet.FrameworkServlet.CONTEXT web子容器.
  • WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE ROOT , parent 容器.

在这个基础上,可以针对他们进行拓展,每一个 key 单独拓展一个 Spring Context ,参考 ==> AnnotationConfigApplicationContext

揭秘

前一阵弄 注册中心时 需要单独实现一些配置去操作 Ribbon 的配置时,必然会出现 No qualifying bean of type ‘com.netflix.client.config.IClientConfig’ available . 经过深入了解发现如下结论.

  • 每一个 ServiceId 都有她专属的 Spring Context .

这样就可以解释为什么在 XxxxxxRibbonClientConfiguration 获取不到 com.netflix.client.config.IClientConfig. 同时也正是因为这个是 子容器 通过 AnnotationConfigApplicationContext 进行读取的,如果将 配置放置到到主应用程序上下文的@ComponentScan中,则会被主应用的上下文读取,最终被所有的 上下文共享 .

代码配置如下: 分别是

  • 通过 org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.netflix.loadbalancer.RaycloudNacosAutoConfiguration 引入配置文件, 使用注解的形式进行我失败了.
  • 加载自定义的配置.
  1. @Configuration
  2. @EnableConfigurationProperties
  3. @ConditionalOnBean({SpringClientFactory.class})
  4. @ConditionalOnRibbonNacos
  5. @ConditionalOnNacosDiscoveryEnabled
  6. @AutoConfigureAfter({RibbonAutoConfiguration.class})
  7. @RibbonClients(
  8. defaultConfiguration = {RaycloudRibbonClientConfiguration.class}
  9. )
  10. public class RaycloudNacosAutoConfiguration {
  11. }

config:

  1. @Configuration
  2. public class RaycloudRibbonClientConfiguration {
  3. @Bean
  4. @LoadBalanced
  5. public RestTemplate getRestTemplate() {
  6. return new RestTemplate();
  7. }
  8. @Bean
  9. public IRule rule(IClientConfig config) {
  10. RaycloudRule raycloudRule = new RaycloudRule();
  11. raycloudRule.initWithNiwsConfig(config);
  12. return raycloudRule;
  13. }
  14. @Bean
  15. @ConditionalOnMissingBean
  16. public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
  17. return new MyPoolUpdater(config);
  18. }
  19. /**
  20. * {@link RibbonClientConfiguration#ribbonLoadBalancer(IClientConfig, ServerList, ServerListFilter, IRule, IPing, ServerListUpdater)} )}
  21. */
  22. @Bean
  23. @ConditionalOnMissingBean
  24. public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList serverList, ServerListFilter serverListFilter, IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
  25. if (this.propertiesFactory.isSet(ILoadBalancer.class, "client")) {
  26. return this.propertiesFactory.get(ILoadBalancer.class, config, "client");
  27. }
  28. return new MyLoad(config, rule, ping, serverList, serverListFilter, serverListUpdater);
  29. }
  30. @Autowired
  31. private PropertiesFactory propertiesFactory;
  32. }

没有服务实例时销毁服务的Spring上下文

  1. public class MyPoolUpdater extends PollingServerListUpdater {
  2. public MyPoolUpdater(IClientConfig clientConfig) {
  3. super(init(clientConfig));
  4. }
  5. public static IClientConfig config = null;
  6. @Autowired
  7. private SpringClientFactory springClientFactory;
  8. private static IClientConfig init(IClientConfig clientConfig) {
  9. config = clientConfig;
  10. return clientConfig;
  11. }
  12. public MyPoolUpdater(long initialDelayMs, long refreshIntervalMs) {
  13. super(initialDelayMs, refreshIntervalMs);
  14. }
  15. private UpdateAction updateAction;
  16. @Override
  17. public synchronized void start(final UpdateAction updateAction) {
  18. super.start(updateAction);
  19. this.updateAction = updateAction;
  20. }
  21. @Override
  22. public synchronized void stop() {
  23. super.stop();
  24. if (springClientFactory != null) {
  25. try {
  26. Field contexts = springClientFactory.getClass().getSuperclass().getDeclaredField("contexts");
  27. contexts.setAccessible(true);
  28. Map<String, AnnotationConfigApplicationContext> contextMap = (Map<String, AnnotationConfigApplicationContext>) contexts.get(springClientFactory);
  29. AnnotationConfigApplicationContext annotationConfigApplicationContext = contextMap.remove(config.getClientName());
  30. if (annotationConfigApplicationContext != null) {
  31. annotationConfigApplicationContext.close();
  32. }
  33. } catch (Exception e) {
  34. e.printStackTrace();
  35. }
  36. }
  37. }
  38. public UpdateAction getUpdateAction() {
  39. return updateAction;
  40. }
  41. }

SpringClientFactory.

A factory that creates client, load balancer and client configuration instances. It creates a Spring ApplicationContext per client name, and extracts the beans that it needs from there.

创建 Context 的类,English就不解释了.

ServiceId 单独创建上下文

  1. protected AnnotationConfigApplicationContext createContext(String name) {
  2. AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
  3. if (this.configurations.containsKey(name)) {
  4. for (Class<?> configuration : this.configurations.get(name)
  5. .getConfiguration()) {
  6. context.register(configuration);
  7. }
  8. }
  9. for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
  10. if (entry.getKey().startsWith("default.")) {
  11. for (Class<?> configuration : entry.getValue().getConfiguration()) {
  12. context.register(configuration);
  13. }
  14. }
  15. }
  16. context.register(PropertyPlaceholderAutoConfiguration.class,
  17. this.defaultConfigType);
  18. context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
  19. this.propertySourceName,
  20. Collections.<String, Object> singletonMap(this.propertyName, name)));
  21. if (this.parent != null) {
  22. // Uses Environment from parent as well as beans
  23. context.setParent(this.parent);
  24. }
  25. context.setDisplayName(generateDisplayName(name));
  26. context.refresh();
  27. return context;
  28. }

过程也不解释了, 这里反向列一下源代码的过程. (查阅时记得从下往上)

  • RibbonAutoConfiguration#springClientFactory
  • RibbonClientConfigurationRegistrar#registerBeanDefinitions
  • RibbonClients

看完这部分就可以了解为为什么是通过 @RibbonClient@RibbonClients 进入导入了.

补充

  1. public class TestMoreContext {
  2. public static void main(String[] args) {
  3. AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
  4. context.register(Hello.class);
  5. context.refresh();
  6. Object bean = context.getBean("good");
  7. System.out.println("bean:" + bean);
  8. AnnotationConfigApplicationContext lastContext = new AnnotationConfigApplicationContext();
  9. lastContext.register(CC.class);
  10. lastContext.setParent(context);
  11. lastContext.refresh();
  12. Date date = lastContext.getBean("last", Date.class);
  13. System.out.println(date);
  14. String good = context.getBean("good", String.class);
  15. System.out.println("good:" + good);
  16. }
  17. }

2019-09-01