扒一扒Bean注入到Spring的那些姿勢,你會幾種?

2023-01-31 15:01:25

大家好,我是三友~~

這篇文章我準備來扒一扒Bean注入到Spring的那些姿勢。

其實關於Bean注入Spring容器的方式網上也有很多相關文章,但是很多文章可能會存在以下常見的問題

  • 注入方式總結的不全
  • 沒有分析可以使用這些注入方式背後的原因
  • 沒有這些注入方式在原始碼中的應用範例
  • ...

所以本文就帶著解決上述的問題的目的來重新梳理一下Bean注入到Spring的那些姿勢。

組態檔

組態檔的方式就是以外部化的設定方式來宣告Spring Bean,在Spring容器啟動時指定組態檔。組態檔方式現在用的不多了,但是為了文章的完整性和連續性,這裡我還是列出來了,知道的小夥伴可以自行跳過這節。

組態檔的型別Spring主要支援xml和properties兩種型別。

xml

在XmlBeanInjectionDemo.xml檔案中宣告一個class為型別為User的Bean

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       "
>


    <bean class="com.sanyou.spring.bean.injection.User"/>

</beans>

User

@Data
@ToString
public class User {

    private String username;

}

測試:

public class XmlBeanInjectionDemo {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:XmlBeanInjectionDemo.xml");
        applicationContext.refresh();
        User user = applicationContext.getBean(User.class);
        System.out.println(user);
    }

}

結果:

User(username=null)

可以看出成功將User注入到Spring中,由於沒有設定username屬性值,所以是null。

properties

除了xml,spring還支援properties組態檔宣告Bean的方式。

如下,在PropertiesBeanInjectionDemo.properties檔案中宣告了class型別為User的Bean,並且設定User的username屬性為sanyou。

user.(class) = com.sanyou.spring.bean.injection.User
user.username = sanyou

測試:

public class PropertiesBeanInjectionDemo {

    public static void main(String[] args) {
        GenericApplicationContext applicationContext = new GenericApplicationContext();
        //建立一個PropertiesBeanDefinitionReader,可以從properties讀取Bean的資訊,將讀到的Bean資訊放到applicationContext中
        PropertiesBeanDefinitionReader propReader = new PropertiesBeanDefinitionReader(applicationContext);
        //建立一個properties檔案對應的Resource物件
        Resource classPathResource = new ClassPathResource("PropertiesBeanInjectionDemo.properties");
        //載入組態檔
        propReader.loadBeanDefinitions(classPathResource);
        applicationContext.refresh();
        User user = applicationContext.getBean(User.class);
        System.out.println(user);
    }

}

結果:

User(username=sanyou)

成功獲取到User物件,並且username的屬性為properties設定的sanyou。

除了可以設定屬性之外還支援其它的設定,如何設定可以檢視PropertiesBeanDefinitionReader類上的註釋。

註解宣告

上一節介紹了通過組態檔的方式來宣告Bean,但是組態檔這種方式最大的缺點就是不方便,因為隨著專案的不斷擴大,可能會產生大量的組態檔。為了解決這個問題,Spring在2.x的版本中開始支援註解的方式來宣告Bean。

@Component + @ComponentScan

這種方式其實就不用多說,在專案中自定義的業務類就是通過@Component及其派生註解(@Service、@Controller等)來注入到Spring容器中的。

在SpringBoot環境底下,一般情況下不需要我們主動呼叫@ComponentScan註解,因為@SpringBootApplication會呼叫@ComponentScan註解,掃描啟動引導類(加了@SpringBootApplication註解的類)所在的包及其子包下所有加了@Component註解及其派生註解的類,注入到Spring容器中。

@Bean

雖然上面@Component + @ComponentScan的這種方式可以將Bean注入到Spring中,但是有個問題那就是對於第三方jar包來說,如果這個類沒加@Component註解,那麼@ComponentScan就掃不到,這樣就無法注入到Spring容器中,所以Spring提供了一種@Bean的方式來宣告Bean。

比如,在使用MybatisPlus的分頁外掛的時候,就可以按如下方式這麼來宣告。

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
}

此時就能將MybatisPlusInterceptor這個Bean注入到Spring容器中。

@Import

@Import註解也可以用來將Bean注入到Spring容器中,@Import註解匯入的類可以分為三種情況:

  • 普通類
  • 類實現了ImportSelector介面
  • 類實現了ImportBeanDefinitionRegistrar介面
普通類

普通類其實就很簡單,就是將@Import匯入的類注入到Spring容器中,這沒什麼好說的。

