在Spring中使用父子分层上下文自定义依赖注入 - EmpathyBroker

19-02-23 banq
              

已经有一段时间了,因为我想要查看在多个Spring上下文中定义的覆盖依赖项的不同选项,所以我决定使用这篇文章来深入研究这个主题。我们来做一些编码!

作为背景,我将遵循这个“ 基础” Spring上下文配置:

@Configuration
public class BaseConfig {
   
   @Bean
   public FooBar fooBar() {
       Foo foo = foo();
       Bar bar = bar();
       return new FooBar(foo, bar);
   }
   
   @Bean
   public Foo foo() {
       return new Foo("foo");
   }
   
   @Bean
   public Bar bar() {
       return new Bar("bar");
   }
}

我想用下面不同的Spring上下文文件中定义的不同实例覆盖上面的 注释@bean的“bar”实例:

@Configuration
public class OverrideBarConfig {

   public static final String OVERRIDE_BAR = "override-bar";

   @Bean
   public Bar bar() {
       return new Bar(OVERRIDE_BAR);
   }
}

目标是使用正确的Bar实现注入适当的FooBar bean 。这个简单的JUnit测试类将帮助我们检查我们的实验是否正确:

public class OverridingBeansTest {

   private ApplicationContext context;

   

   @BeforeEach

   public void setUp() {

       this.context = // Properly create the Spring context;

   }

   

   @Test

   public void testBar() {

       final FooBar fooBar = context.getBean(FooBar.class);

       final Bar bar = context.getBean(Bar.class);

       

       // 这个上下文的Bar实例应该等同于FooBar中的Bar实例

       assertThat(fooBar.getBar()).isSameAs(bar);

       

       // 这里 bar实例应该是  OverrideBarConfig中定义的

       assertThat(OverrideBarConfig.OVERRIDE_BAR).isEqualTo(bar.getBar());

   }

}

简单的方法:单一的上下文

如果我们使用单个Spring上下文,那么该过程实际上非常简单。最后一个bean定义优先于之前的定义,并且一旦Spring构建了整个依赖关系图并为每个bean选择了合适的候选者,就会发生bean依赖关系解析。

@BeforeEach
public void setUp() {
   AnnotationConfigApplicationContext context = 
     new AnnotationConfigApplicationContext(BaseConfig.class, OverrideBarConfig.class);
   this.context = context;
}

在创建上下文时,只需添加OverrideBarConfig作为最后一个配置定义即表示我们的测试通过。到现在为止还挺好。

但是,我真正想要分析的是它如何应用于分层上下文。在我们的EmpathyBroker系统中,我们在许多情况下使用父子上下文,以便为我们的搜索平台上的每个客户提供灵活的方式来定义自定义行为。

我们希望共享一些常见的基础架构和明智的默认设置,但我们必须能够自定义管道中的一些步骤,以使流程适应我们的客户需求。当然,使用单个上下文也可能是一种有效的方法,但这意味着每次我们为客户实例化新的上下文时,我们都必须重新加载所有bean,或至少一组bean,无论是否有任何bean被覆盖或不被覆盖。

使用父子上下文

所以让我们定义我们的分层Spring上下文并运行测试。

@BeforeEach
public void setUp() {
   final AnnotationConfigApplicationContext parentContext =
       new AnnotationConfigApplicationContext(BaseConfig.class);
   
   AnnotationConfigApplicationContext childContext =
       new AnnotationConfigApplicationContext();
   childContext.setParent(parentContext);
   childContext.register(OverrideBarConfig.class);
   childContext.refresh();
   
   this.context = childContext;
}

但是测试失败:

expected specific instance: Bar{bar='override-bar'}
but was                   : Bar{bar='bar'}
at com.eb.tests.config.OverridingBeansTest.testBar(OverridingBeansTest.java:63)

发生了什么?这里FooBar的 bean一旦在父上下文加载时就会创建,由于它没有在子上下文中重新定义,因此Spring使用父上下文中定义的bean并且不会重新创建它。更糟糕的是,如果我们使用fooBar bean 的范围原型,它将被重新实例化,但是bar bean依赖关系将继​​续是父类,因为依赖关系从子上下文解决到父上下文,但反之亦然。这意味着在重新构建FooBar bean时,Spring永远不会在子上下文中查找依赖项,因此不会使用重写的定义。

解决此问题的一种可能方法是在子上下文中重新定义fooBar bean:

@Configuration
public class OverrideBarConfig {
  
   public static final String OVERRIDE_BAR = "override-bar";
  
   @Autowired
   public Foo foo;
   
   @Bean
   public Bar bar() {
       return new Bar(OVERRIDE_BAR);
   }
   
   @Bean
   public FooBar fooBar() {
       return new FooBar(foo, bar());
   }
}

当然,这次测试通过了。看看我们必须做什么,需要进行一些更改才能注入正确的bar实例:

  1. 覆盖fooBar bean定义本身。
  2. 在我们的Configuration类中注入fooBar的每个依赖项,以便能够构建新实例,在本例中是foo bean。

在这个小例子中,这并不是什么大问题,只需要几行代码来调整我们的配置。但是,如果我们在具有非常复杂的依赖图的非常大的Configuration类中考虑到这一点,事情会变得非常快。想想如果fooBar和bar也是另一个bean的依赖关系会发生什么,等等。

