天天看點

Spring建立bean的幾種方式

作者:漫談網際網路技術
Spring建立bean的幾種方式

spring建立bean的幾種方式

本篇我們講解下使用spring建立bean的幾種方式,建立bean,也可以叫元件注冊,就是把單例bean放到spring容器中。我們定義如下工程結構:

sping
--src
----main
      java
        com.xk.spring (包路徑)
        --bean (普通類所在的包名)
        --config   (用來寫各種配置類)
        --service   (用來編寫服務層)
      resources
        --(本目錄下用于存放各類資源配置檔案,後面測試會用到)
----test
--pom.xml           

在bean包下面定義Person類,用于示範

package com.xk.spring.bean;

/**
 * @author xk
 * @since 2023.04.28 8:44
 */
public class Person {

    private Integer id;
 
    private String name;

    private Integer age;

    public Person(){}
    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "name:"+this.name+" age:"+this.age+" id:"+this.id;
    }
}           

另外再定義一個列印spring容器中bean名稱的方法,在單元測試中會使用。

void printBeanNames(ApplicationContext applicationContext){
        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();

        for (String definitionName : beanDefinitionNames) {
            System.out.println("----"+definitionName);
        }
    }           

1、@Configuration注解

首先我們先說下@Configuration這個注解,在springboot應用中,經常可以看到引用的各類jar包中定義的類中用到了這個注解。該注解隻能作用于類上,在一個類上面加@Configuration注解,就說明該類是一個配置類,該配置類也會作為一個bean被存放到spring容器中,bean的名稱就是類的名稱(首字母小寫)。如下所示,MyConfig是個配置類,spring容器中就擁有了一個bean名稱為myConfig的bean。

package com.xk.spring.config;

import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {

}           

我們編寫一個單元測試,來列印下spring容器中bean的名稱。

@Test
    public void testMyConfig(){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);
        printBeanNames(applicationContext);
    }           

執行testMyConfig單元測試,得到結果如下:

----org.springframework.context.annotation.internalConfigurationAnnotationProcessor
----org.springframework.context.annotation.internalAutowiredAnnotationProcessor
----org.springframework.context.annotation.internalCommonAnnotationProcessor
----org.springframework.context.event.internalEventListenerProcessor
----org.springframework.context.event.internalEventListenerFactory
----myConfig           

前面幾個bean,是spring内置的bean,最後一個bean就是我們定義的這個配置類,bean名稱為myConfig.

2、@Bean注解

@Bean作用于配置類的方法上,要求該方法有傳回值,傳回的值就是spring容器中的bean,bean名稱預設就是取方法的名稱。比如上面的代碼說明建立一個名稱為person的bean。

package com.xk.spring.config;

import com.xk.spring.bean.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {
    @Bean
    public Person onePerson(){
        return new Person("xx",12);
    }
}           

我們再次執行上面的testMyConfig單元測試,會得到如下結果:

----org.springframework.context.annotation.internalConfigurationAnnotationProcessor
----org.springframework.context.annotation.internalAutowiredAnnotationProcessor
----org.springframework.context.annotation.internalCommonAnnotationProcessor
----org.springframework.context.event.internalEventListenerProcessor
----org.springframework.context.event.internalEventListenerFactory
----myConfig
----onePerson           

可以看到,相比于第1步中的結果,spring容器中多了一個叫onePerson的bean,而onePerson也就是方法onePerson的方法名稱。

3、@Import注解

@Import注解隻能作用于類上面,可以導入标記有@Configuration的配置類、ImportSelector和ImportBeanDefinitionRegistrar的實作類,還能導入普通的類(不含spring注解),所謂導入,就是将類的執行個體注冊到spring容器中。該注解必須和@Configuration注解結合使用,而且正常情況下,隻有當某個元件不會被spring自動注冊時(比如當我們使用ComponentScan指定了某個包掃描路徑),才會使用該注解。spring-boot-autoconfigure.jar包大量使用了該注解,感興趣的可以去參考下。

3.1、導入标記有@Configuration的配置類

通過@Import注解導入配置類,會把該配置類中定義的bean都注冊到spring容器中,同時spring也會該該配置類建立一個bean,bean的名稱就是該類的全路徑名。

我們定義一個ImportConfig配置類,内容如下:

package com.xk.spring.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * @author xk
 * @since 2023.04.28 20:03
 */
@Import({MyConfig.class})
@Configuration
public class ImportConfig {
}           

我們在配置類上通過@Import注解導入另外的配置類MyConfig,會把MyConfig配置類以及在MyConfig類中定義(注冊)的bean都導入到spring容器中。

我們編寫一個單元測試,來列印下spring容器中bean的名稱。

----org.springframework.context.annotation.internalConfigurationAnnotationProcessor
----org.springframework.context.annotation.internalAutowiredAnnotationProcessor
----org.springframework.context.annotation.internalCommonAnnotationProcessor
----org.springframework.context.event.internalEventListenerProcessor
----org.springframework.context.event.internalEventListenerFactory
----importConfig
----com.xk.spring.config.MyConfig
----onePerson           

注意,此時容器中不僅有importConfig這個bean,也有MyConfig配置類中定義的onePerson這個bean。而導入的MyConfig配置類在spring容器中對應的bean名稱是MyConfig類的全路徑名com.xk.spring.config.MyConfig。特别提醒,由于MyConfig本身是個配置類,如果spring本身能自動掃描注冊該類(比如配置了@ComponentScan包含config包路徑),那麼我們再通過@Import這種方式導入MyConfig,MyConfig在spring容器中對應的名稱就變成了myConfig,也就是bean的名稱是類名(首字母小寫)。當然,正如前面所說,如果我們的配置類能被spring自動掃描注冊,我們就不會使用@Import這種方式導入配置類。

另外,需要格外注意的是,我們通過@Import導入的類本身會被當做配置類,是以即使此處MyConfig類沒有加@Configuration注解,通過配置@Import({Myconfig.class}),也會把MyConfig類裡面定義的bean注冊到spring容器,此時MyConfig類對應的bean名稱就是類的全路徑名。但我們一般不這麼做,如果一個類中定義了bean,我們很自然會為該類加上@Configuration注解。

3.2、導入ImportSelector的實作類

ImportSelector是個接口,它的定義如下:

public interface ImportSelector {

    /**
     * 傳回需要導入的類的字元串數組,數組中的元素就是類的全路徑名。
     * @param importingClassMetadata 目前标記@Import注解的類的中繼資料資訊
     */
    String[] selectImports(AnnotationMetadata importingClassMetadata);

    /**
     * 定義一個斷言,用來對上面selectImports傳回的類名數組進行過濾,如果某個元素被斷言判定為true,則該元素不會被spring注冊為bean。此處我們先不關注該方法。
     */
    @Nullable
    default Predicate<String> getExclusionFilter() {
        return null;
    }

}           

我們定義一個MyImportSelector類,實作該接口,内容如下:

package com.xk.spring.importselector;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

/**
 * @author xk
 * @since 2023.04.28 21:32
 */
public class MyImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.xk.spring.bean.Person"};
    }
}           

然後我們修改下我們的ImportConfig類,通過@Import導入MyImportSelector類,内容如下:

package com.xk.spring.config;

import com.xk.spring.importselector.MyImportSelector;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * @author xk
 * @since 2023.04.28 20:03
 */
@Import({MyImportSelector.class})
@Configuration
public class ImportConfig {
}
           

最後我們執行上面的testImportConfig單元測試,可以得到下面的結果:

----org.springframework.context.annotation.internalConfigurationAnnotationProcessor
----org.springframework.context.annotation.internalAutowiredAnnotationProcessor
----org.springframework.context.annotation.internalCommonAnnotationProcessor
----org.springframework.context.event.internalEventListenerProcessor
----org.springframework.context.event.internalEventListenerFactory
----importConfig
----com.xk.spring.bean.Person           

可以看到,通過導入MyImportSelector類,spring自動幫我們生成了Person的bean執行個體,而Person對應的bean的名稱就是Person類的全路徑名。

3.3、導入ImportBeanDefinitionRegistrar的實作類

ImportBeanDefinitionRegistrar也是個接口,它的定義如下:

public interface ImportBeanDefinitionRegistrar {