類實現了ImportSelector介面
public interface ImportSelector {

    String[] selectImports(AnnotationMetadata importingClassMetadata);

    @Nullable
    default Predicate<String> getExclusionFilter() {
        return null;
    }

}

當@Import匯入的類實現了ImportSelector介面的時候,Spring就會呼叫selectImports方法的實現,獲取一批類的全限定名,最終這些類就會被註冊到Spring容器中。

比如如下程式碼中,UserImportSelector實現了ImportSelector,selectImports方法返回User的全限定名

public class UserImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        System.out.println("呼叫 UserImportSelector 的 selectImports 方法獲取一批類限定名");
        return new String[]{"com.sanyou.spring.bean.injection.User"};
    }

}

當使用@Import註解匯入UserImportSelector這個類的時候,其實最終就會把User注入到Spring容器中,如下測試

@Import(UserImportSelector.class)
public class ImportSelectorDemo 
{

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        //將 ImportSelectorDemo 註冊到容器中
        applicationContext.register(ImportSelectorDemo.class);
        applicationContext.refresh();

        User user = applicationContext.getBean(User.class);
        System.out.println(user);
    }

}

執行結果

User(username=null)

對於類實現了ImportBeanDefinitionRegistrar介面的情況,這個後面說。

一般來說,@Import都是配合@EnableXX這類註解來使用的,比如常見的@EnableScheduling、@EnableAsync註解等,其實最終都是靠@Import來實現的。

@EnableScheduling
@EnableScheduling
@EnableAsync
@EnableAsync

講完通過註解的方式來宣告Bean之後,可以來思考一個問題,那就是既然註解方式這麼簡單,為什麼Spring還寫一堆程式碼來支援組態檔這種宣告的方式?

其實答案很簡單,跟Spring的發展歷程有關。Spring在建立之初Java還不支援註解,所以只能通過組態檔的方式來宣告Bean,在Java1.5版本開始支援註解之後,Spring才開始支援通過註解的方式來宣告Bean。

註冊BeanDefinition

在說註冊BeanDefinition之前,先來聊聊什麼是BeanDefinition?

BeanDefinition是Spring Bean建立環節中很重要的一個東西,它封裝了Bean建立過程中所需要的元資訊。

public interface BeanDefinition extends AttributeAccessorBeanMetadataElement {
    //設定Bean className
    void setBeanClassName(@Nullable String beanClassName);

    //獲取Bean className
    @Nullable
    String getBeanClassName();
    
    //設定是否是懶載入
    void setLazyInit(boolean lazyInit);

    //判斷是否是懶載入
    boolean isLazyInit();
    
    //判斷是否是單例
    boolean isSingleton();

}

如上程式碼是BeanDefinition介面的部分方法,從這方法的定義名稱可以看出,一個Bean所建立過程中所需要的一些資訊都可以從BeanDefinition中獲取,比如這個Bean的class型別,這個Bean是否是懶載入,這個Bean是否是單例的等等,因為有了這些資訊,Spring才知道要建立一個什麼樣的Bean。

有了BeanDefinition這個概念之後,再來看一下組態檔和註解宣告這些方式往Spring容器注入Bean的原理。

Bean注入到Spring原理
Bean注入到Spring原理

如圖為Bean注入到Spring大致原理圖,整個過程大致分為以下幾個步驟

  • 通過BeanDefinitionReader元件讀取組態檔或者註解的資訊,為每一個Bean生成一個BeanDefinition
  • BeanDefinition生成之後,新增到BeanDefinitionRegistry中,BeanDefinitionRegistry就是用來儲存BeanDefinition
  • 當需要建立Bean物件時,會從BeanDefinitionRegistry中拿出需要建立的Bean對應的BeanDefinition,根據BeanDefinition的資訊來生成Bean
  • 當生成的Bean是單例的時候,Spring會將Bean儲存到SingletonBeanRegistry中,也就是平時說的三級快取中的第一級快取中,以免重複建立,需要使用的時候直接從SingletonBeanRegistry中查詢

好了,通過以上分析我們知道,組態檔和註解宣告的方式其實都是宣告Bean的一種方式,最終都會轉換成BeanDefinition,Spring是基於BeanDefinition的資訊來建立Bean。

既然Spring最終是基於BeanDefinition的資訊來建立Bean,那麼我們是不是可以跳過組態檔和註解宣告的方式,直接通過手動建立和註冊BeanDefinition的方式實現往Spring容器中注入呢?

答案是可以的。

