面試官:spring中定義bean的方法有哪些? 我一口氣說出了12種,把面試官征服了。

蘇三說技術發表於2021-07-06

前言

在龐大的java體系中,spring有著舉足輕重的地位,它給每位開發者帶來了極大的便利和驚喜。我們都知道spring是建立和管理bean的工廠,它提供了多種定義bean的方式,能夠滿足我們日常工作中的多種業務場景。

那麼問題來了,你知道spring中有哪些方式可以定義bean?

我估計很多人會說出以下三種:

最近無意間獲得一份BAT大廠大佬寫的刷題筆記,一下子打通了我的任督二脈,越來越覺得演算法沒有想象中那麼難了。

BAT大佬寫的刷題筆記,讓我offer拿到手軟

沒錯,但我想說的是以上三種方式只是開胃小菜,實際上spring的功能遠比你想象中更強大。

各位看官如果不信,請繼續往下看。

1. xml檔案配置bean

我們先從xml配置bean開始,它是spring最早支援的方式。後來,隨著springboot越來越受歡迎,該方法目前已經用得很少了,但我建議我們還是有必要了解一下。

1.1 構造器

如果你之前有在bean.xml檔案中配置過bean的經歷,那麼對如下的配置肯定不會陌生:

<bean id="personService" class="com.sue.cache.service.test7.PersonService">
</bean>

這種方式是以前使用最多的方式,它預設使用了無參構造器建立bean。

當然我們還可以使用有參的構造器,通過<constructor-arg>標籤來完成配置。

<bean id="personService" class="com.sue.cache.service.test7.PersonService">
   <constructor-arg index="0" value="susan"></constructor-arg>
   <constructor-arg index="1" ref="baseInfo"></constructor-arg>
</bean>

其中:

  • index表示下標,從0開始。
  • value表示常量值
  • ref表示引用另一個bean

1.2 setter方法

除此之外,spring還提供了另外一種思路:通過setter方法設定bean所需引數,這種方式耦合性相對較低,比有參構造器使用更為廣泛。

先定義Person實體:

@Data
public class Person {
    private String name;
    private int age;
}

它裡面包含:成員變數name和age,getter/setter方法。

然後在bean.xml檔案中配置bean時,加上<property>標籤設定bean所需引數。

<bean id="person" class="com.sue.cache.service.test7.Person">
   <property name="name" value="susan"></constructor-arg>
   <property name="age" value="18"></constructor-arg>
</bean>

1.3 靜態工廠

這種方式的關鍵是需要定義一個工廠類,它裡面包含一個建立bean的靜態方法。例如:

public class SusanBeanFactory {
    public static Person createPerson(String name, int age) {
        return new Person(name, age);
    }
}

接下來定義Person類如下:

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Person {
    private String name;
    private int age;
}

它裡面包含:成員變數name和age,getter/setter方法,無參構造器和全參構造器。

然後在bean.xml檔案中配置bean時,通過factory-method引數指定靜態工廠方法,同時通過<constructor-arg>設定相關引數。

<bean class="com.sue.cache.service.test7.SusanBeanFactory" factory-method="createPerson">
   <constructor-arg index="0" value="susan"></constructor-arg>
   <constructor-arg index="1" value="18"></constructor-arg>
</bean>

1.4 例項工廠方法

這種方式也需要定義一個工廠類,但裡面包含非靜態的建立bean的方法。

public class SusanBeanFactory {
    public Person createPerson(String name, int age) {
        return new Person(name, age);
    }
}

Person類跟上面一樣,就不多說了。

然後bean.xml檔案中配置bean時,需要先配置工廠bean。然後在配置例項bean時,通過factory-bean引數指定該工廠bean的引用。

<bean id="susanBeanFactory" class="com.sue.cache.service.test7.SusanBeanFactory">
</bean>
<bean factory-bean="susanBeanFactory" factory-method="createPerson">
   <constructor-arg index="0" value="susan"></constructor-arg>
   <constructor-arg index="1" value="18"></constructor-arg>
</bean>

1.5 FactoryBean

