組合Java配置
在XML中,我們可以使用<import/>標籤,在一個XML檔案中引入另一個XML檔案,在Java類中,我們同樣可以在一個配置類中用@Import引入另一個配置類,被引入的配置類中的@Bean也會載入到spring容器。程式碼如下:
@Configuration public class ConfigA { @Bean public A a() { return new A(); } } @Configuration @Import(ConfigA.class) public class ConfigB { @Bean public B b() { return new B(); } }
我們將ConfigB作為配置類傳給AnnotationConfigApplicationContext進行spring容器初始化,我們不但可以從spring容器中獲得A和B所對應的bean,還可以得到ConfigA和ConfigB所對應的bean:
public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class); A a = ctx.getBean(A.class); B b = ctx.getBean(B.class); ConfigA configA = ctx.getBean(ConfigA .class); ConfigB configB = ctx.getBean(ConfigB.class); }
事實上,@Import並不要求我們引入一個配置類,我們也可以引入一個普通的類,spring容器同樣會幫我們把這個類初始化成一個bean:
package org.example.config; import org.example.beans.D; import org.springframework.context.annotation.Import; @Import(D.class) public class MyConfig3 { } package org.example.beans; public class D { }
測試用例:
@Test public void test10() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig3.class); System.out.println(ac.getBean(D.class)); }
執行結果:
org.example.beans.D@134593bf
利用@Import,我們可以把一些第三方的外掛引入spring容器初始化為bean,當然,我們也可以在一個方法裡返回外掛的例項,方法用@Bean註解標註。不過,筆者認為,用@Import或者@Bean來引入bean還是分情況的,如果是引入一個類似DataSource的資料來源的bean,我們要在方法裡指定資料來源的地址、使用者名稱、密碼,那麼我們可以用@Bean,如果我們需要引入的第三方外掛不需要設定額外的屬性,則可以用@Import。
我們還可以用@Import來引入一個ImportBeanDefinitionRegistrar介面的實現,spring容器會幫我們回撥ImportBeanDefinitionRegistrar介面的實現,在ImportBeanDefinitionRegistrar介面的實現內,我們可以往spring容器註冊一個BeanDefinition,spring容器會根據BeanDefinition生成一個bean,BeanDefinition用來描述一個bean的class物件,比如這個bean的class是什麼?作用域是單例還是原型?是否懶載入?等等。幾乎可以認為,只要是存在於spring容器中的bean,都會有一個對應的BeanDefinition來描述這個bean。我們先來看下BeanDefinition介面的部分方法:
package org.springframework.beans.factory.config; …… public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { …… //設定bean所對應的class void setBeanClassName(@Nullable String beanClassName); //獲取bean所對應的class String getBeanClassName(); //設定bean的作用域 void setScope(@Nullable String scope); //獲取bean的作用域 String getScope(); //設定bean是否懶載入 void setLazyInit(boolean lazyInit); //判斷bean是否懶載入 boolean isLazyInit(); //設定bean在初始化前需要的依賴項 void setDependsOn(@Nullable String... dependsOn); //獲取bean在初始化前需要的依賴項 String[] getDependsOn(); …… }
ImportBeanDefinitionRegistrar可以讓我們重寫兩個方法,從方法名可以看出,註冊BeanDefinition,第一個方法僅比第二個多一個引數importBeanNameGenerator,為beanName生成器。
public interface ImportBeanDefinitionRegistrar { default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) { registerBeanDefinitions(importingClassMetadata, registry); } default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { } }
我們在MyImportBeanDefinitionRegistrar裡重寫了registerBeanDefinitions,由於BeanDefinition是介面,所以我們用spring編寫好的實現類GenericBeanDefinition進行註冊,我們宣告瞭一個GenericBeanDefinition後並設定其對應的屬性,然後將beanName和BeanDefinition註冊進registry:
package org.example.beans; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.type.AnnotationMetadata; public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClass(D.class); registry.registerBeanDefinition("d", beanDefinition); } } package org.example.beans; public class D { }
我們在MyConfig4中@Import進MyImportBeanDefinitionRegistrar,並在後面的測試用例中把MyConfig4傳入應用上下文進行初始化。
package org.example.config; import org.example.beans.MyImportBeanDefinitionRegistrar; import org.springframework.context.annotation.Import; @Import(MyImportBeanDefinitionRegistrar.class) public class MyConfig4 { }
測試用例中我們根據在MyImportBeanDefinitionRegistrar註冊的beanName獲取bean,從執行結果可以看到,spring會根據我們傳入的BeanDefinition建立bean。
@Test public void test11() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig4.class); System.out.println(ac.getBean("d")); }
執行結果:
org.example.beans.D@4b53f538
關於上面說的BeanDefinition如果看不懂的話可以先略過,後面還會再詳講BeanDefinition。
@DependsOn
有時候我們存在這樣的需求:AService初始化之前,BService必須先初始化,AService不通過屬性或建構函式引數顯式依賴於BService。比如:AService負責產出訊息,BService負責消費訊息,最理想的做法是BService必須先AService初始化,讓訊息能及時消費掉,而spring建立bean的順序是不固定的,所以我們可以使用@DependsOn來要求在初始化AService之前必須先初始化BService。
package org.example.beans; import org.springframework.context.annotation.DependsOn; import org.springframework.stereotype.Component; @Component @DependsOn({"c"}) public class B { public B() { System.out.println("B construct..."); } } package org.example.beans; import org.springframework.stereotype.Component; @Component public class C { public C() { System.out.println("C construct..."); } } package org.example.config; import org.springframework.context.annotation.ComponentScan; @ComponentScan("org.example.beans") public class MyConfig5 { }
測試用例:
@Test public void test12() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig5.class); }
執行結果:
C construct... B construct...
我們在B類宣告對C類的依賴,所以spring在建立bean的先建立C再建立B,大家也可以嘗試下,把B類上的@DependsOn去掉,spring會先建立B再建立C。
FactoryBean
當建立一個bean的邏輯十分複雜,不適合以@Bean來標註方法返回時,我們可以建立一個類並實現FactoryBean介面,然後在FactoryBean的實現中編寫複雜的建立邏輯。我們來看下FactoryBean介面:
public interface FactoryBean<T> { //返回工廠所建立的例項,例項可被共享,取決於這個例項是單例還是原型 T getObject() throws Exception; //返回bean的class物件 Class<?> getObjectType(); //返回bean是否單例 default boolean isSingleton() { return true; } }
我們編寫一個機器人FactoryBean(RobotFactoryBean)用來產生機器人(Robot)物件:
package org.example.beans; import org.springframework.beans.factory.FactoryBean; import org.springframework.stereotype.Component; @Component public class RobotFactoryBean implements FactoryBean<Robot> { @Override public Robot getObject() throws Exception { return new Robot(); } @Override public Class<?> getObjectType() { return Robot.class; } @Override public boolean isSingleton() { return true; } } package org.example.beans; public class Robot { }
一般我們通過beanName來獲取bean時,都是通過類名來獲取,但FactoryBean有點特殊,如果我們要獲取Robot這個bean,需要傳入它的工廠名稱robotFactoryBean,如果我們需要獲取工廠物件本身,則在robotFactoryBean前面加一個'&'符號。
@Test public void test13() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig5.class); Object robot = ac.getBean("robotFactoryBean"); System.out.println(robot); System.out.println(robot.getClass()); Object factoryBean = ac.getBean("&robotFactoryBean"); System.out.println(factoryBean); System.out.println(factoryBean.getClass()); }
執行結果:
org.example.beans.Robot@6b1274d2 class org.example.beans.Robot org.example.beans.RobotFactoryBean@7bc1a03d class org.example.beans.RobotFactoryBean