前面說過,BeanDefinition最終會被註冊到BeanDefinitionRegistry中,那麼如何拿到BeanDefinitionRegistry呢?主要有以下兩種方式:

  • ImportBeanDefinitionRegistrar
  • BeanDefinitionRegistryPostProcessor

ImportBeanDefinitionRegistrar

上面在說@Import的時候,關於匯入的類實現了ImportBeanDefinitionRegistrar介面的情況沒有說,主要是因為在這裡說比較合適

public interface ImportBeanDefinitionRegistrar {

   default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,BeanNameGenerator importBeanNameGenerator) {
       registerBeanDefinitions(importingClassMetadata, registry);
   }

   default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
   }

}

ImportBeanDefinitionRegistrar中有兩個方法,方法的引數就是BeanDefinitionRegistry。當@Import匯入的類實現了ImportBeanDefinitionRegistrar介面之後,Spring就會呼叫registerBeanDefinitions方法,傳入BeanDefinitionRegistry。

來個Demo

UserImportBeanDefinitionRegistrar實現ImportBeanDefinitionRegistrar

public class UserImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
        //構建一個 BeanDefinition , Bean的型別為 User
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class)
                //設定User這個Bean的屬性username的值為三友的java日記
                .addPropertyValue("username", "三友的java日記")
                .getBeanDefinition()
;
        //把User的BeanDefinition注入到BeanDefinitionRegistry中
        registry.registerBeanDefinition("user", beanDefinition);
    }

}

測試類

@Import(UserImportBeanDefinitionRegistrar.class)
public class UserImportBeanDefinitionRegistrarDemo 
{

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(UserImportBeanDefinitionRegistrarDemo.class);
        applicationContext.refresh();
        User user = applicationContext.getBean(User.class);
        System.out.println(user);
    }

}

結果

User(username=三友的java日記)

從結果可以看出,成功將User注入到了Spring容器中。

上面的例子中有行程式碼

applicationContext.register(UserImportBeanDefinitionRegistrarDemo.class);

這行程式碼的意思就是把UserImportBeanDefinitionRegistrarDemo這個Bean註冊到Spring容器中,所以這裡其實也算一種將Bean注入到Spring的方式,原理也跟上面一樣,會為UserImportBeanDefinitionRegistrarDemo生成一個BeanDefinition註冊到Spring容器中。

BeanDefinitionRegistryPostProcessor

除了ImportBeanDefinitionRegistrar可以拿到BeanDefinitionRegistry之外,還可以通過BeanDefinitionRegistryPostProcessor拿到BeanDefinitionRegistry

BeanDefinitionRegistryPostProcessor
BeanDefinitionRegistryPostProcessor

這種方式就不演示了。

手動註冊BeanDefinition這種方式還是比較常見的。就比如說OpenFeign在啟用過程中,會為每個標註了@FeignClient註解的介面建立一個BeanDefinition,然後再往Spring中的註冊的,如下是OpenFeign註冊FeignClient的部分程式碼

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrarResourceLoaderAwareEnvironmentAware {

    private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        //構建BeanDefinition,class型別為FeignClientFactoryBean
        BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
        String alias = contextId + "FeignClient";
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias });
        //註冊BeanDefinition
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }
}

註冊建立完成的Bean

上一節說可以跳過組態檔或者是註解,直接通過註冊BeanDefinition以達到將Bean注入到Spring中的目的。

既然已經可以跳過組態檔或者是註解,那麼我們可不可以更激進一步,跳過註冊BeanDefinition這一步,直接往Spring中註冊一個已經建立好的Bean呢?

答案依然是可以的。

因為上面在提到當建立的Bean是單例的時候,會將這個建立完成的Bean儲存到SingletonBeanRegistry中,需要用到直接從SingletonBeanRegistry中查詢。既然最終是從SingletonBeanRegistry中查詢的Bean,那麼直接注入一個建立好的Bean有什麼不可以呢?

既然可以,那麼如何拿到SingletonBeanRegistry呢?

其實拿到SingletonBeanRegistry的方法其實很多,因為ConfigurableListableBeanFactory就繼承了SingletonBeanRegistry介面,所以只要能拿到ConfigurableListableBeanFactory就相當於拿到了SingletonBeanRegistry。

ConfigurableListableBeanFactory類圖
ConfigurableListableBeanFactory類圖

而ConfigurableListableBeanFactory可以通過BeanFactoryPostProcessor來獲取

BeanFactoryPostProcessor
BeanFactoryPostProcessor

來個Demo