不知道大家有沒有發現,上面的例項工廠方法每次都需要建立一個工廠類,不方面統一管理。

這時我們可以使用FactoryBean介面。

public class UserFactoryBean implements FactoryBean<User> {
    @Override
    public User getObject() throws Exception {
        return new User();
    }

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

在它的getObject方法中可以實現我們自己的邏輯建立物件,並且在getObjectType方法中我們可以定義物件的型別。

然後在bean.xml檔案中配置bean時,只需像普通的bean一樣配置即可。

<bean id="userFactoryBean" class="com.sue.async.service.UserFactoryBean">
</bean>

輕鬆搞定,so easy。

注意:getBean("userFactoryBean");獲取的是getObject方法中返回的物件。而getBean("&userFactoryBean");獲取的才是真正的UserFactoryBean物件。

我們通過上面五種方式,在bean.xml檔案中把bean配置好之後,spring就會自動掃描和解析相應的標籤,並且幫我們建立和例項化bean,然後放入spring容器中。

雖說基於xml檔案的方式配置bean,簡單而且非常靈活,比較適合一些小專案。但如果遇到比較複雜的專案,則需要配置大量的bean,而且bean之間的關係錯綜複雜,這樣久而久之會導致xml檔案迅速膨脹,非常不利於bean的管理。

2. Component註解

為了解決bean太多時,xml檔案過大,從而導致膨脹不好維護的問題。在spring2.5中開始支援:@Component@Repository@Service@Controller等註解定義bean。

如果你有看過這些註解的原始碼的話,就會驚奇得發現:其實後三種註解也是@Component

@Component系列註解的出現,給我們帶來了極大的便利。我們不需要像以前那樣在bean.xml檔案中配置bean了,現在只用在類上加Component、Repository、Service、Controller,這四種註解中的任意一種,就能輕鬆完成bean的定義。

@Service
public class PersonService {
    public String get() {
        return "data";
    }
}

其實,這四種註解在功能上沒有特別的區別,不過在業界有個不成文的約定:

  • Controller 一般用在控制層
  • Service 一般用在業務層
  • Repository 一般用在資料層
  • Component 一般用在公共元件上

太棒了,簡直一下子解放了我們的雙手。

不過,需要特別注意的是,通過這種@Component掃描註解的方式定義bean的前提是:需要先配置掃描路徑

目前常用的配置掃描路徑的方式如下:

  1. 在applicationContext.xml檔案中使用<context:component-scan>標籤。例如:
<context:component-scan base-package="com.sue.cache" />
  1. 在springboot的啟動類上加上@ComponentScan註解,例如:
@ComponentScan(basePackages = "com.sue.cache")
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
    }
}
  1. 直接在SpringBootApplication註解上加,它支援ComponentScan功能:
@SpringBootApplication(scanBasePackages = "com.sue.cache")
public class Application {
    
    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
    }
}

當然,如果你需要掃描的類跟springboot的入口類,在同一級或者子級的包下面,無需指定scanBasePackages引數,spring預設會從入口類的同一級或者子級的包去找。

@SpringBootApplication
public class Application {
    
    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
    }
}

此外,除了上述四種@Component註解之外,springboot還增加了@RestController註解,它是一種特殊的@Controller註解,所以也是@Component註解。

@RestController還支援@ResponseBody註解的功能,即將介面響應資料的格式自動轉換成json。

@Component系列註解已經讓我們愛不釋手了,它目前是我們日常工作中最多的定義bean的方式。

3. JavaConfig

@Component系列註解雖說使用起來非常方便,但是bean的建立過程完全交給spring容器來完成,我們沒辦法自己控制。

spring從3.0以後,開始支援JavaConfig的方式定義bean。它可以看做spring的配置檔案,但並非真正的配置檔案,我們需要通過編碼java程式碼的方式建立bean。例如:

@Configuration
public class MyConfiguration {

    @Bean
    public Person person() {
        return new Person();
    }
}

在JavaConfig類上加@Configuration註解,相當於配置了<beans>標籤。而在方法上加@Bean註解,相當於配置了<bean>標籤。

