組合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