    /**
     * 基于标記@Import注解的類的中繼資料資訊來注冊bean定義資訊(最終spring會根據這些bean定義資訊生成bean)。注意:由于spring bean生命周期的限制,BeanDefinitionRegistryPostProcessor類型的bean不會在此處被注冊。後面我們再來讨論bean的生命周期。
     * @param importingClassMetadata 目前标記@Import注解的類的中繼資料資訊
     * @param registry bean定義注冊中心,所有的bean定義資訊都會被注冊到這裡
     * @param importBeanNameGenerator 被導入的bean名稱的生成政策 
     */
    default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
            BeanNameGenerator importBeanNameGenerator) {

        registerBeanDefinitions(importingClassMetadata, registry);
    }

    /**
     * 基于标記@Import注解的類的中繼資料資訊來注冊bean定義資訊(最終spring會根據這些bean定義資訊生成bean)。注意:由于spring bean生命周期的限制,BeanDefinitionRegistryPostProcessor類型的bean不會在此處被注冊。後面我們再來讨論bean的生命周期。
     * @param importingClassMetadata 目前标記@Import注解的類的中繼資料資訊
     * @param registry bean定義注冊中心,所有的bean定義資訊都會被注冊到這裡
     */
    default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    }

}           

我們定義一個MyImportBeanDefinitionRegistrar類,實作ImportBeanDefinitionRegistrar接口,定義如下:

package com.xk.spring.importbeandefinitionregistrar;

import com.xk.spring.bean.Person;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

/**
 * @author xk
 * @since 2023.04.28 21:45
 */
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        boolean flag = registry.containsBeanDefinition("com.xk.spring.bean.Person");
        if(!flag){
            RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Person.class);
            //注冊person,并且自定義bean的名稱為myPerson
            registry.registerBeanDefinition("myPerson",rootBeanDefinition);
        }
    }
}
           

然後我們修改下我們的ImportConfig配置類,使用@Import導入MyImportBeanDefinitionRegistrar類,内容如下:

package com.xk.spring.config;

import com.xk.spring.importbeandefinitionregistrar.MyImportBeanDefinitionRegistrar;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * @author xk
 * @since 2023.04.28 20:03
 */
@Import({MyImportBeanDefinitionRegistrar.class})
@Configuration
public class ImportConfig {
}
           

最後我們執行testImportConfig單元測試,會得到如下的結果:

----org.springframework.context.annotation.internalConfigurationAnnotationProcessor
----org.springframework.context.annotation.internalAutowiredAnnotationProcessor
----org.springframework.context.annotation.internalCommonAnnotationProcessor
----org.springframework.context.event.internalEventListenerProcessor
----org.springframework.context.event.internalEventListenerFactory
----importConfig
----myPerson
           

可以看到,我們通過這種方式成功的導入Person類對應的bean,而且bean的名稱就是我們自定義的myPerson。

3.4、導入普通的類

我們還可以通過@Import導入普通類到spring容器,導入之後的bean的名稱就是類名的全路徑。這些普通類可以什麼注解都不加,比如沒有@Component注解,也沒有@Configuration注解。就拿我們的Person類來說,我們可以通過@Import({Person.class})的方式直接将其導入spring容器,。

修改ImportConfig配置類,内容如下:

package com.xk.spring.config;

import com.xk.spring.bean.Person;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * @author xk
 * @since 2023.04.28 20:03
 */
@Import({Person.class})
@Configuration
public class ImportConfig {
}
           

最後執行testImportConfig單元測試,會得出如下結果:

----org.springframework.context.annotation.internalConfigurationAnnotationProcessor
----org.springframework.context.annotation.internalAutowiredAnnotationProcessor
----org.springframework.context.annotation.internalCommonAnnotationProcessor
----org.springframework.context.event.internalEventListenerProcessor
----org.springframework.context.event.internalEventListenerFactory
----importConfig
----com.xk.spring.bean.Person           

可以看到,Person類被導入到spring容器,并且對應的bean名稱是Person類的全路徑名。

4、@ImportResource注解

傳統的spring項目通過使用xml配置檔案來定義bean資訊,然後在web.xml中聲明我們的spring的xml配置檔案。現在我們換種方式,我們可以通過使用@ImportResource注解,将整個的xml配置檔案導入到配置類,進而由spring替我們生成對應的bean。