此外,springboot還引入了一些列的@Conditional註解,用來控制bean的建立。

@Configuration
public class MyConfiguration {

    @ConditionalOnClass(Country.class)
    @Bean
    public Person person() {
        return new Person();
    }
}

@ConditionalOnClass註解的功能是當專案中存在Country類時,才例項化Person類。換句話說就是,如果專案中不存在Country類,就不例項化Person類。

這個功能非常有用,相當於一個開關控制著Person類,只有滿足一定條件才能例項化。

spring中使用比較多的Conditional還有:

  • ConditionalOnBean
  • ConditionalOnProperty
  • ConditionalOnMissingClass
  • ConditionalOnMissingBean
  • ConditionalOnWebApplication

如果你對這些功能比較感興趣,可以看看《》,這是我之前寫的一篇文章,裡面做了更詳細的介紹。

下面用一張圖整體認識一下@Conditional家族:

nice,有了這些功能,我們終於可以告別麻煩的xml時代了。

4. Import註解

通過前面介紹的@Configuration和@Bean相結合的方式,我們可以通過程式碼定義bean。但這種方式有一定的侷限性,它只能建立該類中定義的bean例項,不能建立其他類的bean例項,如果我們想建立其他類的bean例項該怎麼辦呢?

這時可以使用@Import註解匯入。

4.1 普通類

spring4.2之後@Import註解可以例項化普通類的bean例項。例如:

先定義了Role類:

@Data
public class Role {
    private Long id;
    private String name;
}

接下來使用@Import註解匯入Role類:

@Import(Role.class)
@Configuration
public class MyConfig {
}

然後在呼叫的地方通過@Autowired註解注入所需的bean。

@RequestMapping("/")
@RestController
public class TestController {

    @Autowired
    private Role role;

    @GetMapping("/test")
    public String test() {
        System.out.println(role);
        return "test";
    }
}

聰明的你可能會發現,我沒有在任何地方定義過Role的bean,但spring卻能自動建立該類的bean例項,這是為什麼呢?

這也許正是@Import註解的強大之處。

此時,有些朋友可能會問:@Import註解能定義單個類的bean,但如果有多個類需要定義bean該怎麼辦呢?

恭喜你,這是個好問題,因為@Import註解也支援。

@Import({Role.class, User.class})
@Configuration
public class MyConfig {
}

甚至,如果你想偷懶,不想寫這種MyConfig類,springboot也歡迎。

@Import({Role.class, User.class})
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
        DataSourceTransactionManagerAutoConfiguration.class})
public class Application {

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
    }
}

可以將@Import加到springboot的啟動類上。

這樣也能生效?

springboot的啟動類一般都會加@SpringBootApplication註解,該註解上加了@SpringBootConfiguration註解。

而@SpringBootConfiguration註解,上面又加了@Configuration註解

所以,springboot啟動類本身帶有@Configuration註解的功能。

意不意外?驚不驚喜?

4.2 Configuration類

上面介紹了@Import註解匯入普通類的方法,它同時也支援匯入Configuration類。

先定義一個Configuration類:

@Configuration
public class MyConfig2 {

    @Bean
    public User user() {
        return  new User();
    }

    @Bean
    public Role role() {
        return new Role();
    }
}

然後在另外一個Configuration類中引入前面的Configuration類:

@Import({MyConfig2.class})
@Configuration
public class MyConfig {
}

這種方式,如果MyConfig2類已經在spring指定的掃描目錄或者子目錄下,則MyConfig類會顯得有點多餘。因為MyConfig2類本身就是一個配置類,它裡面就能定義bean。

但如果MyConfig2類不在指定的spring掃描目錄或者子目錄下,則通過MyConfig類的匯入功能,也能把MyConfig2類識別成配置類。這就有點厲害了喔。

其實下面還有更高階的玩法

swagger作為一個優秀的文件生成框架,在spring專案中越來越受歡迎。接下來,我們以swagger2為例,介紹一下它是如何匯入相關類的。

眾所周知,我們引入swagger相關jar包之後,只需要在springboot的啟動類上加上@EnableSwagger2註解,就能開啟swagger的功能。

