扒一扒Bean注入到Spring的那些姿勢

張哥說技術發表於2023-04-14

來源:三友的java日記

大家好,我是三友~~

這篇文章我準備來扒一扒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="
       xmlns="
       xsi:schemaLocation="
       
       /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的那些姿勢

註解宣告

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

@Component + @ComponentScan

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

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

扒一扒Bean注入到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來實現的。

扒一扒Bean注入到Spring的那些姿勢
扒一扒Bean注入到Spring的那些姿勢

講完透過註解的方式來宣告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大致原理圖,整個過程大致分為以下幾個步驟

  • 透過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

扒一扒Bean注入到Spring的那些姿勢

這種方式就不演示了。

手動註冊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。

扒一扒Bean注入到Spring的那些姿勢

而ConfigurableListableBeanFactory可以透過BeanFactoryPostProcessor來獲取

扒一扒Bean注入到Spring的那些姿勢

來個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中的。

扒一扒Bean注入到Spring的那些姿勢

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

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

FactoryBean

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

扒一扒Bean注入到Spring的那些姿勢

當我們透過配置檔案、註解宣告或者是註冊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的方式,有但是不多。

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

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2945713/,如需轉載,請註明出處,否則將追究法律責任。

相關文章