我們在工程的resources資源目錄下建立application-beans.xml檔案(檔案名随便起),内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <!--定義person-->
    <bean id="person" class="com.xk.spring.bean.Person">
        <!-- collaborators and configuration for this bean go here -->
    </bean>

</beans>           

然後我們修改ImportConfig配置類,内容如下:

package com.xk.spring.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;

/**
 * @author xk
 * @since 2023.04.28 20:03
 */
@ImportResource({"classpath:/application-beans.xml"})
@Configuration
public class ImportConfig {
}
           

最後執行testImportResource單元測試,得出結果如下:

----org.springframework.context.annotation.internalConfigurationAnnotationProcessor
----org.springframework.context.annotation.internalAutowiredAnnotationProcessor
----org.springframework.context.annotation.internalCommonAnnotationProcessor
----org.springframework.context.event.internalEventListenerProcessor
----org.springframework.context.event.internalEventListenerFactory
----importConfig
----person           

可以看到,通過@ImportResource的方式,我們把在xml配置檔案中定義的person這個bean注冊到了spring容器中。

5、@ComponentScan注解

@ComponentScan注解可以讓spring友善地識别标記了@Component注解的元件,需要和@Configuration注解一起使用。我們在web開發時,經常會使用@Service、@Repository、@Controller、@Configuration@Component等注解,用來表示MVC不同層次的元件bean。其實前面四種注解的元注解上面都标記了@Component,說明被這四種注解标記的類通過配置@ComponentScan指令,就可以被注冊到spring容器中,而它們對應的bean的名稱就是類名(首字母小寫)。

我們拿@Service為例,在service包下面建立PersonService類,内容如下:

package com.xk.spring.service;

import org.springframework.stereotype.Service;

/**
 * 人員服務層
 * @author xk
 * @since 2023.04.29 7:25
 */
@Service
public class PersonService {
    
}           

内容很簡單,就簡單的通過@Service将該類标記為一個元件。接下來我們修改MyConfig類,在它上面加上@ComponentScan注解,内容如下:

package com.xk.spring.config;

import com.xk.spring.bean.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;


@ComponentScan("com.xk.spring.service")
@Configuration
public class MyConfig {
    
    @Bean
    public Person onePerson(){
        return new Person("xx",18);
    }
}           

最後我們執行下先前定義的testMyConfig單元測試,得到結果如下:

----org.springframework.context.annotation.internalConfigurationAnnotationProcessor
----org.springframework.context.annotation.internalAutowiredAnnotationProcessor
----org.springframework.context.annotation.internalCommonAnnotationProcessor
----org.springframework.context.event.internalEventListenerProcessor
----org.springframework.context.event.internalEventListenerFactory
----myConfig
----personService
----onePerson           

可以看到,容器中多了一個personService的bean,而該bean就是對應被我們标記了@Service注解的PersonService。雖然我們config包下面的ImportConfig類也加了@Configuration注解,但由于它不在包掃描的範圍,是以spring無法将其自動注冊到容器中。

6、使用總結

(1)如果一個類不是ImportSelector或ImportBeanDefinitionRegistrar的子類,當它被@Import導入的時候,它就會被當成一個配置類存在,可以簡單了解為它被加了@Configuration注解,那麼原本在這個類上面的注解(比如@ComponentScan、@Import、@ImportResource等等)就也會生效。

(2)我們通過springboot開發用戶端jar包,提供給其他團隊使用時,如果裡面涉及到多個配置類,并且需要實作自動配置的功能,那麼可以在配置類A上面通過@Import({B.class})将配置類B導入,然後再根據springboot自動配置類編寫規範,将配置類A寫到類路徑下面的META-INF/spring.factories檔案中。

(3)@ComponentScan注解可以讓我們指定掃描的包路徑,假如我們公司裡面用的項目都以com.xk開頭,我們的項目的包名以com.xk.spring開始,其他的團隊的項目包名以com.xk.mybatis開始。如果com.xk.mybatis中也配置了一些元件,但是我們并不想将它們注冊到spring容器中,就可以通過配置@ComponentScan({"com.xk.spring"})的方式隻掃描我們項目下的包,也相當于排除掉其他路徑的包。

結束語

本文先分享到這裡,覺得有收獲的朋友,可以關注我,或者進行分享或收藏,有疑惑的也可以來私聊評論,我會及時進行回複~