其中@EnableSwagger2註解中匯入了Swagger2DocumentationConfiguration類。


該類是一個Configuration類,它又匯入了另外兩個類:

  • SpringfoxWebMvcConfiguration
  • SwaggerCommonConfiguration


SpringfoxWebMvcConfiguration類又會匯入新的Configuration類,並且通過@ComponentScan註解掃描了一些其他的路徑。

SwaggerCommonConfiguration同樣也通過@ComponentScan註解掃描了一些額外的路徑。

如此一來,我們通過一個簡單的@EnableSwagger2註解,就能輕鬆的匯入swagger所需的一系列bean,並且擁有swagger的功能。

還有什麼好說的,狂起點贊,簡直完美。

4.3 ImportSelector

上面提到的Configuration類,它的功能非常強大。但怎麼說呢,它不太適合加複雜的判斷條件,根據某些條件定義這些bean,根據另外的條件定義那些bean。

那麼,這種需求該怎麼實現呢?

這時就可以使用ImportSelector介面了。

首先定義一個類實現ImportSelector介面:

public class DataImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.sue.async.service.User", "com.sue.async.service.Role"};
    }
}

重寫selectImports方法,在該方法中指定需要定義bean的類名,注意要包含完整路徑,而非相對路徑。

然後在MyConfig類上@Import匯入這個類即可:

@Import({DataImportSelector.class})
@Configuration
public class MyConfig {
}

朋友們是不是又發現了一個新大陸?

不過,這個註解還有更牛逼的用途。

@EnableAutoConfiguration註解中匯入了AutoConfigurationImportSelector類,並且裡面包含系統引數名稱:spring.boot.enableautoconfiguration

AutoConfigurationImportSelector類實現了ImportSelector介面。

並且重寫了selectImports方法,該方法會根據某些註解去找所有需要建立bean的類名,然後返回這些類名。其中在查詢這些類名之前,先呼叫isEnabled方法,判斷是否需要繼續查詢。

該方法會根據ENABLED_OVERRIDE_PROPERTY的值來作為判斷條件。

而這個值就是spring.boot.enableautoconfiguration

換句話說,這裡能根據系統引數控制bean是否需要被例項化,優秀。

我個人認為實現ImportSelector介面的好處主要有以下兩點:

  1. 把某個功能的相關類,可以放到一起,方面管理和維護。
  2. 重寫selectImports方法時,能夠根據條件判斷某些類是否需要被例項化,或者某個條件例項化這些bean,其他的條件例項化那些bean等。我們能夠非常靈活的定製化bean的例項化。

4.4 ImportBeanDefinitionRegistrar

我們通過上面的這種方式,確實能夠非常靈活的自定義bean。

但它的自定義能力,還是有限的,它沒法自定義bean的名稱和作用域等屬性。

有需求,就有解決方案。

接下來,我們一起看看ImportBeanDefinitionRegistrar介面的神奇之處。

先定義CustomImportSelector類實現ImportBeanDefinitionRegistrar介面:

public class CustomImportSelector implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        RootBeanDefinition roleBeanDefinition = new RootBeanDefinition(Role.class);
        registry.registerBeanDefinition("role", roleBeanDefinition);

        RootBeanDefinition userBeanDefinition = new RootBeanDefinition(User.class);
        userBeanDefinition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);
        registry.registerBeanDefinition("user", userBeanDefinition);
    }
}

重寫registerBeanDefinitions方法,在該方法中我們可以獲取BeanDefinitionRegistry物件,通過它去註冊bean。不過在註冊bean之前,我們先要建立BeanDefinition物件,它裡面可以自定義bean的名稱、作用域等很多引數。

然後在MyConfig類上匯入上面的類:

@Import({CustomImportSelector.class})
@Configuration
public class MyConfig {
}

我們所熟悉的fegin功能,就是使用ImportBeanDefinitionRegistrar介面實現的:

具體細節就不多說了,有興趣的朋友可以加我微信找我私聊。

5. PostProcessor

除此之外,spring還提供了專門註冊bean的介面:BeanDefinitionRegistryPostProcessor

