生鮮電商網(wǎng)站建設(shè)百度指數(shù)查詢官方網(wǎng)
🚩本文已收錄至專欄:Spring家族學(xué)習(xí)
一.引入
(1) 概述
? 關(guān)于bean的加載方式,spring提供了各種各樣的形式。因為spring管理bean整體上來說就是由spring維護對象的生命周期,所以bean的加載可以從大的方面劃分成2種形式:
- 已知類通過(
類名.class
)交給spring管理 - 已知類名通過(
類名字符串
)并交給spring管理。
兩種形式內(nèi)部其實都一樣,都是通過spring的BeanDefinition
對象初始化spring的bean。
- bean的定義由前期xml配置逐步演化成注解配置,本質(zhì)是一樣的,都是通過反射機制加載類名后創(chuàng)建對象,對象就是spring管控的bean
- @Import注解可以指定加載某一個類作為spring管控的bean,如果被加載的類中還具有@Bean相關(guān)的定義,會被一同加載
- spring開放出了若干種可編程控制的bean的初始化方式,通過分支語句由固定的加載bean轉(zhuǎn)成了可以選擇bean是否加載或者選擇加載哪一種bean
本文介紹八種常見的bean加載方式:
- xml
- xml+注解
- 注解
- @Import導(dǎo)入
- 使用register方法編程形式注冊
- @Import導(dǎo)入實現(xiàn)ImportSelector接口的類
- @Import導(dǎo)入實現(xiàn)ImportBeanDefinitionRegistrar接口的類
- @Import導(dǎo)入實現(xiàn)BeanDefinitionRegistryPostProcessor接口的類
此外還介紹一些相關(guān)涉及知識
- @Bean定義FactoryBean接口
- @ImportResource
- @Configuration注解的proxyBeanMethods屬性
(2) 環(huán)境搭建
在開始講解之前,我們需要先介紹一下測試所用的環(huán)境。
- 創(chuàng)建Maven工程,可以選擇導(dǎo)入如下Spring坐標(biāo)用于測試
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.3.9</version></dependency>
<!-- 演示加載第三方bean--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.16</version></dependency>
- 創(chuàng)建一些用于后續(xù)示例演示的bean
二.八種加載方式
(1) XML方式
? 最初級的bean的加載方式其實可以直擊spring管控bean的核心思想,就是提供類名,然后spring就可以管理了。所以第一種方式就是給出bean的類名,至于內(nèi)部就是通過反射機制加載成class。
- 創(chuàng)建Spring的xml配置文件,通過其中的
<bean/>
標(biāo)簽加載bean,我們可以在其中聲明加載自己創(chuàng)建的bean,也可以加載第三方開發(fā)的bean。
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd"><!--xml方式聲明 自己 開發(fā)的bean--><bean class="com.guanzhi.bean.Cat"/><bean class="com.guanzhi.bean.Dog"/><!--xml方式聲明 第三方 開發(fā)的bean--><bean class="com.alibaba.druid.pool.DruidDataSource"/></beans>
- 我們可以測試一下是否成功加載了這些bean
public class App {public static void main(String[] args) {// 加載配置文件ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");// 獲取所有已加載bean的名稱String[] names = ctx.getBeanDefinitionNames();// 打印查看for (String name : names) {System.out.println(name);}}
}
- 啟動我們可以發(fā)現(xiàn)成功加載到了上述配置的三個bean
(2) XML+注解方式
? 由于方式一種需要將spring管控的bean全部寫在xml文件中,對于程序員來說非常不友好,所以就有了第二種方式。哪一個類要受到spring管控加載成bean,就在這個類的上面加一個注解,還可以順帶起一個bean的名字(id)。這里可以使用的注解有@Component以及三個衍生注解@Service、@Controller、@Repository。
- 例如我們可以在上述Cat類和Mouse類中加上注解
package com.guanzhi.bean;@Component("tom")
public class Cat {
}
package com.guanzhi.bean;@Service
public class Mouse {
}
? 當(dāng)然,由于我們無法在第三方提供的技術(shù)源代碼中去添加上述4個注解,因此當(dāng)你需要加載第三方開發(fā)的bean的時候可以使用下列方式定義注解式的bean。將@Bean
定義在一個方法上方,當(dāng)前方法的返回值就可以交給spring管控,注意,這個方法所在的類一定要定義在@Configuration修飾的類中。
package com.guanzhi.config;@Configuration
public class DbConfig {@Beanpublic DruidDataSource dataSource(){DruidDataSource ds = new DruidDataSource();return ds;}
}
? 2. 僅僅如此還是不夠,上面提供的只是bean的聲明,spring并沒有感知到這些東西。想讓spring感知到這些聲明,必須設(shè)置spring去檢查這些類。我們可以通過下列xml配置設(shè)置spring去檢查哪些包,發(fā)現(xiàn)定了對應(yīng)注解,就將對應(yīng)的類納入spring管控范圍,聲明成bean。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:context="http://www.springframework.org/schema/context"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd"><!--指定掃描加載bean的位置--><context:component-scan base-package="com.guanzhi.bean,com.guanzhi.config"/>
</beans>
- 同樣我們可以測試一下是否成功加載了這些bean
public class App {public static void main(String[] args) {// 加載配置文件ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");// 獲取所有已加載bean的名稱String[] names = ctx.getBeanDefinitionNames();// 打印查看for (String name : names) {System.out.println(name);}}
}
- 啟動我們可以發(fā)現(xiàn)成功加載到了上述配置的三個bean
方式二聲明bean的方式是目前企業(yè)中較為常見的bean的聲明方式,但是也有缺點。方式一中,通過一個配置文件,你可以查閱當(dāng)前spring環(huán)境中定義了多少個或者說多少種bean,但是方式二沒有任何一個地方可以查閱整體信息,只有當(dāng)程序運行起來才能感知到加載了多少個bean。
(3) 注解方式聲明配置類
? 方式二已經(jīng)完美的簡化了bean的聲明,以后再也不用寫茫茫多的配置信息了。仔細(xì)觀察xml配置文件,會發(fā)現(xiàn)這個文件中只剩了掃描包這句話,于是就有人提出,使用java類替換掉這種固定格式的配置,所以下面這種格式就出現(xiàn)了。
- 定義一個類并使用
@ComponentScan
替代原始xml配置中的包掃描這個動作,其實功能基本相同。
@ComponentScan({"com.guanzhi.bean","com.guanzhi.config"})
public class SpringConfig {
}
- 同樣我們可以測試一下是否成功加載了這些bean
public class App {public static void main(String[] args) {// 加載配置文件ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);// 獲取所有已加載bean的名稱String[] names = ctx.getBeanDefinitionNames();// 打印查看for (String name : names) {System.out.println(name);}}
}
- 啟動我們可以發(fā)現(xiàn)成功加載了這些bean
(4) @Import注解注入
? 使用掃描的方式加載bean是企業(yè)級開發(fā)中常見的bean的加載方式,但是由于掃描的時候不僅可以加載到你要的東西,還有可能加載到各種各樣的亂七八糟的東西。
? 有人就會奇怪,會有什么問題呢?比如你掃描了com.guanzhi.service包,后來因為業(yè)務(wù)需要,又掃描了com.guanzhi.dao包,你發(fā)現(xiàn)com.guanzhi包下面只有service和dao這兩個包,這就簡單了,直接掃描com.guanzhi就行了。但是萬萬沒想到,十天后你加入了一個外部依賴包,里面也有com.guanzhi包,這下便加載了許多不需要的東西。
? 所以我們需要一種精準(zhǔn)制導(dǎo)的加載方式,使用@Import
注解就可以解決你的問題。它可以加載所有的一切,只需要在注解的參數(shù)中寫上加載的類對應(yīng)的.class即可。有人就會覺得,還要自己手寫,多麻煩,不如掃描好用。 但是他可以指定加載啊,好的命名規(guī)范配合@ComponentScan可以解決很多問題,但是@Import注解擁有其重要的應(yīng)用場景。有沒有想過假如你要加載的bean沒有使用@Component修飾呢?這下就無解了,而@Import就無需考慮這個問題。
@Import({Dog.class})
public class SpringConfig {
}
被導(dǎo)入的bean無需使用注解聲明為bean
public class Dog {
}
此形式可以有效的降低源代碼與Spring技術(shù)的耦合度,在spring技術(shù)底層及諸多框架的整合中大量使用
除了加載bean,還可以使用@Import注解加載配置類。其實本質(zhì)上是一樣的。
@Import(Dbconfig.class)
public class SpringConfig {
}
(5) 編程形式注冊bean
? 前面介紹的加載bean的方式都是在容器啟動階段完成bean的加載,下面這種方式就比較特殊了,可以在容器初始化完成后手動加載bean。通過這種方式可以實現(xiàn)編程式控制bean的加載。
public class App {public static void main(String[] args) {AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);//上下文容器對象已經(jīng)初始化完畢后,手工加載beanctx.register(Mouse.class);ctx.register(Dog.class);ctx.register(Cat.class);}
}
? 其實這種方式坑還是挺多的,比如容器中已經(jīng)有了某種類型的bean,再加載會不會覆蓋呢?這都是要思考和關(guān)注的問題。
public class App {public static void main(String[] args) {AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);//上下文容器對象已經(jīng)初始化完畢后,手工加載beanctx.registerBean("tom", Cat.class,0);ctx.registerBean("tom", Cat.class,1);ctx.registerBean("tom", Cat.class,2);System.out.println(ctx.getBean(Cat.class));}
}
運行可以發(fā)現(xiàn),后加載的覆蓋了之前加載的
(6) 導(dǎo)入實現(xiàn)ImportSelector接口的類
? 在方式五中,我們感受了bean的加載可以進(jìn)行編程化的控制,添加if語句就可以實現(xiàn)bean的加載控制了。但是畢竟是在容器初始化后實現(xiàn)bean的加載控制,那是否可以在容器初始化過程中進(jìn)行控制呢?答案是必須的。實現(xiàn)ImportSelector接口的類可以設(shè)置加載的bean的全路徑類名,記得一點,只要能編程就能判定,能判定意味著可以控制程序的運行走向,進(jìn)而控制一切。
public class MyImportSelector implements ImportSelector { @Overridepublic String[] selectImports(AnnotationMetadata metadata) {// 各種條件的判定,判定完畢后,決定是否裝載指定的bean// 判斷是否滿足xx條件,滿足則加載xx,否則xxboolean flag = metadata.hasAnnotation("org.springframework.context.annotation.Configuration");if(flag){return new String[]{"com.guanzhi.bean.Dog"};}return new String[]{"com.guanzhi.bean.Cat"};}
}
在配置類中導(dǎo)入
//@Configuration
@Import(MyImportSelector.class)
public class SpringConfig {
}
編寫測試類
public class App {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);String[] names = ctx.getBeanDefinitionNames();for (String name : names) {System.out.println(name);}System.out.println("----------------------");}
}
運行可以發(fā)現(xiàn),根據(jù)MyImportSelector中的判斷條件,如果在SpringConfig加上Configuration注解得打印com.guanzhi.bean.Cat,否則打印com.guanzhi.bean.Dog。如此我們便實現(xiàn)了Bean的動態(tài)加載。
(7) 導(dǎo)入實現(xiàn)ImportBeanDefinitionRegistrar接口的類
? 方式六中提供了給定類全路徑類名控制bean加載的形式,如果對spring的bean的加載原理比較熟悉的小伙伴知道,其實bean的加載不是一個簡簡單單的對象,spring中定義了一個叫做BeanDefinition的東西,它才是控制bean初始化加載的核心。BeanDefinition接口中給出了若干種方法,可以控制bean的相關(guān)屬性。說個最簡單的,創(chuàng)建的對象是單例還是非單例,在BeanDefinition中定義了scope屬性就可以控制這個。
如果你感覺方式六沒有給你開放出足夠的對bean的控制操作,那么方式七你值得擁有。我們可以通過定義一個類,然后實現(xiàn)ImportBeanDefinitionRegistrar
接口的方式定義bean,并且還可以讓你對bean的初始化進(jìn)行更加細(xì)粒度的控制.
public class MyRegistrar implements ImportBeanDefinitionRegistrar {@Overridepublic void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Dog.class).getBeanDefinition();registry.registerBeanDefinition("dog",beanDefinition);}
}
在配置類中導(dǎo)入
@Import(MyRegistrar.class)
public class SpringConfig {
}
測試
public class App {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);String[] names = ctx.getBeanDefinitionNames();for (String name : names) {System.out.println(name);}}
}
(8) 導(dǎo)入實現(xiàn)BeanDefinitionRegistryPostProcessor接口的類
? 上述七種方式都是在容器初始化過程中進(jìn)行bean的加載或者聲明,但是這里有一個bug。這么多種方式,它們之間如果有沖突怎么辦?誰能有最終裁定權(quán)?這是個好問題,當(dāng)某種類型的bean被接二連三的使用各種方式加載后,在你對所有加載方式的加載順序沒有完全理解清晰之前,你還真不知道最后誰說了算。
? BeanDefinitionRegistryPostProcessor,看名字知道,BeanDefinition意思是bean定義,Registry注冊的意思,Post后置,Processor處理器,全稱bean定義后處理器,在所有bean注冊都加載完后,它是最后一個運行的,實現(xiàn)對容器中bean的最終裁定.
public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {@Overridepublic void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Dog.class).getBeanDefinition();registry.registerBeanDefinition("Dog",beanDefinition);}
}
使用與上述一致
三.相關(guān)補充
(1) FactroyBean接口
? spring提供了一個接口FactoryBean
,也可以用于聲明bean,只不過實現(xiàn)了FactoryBean接口的類造出來的對象不是當(dāng)前類的對象,而是FactoryBean接口泛型指定類型的對象。如下列,造出來的bean并不是DogFactoryBean,而是Dog。這有什么用呢?它可以幫助我們在對象初始化前做一些事情。
// 創(chuàng)建一個類實現(xiàn)FactoryBean接口
public class DogFactoryBean implements FactoryBean<Dog> {@Overridepublic Dog getObject() throws Exception {Dog dog = new Dog();// 擴展要做的其他事情.....return dog;}@Overridepublic Class<?> getObjectType() {return Dog.class;}// 是否為單例@Overridepublic boolean isSingleton() {return true;}
}
? 有人說,注釋中的代碼寫入Dog的構(gòu)造方法不就行了嗎?干嘛這么費勁轉(zhuǎn)一圈,還寫個類,還要實現(xiàn)接口,多麻煩啊。還真不一樣,你可以理解為Dog是一個抽象后剝離的特別干凈的模型,但是實際使用的時候必須進(jìn)行一系列的初始化動作。只不過根據(jù)情況不同,初始化動作不同而已。如果寫入Dog,或許初始化動作A當(dāng)前并不能滿足你的需要,這個時候你就要做一個DogB的方案了。如此,你就要做兩個Dog類。而使用FactoryBean接口就可以完美解決這個問題。
? 通常實現(xiàn)了FactoryBean接口的類使用@Bean的形式進(jìn)行加載,當(dāng)然也可以使用@Component去聲明DogFactoryBean,只要被掃描加載到即可。
@ComponentScan({"com.guanzhi.bean","com.guanzhi.config"})
public class SpringConfig {@Beanpublic DogFactoryBean dog(){return new DogFactoryBean();}
}
(2) 注解導(dǎo)入XML配置的bean
? 由于早起開發(fā)的系統(tǒng)大部分都是采用xml的形式配置bean,現(xiàn)在的企業(yè)級開發(fā)基本上不用這種模式了。但是如果你特別幸運,需要基于之前的系統(tǒng)進(jìn)行二次開發(fā),這就尷尬了。新開發(fā)的用注解格式,之前開發(fā)的是xml格式。這個時候可不是讓你選擇用哪種模式的,而是兩種要同時使用。spring提供了一個注解可以解決這個問題,@ImportResource
,在配置類上直接寫上要被融合的xml配置文件名即可,算的上一種兼容性解決方案。
@Configuration
@ComponentScan("com.guanzhi")
@ImportResource("applicationContext.xml")
public class SpringConfig {
}
(3) proxyBeanMethods屬性
? 前面的例子中用到了@Configuration這個注解,它可以保障配置類中使用方法創(chuàng)建的bean的唯一性,使我們得到的對象是從容器中獲取的而不是重新創(chuàng)建的。只需為@Configuration注解設(shè)置proxyBeanMethods屬性值為true即可,由于此屬性默認(rèn)值為true,所以很少看見明確書寫的,除非想放棄此功能。
@Configuration(proxyBeanMethods = true)
public class SpringConfig {@Beanpublic Cat cat(){return new Cat();}
}
? 下面通過容器再調(diào)用上面的cat方法時,得到的就是同一個對象了。注意,必須使用spring容器對象調(diào)用此方法才有保持bean唯一性的特性。此特性在很多底層源碼中有應(yīng)用,在MQ中也應(yīng)用了此特性。
public class App {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);SpringConfig springConfig = ctx.getBean("springConfig", SpringConfig.class);System.out.println(springConfig.cat());System.out.println(springConfig.cat());System.out.println(springConfig.cat());}
}