在Spring中使用父子分層上下文自定義依賴注入 - EmpathyBroker
已經有一段時間了,因為我想要檢視在多個Spring上下文中定義的覆蓋依賴項的不同選項,所以我決定使用這篇文章來深入研究這個主題。我們來做一些編碼!
作為背景,我將遵循這個“ 基礎” Spring上下文配置:
@Configuration <b>public</b> <b>class</b> BaseConfig { @Bean <b>public</b> FooBar fooBar() { Foo foo = foo(); Bar bar = bar(); <b>return</b> <b>new</b> FooBar(foo, bar); } @Bean <b>public</b> Foo foo() { <b>return</b> <b>new</b> Foo(<font>"foo"</font><font>); } @Bean <b>public</b> Bar bar() { <b>return</b> <b>new</b> Bar(</font><font>"bar"</font><font>); } } </font>
我想用下面不同的Spring上下文檔案中定義的不同例項覆蓋上面的 註釋@bean的“bar”例項:
@Configuration <b>public</b> <b>class</b> OverrideBarConfig { <b>public</b> <b>static</b> <b>final</b> String OVERRIDE_BAR = <font>"override-bar"</font><font>; @Bean <b>public</b> Bar bar() { <b>return</b> <b>new</b> Bar(OVERRIDE_BAR); } } </font>
目標是使用正確的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 <b>public</b> <b>void</b> setUp() { AnnotationConfigApplicationContext context = <b>new</b> AnnotationConfigApplicationContext(BaseConfig.<b>class</b>, OverrideBarConfig.<b>class</b>); <b>this</b>.context = context; }
在建立上下文時,只需新增OverrideBarConfig作為最後一個配置定義即表示我們的測試通過。到現在為止還挺好。
但是,我真正想要分析的是它如何應用於分層上下文。在我們的EmpathyBroker系統中,我們在許多情況下使用父子上下文,以便為我們的搜尋平臺上的每個客戶提供靈活的方式來定義自定義行為。
我們希望共享一些常見的基礎架構和明智的預設設定,但我們必須能夠自定義管道中的一些步驟,以使流程適應我們的客戶需求。當然,使用單個上下文也可能是一種有效的方法,但這意味著每次我們為客戶例項化新的上下文時,我們都必須重新載入所有bean,或至少一組bean,無論是否有任何bean被覆蓋或不被覆蓋。
使用父子上下文
所以讓我們定義我們的分層Spring上下文並執行測試。
@BeforeEach <b>public</b> <b>void</b> setUp() { <b>final</b> AnnotationConfigApplicationContext parentContext = <b>new</b> AnnotationConfigApplicationContext(BaseConfig.<b>class</b>); AnnotationConfigApplicationContext childContext = <b>new</b> AnnotationConfigApplicationContext(); childContext.setParent(parentContext); childContext.register(OverrideBarConfig.<b>class</b>); childContext.refresh(); <b>this</b>.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 <b>public</b> <b>class</b> OverrideBarConfig { <b>public</b> <b>static</b> <b>final</b> String OVERRIDE_BAR = <font>"override-bar"</font><font>; @Autowired <b>public</b> Foo foo; @Bean <b>public</b> Bar bar() { <b>return</b> <b>new</b> Bar(OVERRIDE_BAR); } @Bean <b>public</b> FooBar fooBar() { <b>return</b> <b>new</b> FooBar(foo, bar()); } } </font>
當然,這次測試通過了。看看我們必須做什麼,需要進行一些更改才能注入正確的bar例項:
- 覆蓋fooBar bean定義本身。
- 在我們的Configuration類中注入fooBar的每個依賴項,以便能夠構建新例項,在本例中是foo bean。
在這個小例子中,這並不是什麼大問題,只需要幾行程式碼來調整我們的配置。但是,如果我們在具有非常複雜的依賴圖的非常大的Configuration類中考慮到這一點,事情會變得非常快。想想如果fooBar和bar也是另一個bean的依賴關係會發生什麼,等等。
在執行時註冊新的bean定義
考慮一種使這個過程更加自動化的方法,我們可以使用BeanFactoryPostProcessor。這是一個Spring提供的鉤子,允許自定義修改應用程式的上下文bean定義,例如,有機會在執行時更改或新增定義。
請注意,只能在此處理器中處理bean定義。無意中在BeanFactoryPostProcessor中建立bean例項可能會導致意外的行為,因為它會在Spring上下文載入過程中過早地強制進行bean例項化,並可能產生錯誤的結果。
本質上,我們的想法是使用子定義的bean作為起點來分析父上下文的依賴關係圖。如果父上下文中的某些bean定義依賴於子上下文中定義的一個或多個bean,那麼我們將定義匯出到子上下文並讓Spring建立一個新的例項。這可能是我們的BeanFactoryPostProcessor的初始實現:
<b>public</b> <b>class</b> ExportParentBeansFactoryProcessor implements BeanFactoryPostProcessor { <b>private</b> <b>final</b> GenericApplicationContext parentContext; <b>public</b> ExportParentBeansFactoryProcessor(GenericApplicationContext parentContext) { <b>this</b>.parentContext = parentContext; } <b>public</b> <b>void</b> postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { <font><i>// Only "DefaultListableBeanFactory" can register new beans definitions</i></font><font> <b>if</b> (!(beanFactory instanceof DefaultListableBeanFactory)) { <b>throw</b> <b>new</b> IllegalStateException(</font><font>"Not a DefaultListableBeanFactory: "</font><font> + beanFactory.getClass()); } DefaultListableBeanFactory factory = (DefaultListableBeanFactory) beanFactory; </font><font><i>// Build the Map of dependant beans, that is, given an specific bean gets the </i></font><font> </font><font><i>// whole set of beans that depends on it</i></font><font> <b>final</b> DependencyAnalyzer dependencyAnalyzer = <b>new</b> DependencyAnalyzer(); <b>final</b> Map<String, Set<String>> dependantBeans = dependencyAnalyzer .getDependantBeans(parentContext); </font><font><i>// Temporary map where exported bean definitions are stored</i></font><font> <b>final</b> Map<String, BeanDefinition> exportedDefinitions = <b>new</b> HashMap<>(); </font><font><i>// For each bean definition in the CHILD beanFactory</i></font><font> <b>for</b> (String localBeanName : beanFactory.getBeanDefinitionNames()) { </font><font><i>// Get the beans in the PARENT context that depends on it</i></font><font> <b>final</b> Set<String> parentDependentBeans = dependantBeans .getOrDefault(localBeanName, Collections.emptySet()); <b>for</b> (String beanName : parentDependentBeans) { </font><font><i>// For each bean defined in the PARENT and not overwritten in the </i></font><font> </font><font><i>// current context, save its definition</i></font><font> <b>if</b> (!beanFactory.containsBeanDefinition(beanName)) { <b>final</b> BeanDefinition beanDefinition = parentContext .getBeanDefinition(beanName); exportedDefinitions.put(beanName, beanDefinition); } } } </font><font><i>// Now register all collected definitions in the current bean factory</i></font><font> exportedDefinitions.forEach(factory::registerBeanDefinition); } } </font>
依賴分析器實現超出了本文的範圍,儘管使用ConfigurableBeanFactory.getDependenciesForBean 方法實現並不困難,但基本上它將bean名稱對映到每個依賴bean,無論依賴是否是直接的。
現在,我們需要在建立子上下文時註冊ExportParentBeansFactoryProcessor。
@BeforeEach <b>public</b> <b>void</b> setUp() { <b>final</b> AnnotationConfigApplicationContext parentContext = <b>new</b> AnnotationConfigApplicationContext(BaseConfig.<b>class</b>); AnnotationConfigApplicationContext childContext = <b>new</b> AnnotationConfigApplicationContext(); childContext.setParent(parentContext); childContext.register(OverrideBarConfig.<b>class</b>); childContext.addBeanFactoryPostProcessor( <b>new</b> ExportParentBeansFactoryProcessor(parentContext)); childContext.refresh(); <b>this</b>.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 <b>public</b> FooBar fooBar() { Foo foo = foo(); Bar bar = bar(); <b>return</b> <b>new</b> FooBar(foo, bar); }
依賴關係在工廠方法中解析。當使用這種依賴性解析時,Spring使用相同的上下文來定義依賴關係來解析bean。因此,Spring使用父上下文中定義的bar bean。
但是,在Spring中,我們還可以使用方法引數指定我們的bean依賴項:
@Bean <b>public</b> FooBar fooBar(Foo foo, Bar bar) { <b>return</b> <b>new</b> FooBar(foo, bar); }
這樣,Spring依賴項解析在呼叫工廠方法之前發生,並且使用包含bean定義的上下文(在我們的例子中是子上下文,因為我們有匯出的定義)來解析bean。使用這種機制,測試通過!
結論
我們已經看到,分析bean的整個依賴圖並自動將依賴的定義匯出到子上下文似乎是可行的。該實驗的目的是能夠減少在子上下文中覆蓋bean時必須編寫的程式碼量。
有時我們最終會覆蓋很多東西,因為我們希望有一個用於其他許多bean的bean。可能仍需要一些有意識的測試來檢查這種方法是否存在任何不良行為,但這似乎是一個很好的起點。讓我知道你的想法!