該介面的方法postProcessBeanDefinitionRegistry上有這樣一段描述:

修改應用程式上下文的內部bean定義登錄檔標準初始化。所有常規bean定義都將被載入,但是還沒有bean被例項化。這允許進一步新增在下一個後處理階段開始之前定義bean。

如果用這個介面來定義bean,我們要做的事情就變得非常簡單了。只需定義一個類實現BeanDefinitionRegistryPostProcessor介面。

@Component
public class MyRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        RootBeanDefinition roleBeanDefinition = new RootBeanDefinition(Role.class);
        registry.registerBeanDefinition("role", roleBeanDefinition);

        RootBeanDefinition userBeanDefinition = new RootBeanDefinition(User.class);
        userBeanDefinition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);
        registry.registerBeanDefinition("user", userBeanDefinition);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    }
}

重寫postProcessBeanDefinitionRegistry方法,在該方法中能夠獲取BeanDefinitionRegistry物件,它負責bean的註冊工作。

不過細心的朋友可能會發現,裡面還多了一個postProcessBeanFactory方法,沒有做任何實現。

這個方法其實是它的父介面:BeanFactoryPostProcessor裡的方法。

在應用程式上下文的標準bean工廠之後修改其內部bean工廠初始化。所有bean定義都已載入,但沒有bean將被例項化。這允許重寫或新增屬性甚至可以初始化bean。

@Component
public class MyPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        DefaultListableBeanFactory registry = (DefaultListableBeanFactory)beanFactory;
        RootBeanDefinition roleBeanDefinition = new RootBeanDefinition(Role.class);
        registry.registerBeanDefinition("role", roleBeanDefinition);

        RootBeanDefinition userBeanDefinition = new RootBeanDefinition(User.class);
        userBeanDefinition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);
        registry.registerBeanDefinition("user", userBeanDefinition);
    }
}

既然這兩個介面都能註冊bean,那麼他們有什麼區別?

  • BeanDefinitionRegistryPostProcessor 更側重於bean的註冊
  • BeanFactoryPostProcessor 更側重於對已經註冊的bean的屬性進行修改,雖然也可以註冊bean。

此時,有些朋友可能會問:既然拿到BeanDefinitionRegistry物件就能註冊bean,那通過BeanFactoryAware的方式是不是也能註冊bean呢?

從下面這張圖能夠看出DefaultListableBeanFactory就實現了BeanDefinitionRegistry介面。

這樣一來,我們如果能夠獲取DefaultListableBeanFactory物件的例項,然後呼叫它的註冊方法,不就可以註冊bean了?

說時遲那時快,定義一個類實現BeanFactoryAware介面:

@Component
public class BeanFactoryRegistry implements BeanFactoryAware {
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        DefaultListableBeanFactory registry = (DefaultListableBeanFactory) beanFactory;
        RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(User.class);
        registry.registerBeanDefinition("user", rootBeanDefinition);

        RootBeanDefinition userBeanDefinition = new RootBeanDefinition(User.class);
        userBeanDefinition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);
        registry.registerBeanDefinition("user", userBeanDefinition);
    }
}

重寫setBeanFactory方法,在該方法中能夠獲取BeanFactory物件,它能夠強制轉換成DefaultListableBeanFactory物件,然後通過該物件的例項註冊bean。

當你滿懷喜悅的執行專案時,發現竟然報錯了:

為什麼會報錯?

spring中bean的建立過程順序大致如下:

BeanFactoryAware介面是在bean建立成功,並且完成依賴注入之後,在真正初始化之前才被呼叫的。在這個時候去註冊bean意義不大,因為這個介面是給我們獲取bean的,並不建議去註冊bean,會引發很多問題。

最近無意間獲得一份BAT大廠大佬寫的刷題筆記,一下子打通了我的任督二脈,越來越覺得演算法沒有想象中那麼難了。

BAT大佬寫的刷題筆記,讓我offer拿到手軟

此外,ApplicationContextRegistry和ApplicationListener介面也有類似的問題,我們可以用他們獲取bean,但不建議用它們註冊bean。

相關文章