RegisterUserBeanFactoryPostProcessor實現BeanFactoryPostProcessor, 往Spring容器中新增一個手動建立的User物件

public class RegisterUserBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        //建立一個User物件
        User user = new User();
        user.setUsername("三友的java日記");
        //將這個User物件注入到Spring容器中
        beanFactory.registerSingleton("user", user);
    }

}

測試

public class RegisterUserDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(RegisterUserBeanFactoryPostProcessor.class);
        applicationContext.refresh();
        User user = applicationContext.getBean(User.class);
        System.out.println(user);
    }

}

結果

User(username=三友的java日記)

從結果還是可以看出,成功從Spring容器中獲取到了User物件。

這種直接將建立好的Bean注入到Spring容器中在Spring框架內部使用的還是比較多的,Spring的一些內建的Bean就是通過這個方式注入到Spring中的。

如上圖,在SpringBoot專案啟動的過程中會往Spring容器中新增兩個建立好的Bean,如果你的程式需要使用到這些Bean,就可以通過依賴注入的方式獲取到。

雖然基於這種方式可以將Bean注入到Spring容器,但是這種方式注入的Bean是不經過Bean的生命週期的,也就是說這個Bean中諸如@Autowired等註解和Bean生命週期相關的回撥都不會生效的,注入到Spring時Bean是什麼樣就是什麼樣,Spring不做處理,僅僅只是做一個儲存作用。

FactoryBean

FactoryBean是一種特殊的Bean的型別,通過FactoryBean也可以將Bean注入到Spring容器中。

FactoryBean
FactoryBean

當我們通過組態檔、註解宣告或者是註冊BeanDenifition的方式,往Spring容器中注入了一個class型別為FactoryBean型別的Bean時候,其實真正注入的Bean型別為getObjectType方法返回的型別,並且Bean的物件是通過getObject方法返回的。

來個Demo

UserFactoryBean實現了FactoryBean,getObjectType返回了User型別,所以這個UserFactoryBean會往Spring容器中注入User這個Bean,並且User物件是通過getObject()方法的實現返回的。

public class UserFactoryBean implements FactoryBean<User{
    @Override
    public User getObject() throws Exception {
        User user = new User();
        user.setUsername("三友的java日記");
        return user;
    }

    @Override
    public Class<?> getObjectType() {
        return User.class;
    }
}

測試

public class UserFactoryBeanDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        //將UserFactoryBean注入到Spring容器中
        applicationContext.register(UserFactoryBean.class);
        applicationContext.refresh();
        User user = applicationContext.getBean(User.class);
        System.out.println(user);
    }

}

結果

User(username=三友的java日記)

成功通過UserFactoryBean將User這個Bean注入到Spring容器中了。

FactoryBean這中注入的方式使用也是非常多的,就拿上面舉例的OpenFeign來說,OpenFeign為每個FeignClient的介面建立的BeanDefinition的Bean的class型別FeignClientFactoryBean就是FactoryBean的實現。

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBeanApplicationContextAware {
    
    // FeignClient介面型別
    private Class<?> type;
    
    @Override
    public Object getObject() throws Exception {
       return getTarget();
    }
    
    @Override
    public Class<?> getObjectType() {
       return type;
    }
}

getObject()方法就會返回介面的動態代理的物件,並且這個代理物件是由Feign建立的,這也就實現了Feign和Spring的整合。

總結

通過以上分析可以看出,將Bean注入到Spring容器中大致可以分為5類:

  • 組態檔
  • 註解宣告
  • 註冊BeanDefinition
  • 註冊建立完成的Bean
  • FactoryBean

以上幾種注入的方式,在日常業務開發中,基本上都是使用註解宣告的方式注入Spring中的;在第三方框架在和Spring整合時,註冊BeanDefinition和FactoryBean這些注入方式也會使用的比較多;至於組態檔和註冊建立完成的Bean的方式,有但是不多。

最後,本文所有的範例程式碼地址:

https://github.com/sanyou3/spring-bean-injection.git

往期熱門文章推薦

RocketMQ訊息短暫而又精彩的一生

寫出漂亮程式碼的45個小技巧

兩萬字盤點那些被玩爛了的設計模式

RocketMQ保姆級教學

三萬字盤點Spring/Boot的那些常用擴充套件點

@Async註解的坑,小心

掃碼或者搜尋關注公眾號 三友的java日記 ,及時乾貨不錯過,公眾號致力於通過畫圖加上通俗易懂的語言講解技術,讓技術更加容易學習,回覆 面試 即可獲得一套面試真題。