在运行时注册新的bean定义

考虑一种使这个过程更加自动化的方法,我们可以使用BeanFactoryPostProcessor。这是一个Spring提供的钩子,允许自定义修改应用程序的上下文bean定义,例如,有机会在运行时更改或添加定义。

请注意,只能在此处理器中处理bean定义。无意中在BeanFactoryPostProcessor中创建bean实例可能会导致意外的行为,因为它会在Spring上下文加载过程中过早地强制进行bean实例化,并可能产生错误的结果。

本质上,我们的想法是使用子定义的bean作为起点来分析父上下文的依赖关系图。如果父上下文中的某些bean定义依赖于子上下文中定义的一个或多个bean,那么我们将定义导出到子上下文并让Spring创建一个新的实例。这可能是我们的BeanFactoryPostProcessor的初始实现:

public class ExportParentBeansFactoryProcessor implements BeanFactoryPostProcessor {
  
   private final GenericApplicationContext parentContext;
  
   public ExportParentBeansFactoryProcessor(GenericApplicationContext parentContext) {
       this.parentContext = parentContext;
   }
  
   public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) 
     throws BeansException {
       // Only "DefaultListableBeanFactory" can register new beans definitions
       if (!(beanFactory instanceof DefaultListableBeanFactory)) {
           throw new IllegalStateException("Not a DefaultListableBeanFactory: " 
                                           + beanFactory.getClass());
       }
     
       DefaultListableBeanFactory factory = (DefaultListableBeanFactory) beanFactory;
     
       // Build the Map of dependant beans, that is, given an specific bean gets the 
       // whole set of beans that depends on it
       final DependencyAnalyzer dependencyAnalyzer = new DependencyAnalyzer();
       final Map<String, Set<String>> dependantBeans = dependencyAnalyzer
         .getDependantBeans(parentContext);
     
       // Temporary map where exported bean definitions are stored
       final Map<String, BeanDefinition> exportedDefinitions = new HashMap<>();
     
       // For each bean definition in the CHILD beanFactory
       for (String localBeanName : beanFactory.getBeanDefinitionNames()) {
           // Get the beans in the PARENT context that depends on it
           final Set<String> parentDependentBeans = dependantBeans
             .getOrDefault(localBeanName, Collections.emptySet());
         
           for (String beanName : parentDependentBeans) {
               // For each bean defined in the PARENT and not overwritten in the 
               // current context, save its definition
               if (!beanFactory.containsBeanDefinition(beanName)) {
                   final BeanDefinition beanDefinition = parentContext
                     .getBeanDefinition(beanName);
                   exportedDefinitions.put(beanName, beanDefinition);
               }
           }
       }
     
       // Now register all collected definitions in the current bean factory
       exportedDefinitions.forEach(factory::registerBeanDefinition);
   }
}

依赖分析器实现超出了本文的范围,尽管使用ConfigurableBeanFactory.getDependenciesForBean方法实现并不困难,但基本上它将bean名称映射到每个依赖bean,无论依赖是否是直接的。

现在,我们需要在创建子上下文时注册ExportParentBeansFactoryProcessor。

@BeforeEach
public void setUp() {
   final AnnotationConfigApplicationContext parentContext = 
     new AnnotationConfigApplicationContext(BaseConfig.class);
   
   AnnotationConfigApplicationContext childContext = 
     new AnnotationConfigApplicationContext();     
   childContext.setParent(parentContext);
   childContext.register(OverrideBarConfig.class);
   childContext.addBeanFactoryPostProcessor(
      new ExportParentBeansFactoryProcessor(parentContext));
   childContext.refresh();
  
   this.context = childContext;
}

但是又失败了:

expected specific instance: Bar{bar='override-bar'}
but was                   : Bar{bar='bar'}
at com.eb.tests.config.OverridingBeansTest.testBar(OverridingBeansTest.java:63)

这次发生了什么?好吧,问题与我们在BaseConfig中定义fooBar bean 的方式有关。我们来看看它:

@Bean
public FooBar fooBar() {
    Foo foo = foo();
    Bar bar = bar();
    return new FooBar(foo, bar);
}

依赖关系在工厂方法中解析。当使用这种依赖性解析时,Spring使用相同的上下文来定义依赖关系来解析bean。因此,Spring使用父上下文中定义的bar bean。

但是,在Spring中,我们还可以使用方法参数指定我们的bean依赖项:

@Bean
public FooBar fooBar(Foo foo, Bar bar) {
    return new FooBar(foo, bar);
}

这样,Spring依赖项解析在调用工厂方法之前发生,并且使用包含bean定义的上下文(在我们的例子中是子上下文,因为我们有导出的定义)来解析bean。使用这种机制,测试通过!

结论

我们已经看到,分析bean的整个依赖图并自动将依赖的定义导出到子上下文似乎是可行的。该实验的目的是能够减少在子上下文中覆盖bean时必须编写的代码量。

有时我们最终会覆盖很多东西,因为我们希望有一个用于其他许多bean的bean。可能仍需要一些有意识的测试来检查这种方法是否存在任何不良行为,但这似乎是一个很好的起点。让我知道你的想法!