Spring MVC系列-(3) Bean的裝配
- 2020 年 3 月 26 日
- 筆記
3. 高級裝配Bean
3.1 Bean的作用域
默認情況下,Spring中的bean都是以單例的形式存在的,無論注入多少次,每次注入的都是同一個實例。
考慮到某些bean可能是可變的,Spring定義了不同的作用域,可以基於這些作用域創建不同的bean,
單例是默認的作用域,如果選擇@Scope
註解選擇其他作用域,這可以和@Component
和@Bean
一起使用。
@Configuration public class Cap3MainConfig { //給容器中註冊一個bean, 類型為返回值的類型, 默認是單實例 /* * prototype:多實例: IOC容器啟動的時候,IOC容器啟動並不會去調用方法創建對象, 而是每次獲取的時候才會調用方法創建對象 * singleton:單實例(默認):IOC容器啟動的時候會調用方法創建對象並放到IOC容器中,以後每次獲取的就是直接從容器中拿(大Map.get)的同一個bean * request: 主要針對web應用, 遞交一次請求創建一個實例 * session:同一個session創建一個實例 */ @Scope("prototype") @Bean public Person person(){ return new Person("vincent",20); } }
3.2 Lazy懶加載
顧名思義,懶加載推遲加載Bean。默認情況下,在IOC容器初始化時,會將各個Bean註冊到容器中;如果在定義Bean時,使用@Lazy
聲明,則該Bean只有在第一次使用時,才會被註冊到IOC容器中。
下面的例子中,person實例將會在第一次被獲取的時候才會初始化。
@Configuration public class Cap4MainConfig { //給容器中註冊一個bean, 類型為返回值的類型, 默認是單實例 /* * 懶加載: 主要針對單實例bean:默認在容器啟動的時候創建對象 * 懶加載: 容器啟動時候不創建對象, 僅當第一次使用(獲取)bean的時候才創建被初始化 */ @Lazy @Bean public Person person(){ System.out.println("給容器中添加person......."); return new Person("vincent",20); } }
可以使用如下測試程序進行測試:
public class Cap4Test { @Test public void test01(){ AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(Cap4MainConfig.class); String[] names = app.getBeanDefinitionNames(); // 此時可以獲取到person的name,但是person依然未實例化 for(String name:names){ System.out.println(name); } System.out.println("IOC容器創建完成........"); // 實例化person app.getBean("person");//執行獲取的時候才創建並初始化bean } }
3.3 Conditional條件註冊Bean
Spring4引入了@Conditional
註解,用於條件化註冊Bean。如果給定的條件,計算結果為true,就會創建這個bean,否則的話,bean會被忽略。
下面的例子中,將IOC容器註冊bean時, 當操作系統為WINDOWS時,註冊Lison實例; 當操作系統為LINUX時, 註冊James實例,此時要用得@Conditional註解進行定製化條件選擇註冊bean;
@Configuration public class Cap5MainConfig { @Bean("person") public Person person(){ System.out.println("給容器中添加person......."); return new Person("person",20); } @Conditional(WinCondition.class) @Bean("lison") public Person lison(){ System.out.println("給容器中添加win......."); return new Person("win",58); } @Conditional(LinCondition.class) @Bean("james")//bean在容器中的ID為james, IOC容器MAP, map.put("id",value) public Person james(){ System.out.println("給容器中添加mac......."); return new Person("mac",20); } }
注意到,我們需要自己實現對應的WinCondition.class
和LinCondition.class
類,以其中的一個為例,如下可以看到,需要實現自己的match函數,
public class LinCondition implements Condition{ /* *ConditionContext: 判斷條件可以使用的上下文(環境) *AnnotatedTypeMetadata: 註解的信息 */ @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { // TODO 是否為WINDOW系統 //能獲取到IOC容器正在使用的beanFactory ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); //獲取當前環境變量(包括我們操作系統是WIN還是LINUX??) Environment environment = context.getEnvironment(); String os_name = environment.getProperty("os.name"); if(os_name.contains("Mac")){ return true; } return false; } }
3.4 import註冊Bean
這節中,首先總結一下Spring中常見的注入Bean的方法。
- @Bean: [導入第三方的類或包的組件],比如Person為第三方的類, 需要在我們的IOC容器中使用
- 包掃描+組件的標註註解(@ComponentScan: @Controller, @Service @Repository,@Component),一般是針對我們自己寫的類。
- @Import:快速給容器導入一個組件
- a, @Import(要導入到容器中的組件):容器會自動註冊這個組件,bean的id為全類名
- b, ImportSelector:是一個接口,返回需要導入到容器的組件的全類名數組。
- c, ImportBeanDefinitionRegistrar:可以手動添加組件到IOC容器, 所有Bean的註冊可以使用BeanDifinitionRegistry,只需要實現。ImportBeanDefinitionRegistrar接口即可
- 使用Spring提供的FactoryBean(工廠bean)進行註冊
前兩種方法在上一章已經介紹了,現在主要介紹剩下兩類。
下面的配置類中,直接將Dog和Cat import到配置中,本身配置類中也定義了person的實例bean以及自定義的factoryBean。
@Configuration @Import(value = { Dog.class,Cat.class, JamesImportSelector.class, JamesImportBeanDefinitionRegistrar.class }) public class Cap6MainConfig { //容器啟動時初始化person的bean實例 @Bean("person") public Person persond(){ System.out.println("aaaaaaaaaaaa"); return new Person("james",20); } @Bean public JamesFactoryBean jamesFactoryBean(){ return new JamesFactoryBean(); } }
在JamesImportSelector.class
實現中,只需要返回所有需要import的class類名即可。
public class JamesImportSelector implements ImportSelector{ @Override public String[] selectImports(AnnotationMetadata importingClassMetadata){ //返回全類名的bean return new String[]{"com.enjoy.cap6.bean.Fish","com.enjoy.cap6.bean.Tiger"}; } }
在JamesImportBeanDefinitionRegistrar.class
中,根據需要可以手動注入需要的bean實例,
public class JamesImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { /* *AnnotationMetadata:當前類的註解信息 *BeanDefinitionRegistry:BeanDefinition註冊類 * 把所有需要添加到容器中的bean加入; * @Scope */ @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { boolean bean1 = registry.containsBeanDefinition("com.enjoy.cap6.bean.Dog"); boolean bean2 = registry.containsBeanDefinition("com.enjoy.cap6.bean.Cat"); //如果Dog和Cat同時存在於我們IOC容器中,那麼創建Pig類, 加入到容器 //對於我們要註冊的bean, 給bean進行封裝, if(bean1 && bean2){ RootBeanDefinition beanDefinition = new RootBeanDefinition(Pig.class); registry.registerBeanDefinition("pig", beanDefinition); } } }
注意到上面,也可以通過FactoryBean的方法來將所需要的bean注入到IOC容器中,在這其中,需要手動實現其中的getObject等方法。
public class JamesFactoryBean implements FactoryBean<Monkey>{ @Override public Monkey getObject() throws Exception { // TODO Auto-generated method stub return new Monkey(); } @Override public Class<?> getObjectType() { // TODO Auto-generated method stub return Monkey.class; } @Override public boolean isSingleton() { return true; } }
下面是實際的測試程序,需要注意的是,直接使用getBean(bean name)是取出FactoryBean裏面封裝的Monkey實例,如果需要拿到FactoryBean本身,需要加上&
符號。
public class Cap6Test { @Test public void test01(){ AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(Cap6MainConfig.class); System.out.println("IOC容器創建完成........"); Object bean1 = app.getBean("jamesFactoryBean"); Object bean2 = app.getBean("jamesFactoryBean");//取Monkey bean System.out.println("bean的類型="+bean1.getClass()); System.out.println(bean1 == bean2); Object bean3 = app.getBean("&jamesFactoryBean");//取factoryBean System.out.println("bean的類型="+bean3.getClass()); // 打印輸出所有bean String[] beanDefinitionNames = app.getBeanDefinitionNames(); for(String name:beanDefinitionNames){ System.out.println(name); } } }
Spring中出現了BeanFactory和FactoryBean,下面對兩者的區別進行解釋:
BeanFactory是個Factory,也就是IOC容器或對象工廠,FactoryBean是個Bean。在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器)來進行管理的。
FactoryBean是一個Bean,這個Bean不是簡單的Bean,而是一個能生產或者修飾對象生成的工廠Bean,它的實現與設計模式中的工廠模式和修飾器模式類似。
1. BeanFactory
BeanFactory,以Factory結尾,表示它是一個工廠類(接口),它負責生產和管理bean的一個工廠。在Spring中,BeanFactory是IOC容器的核心接口,它的職責包括:實例化、定位、配置應用程序中的對象及建立這些對象間的依賴。
BeanFactory只是個接口,並不是IOC容器的具體實現,但是Spring容器給出了很多種實現,如 DefaultListableBeanFactory、XmlBeanFactory、ApplicationContext等,其中XmlBeanFactory就是常用的一個,該實現將以XML方式描述組成應用的對象及對象間的依賴關係。XmlBeanFactory類將持有此XML配置元數據,並用它來構建一個完全可配置的系統或應用。
ApplicationContext包含BeanFactory的所有功能,通常建議比BeanFactory優先 。ApplicationContext以一種更向面向框架的方式工作以及對上下文進行分層和實現繼承,ApplicationContext包還提供了以下的功能:
- MessageSource, 提供國際化的消息訪問
- 資源訪問,如URL和文件
- 事件傳播
- 載入多個(有繼承關係)上下文 ,使得每一個上下文都專註於一個特定的層次,比如應用的web層;
BeanFactory提供的方法及其簡單,僅提供了六種方法供客戶調用:
- boolean containsBean(String beanName) 判斷工廠中是否包含給定名稱的bean定義,若有則返回true
- Object getBean(String) 返回給定名稱註冊的bean實例。根據bean的配置情況,如果是singleton模式將返回一個共享實例,否則將返回一個新建的實例,如果沒有找到指定bean,該方法可能會拋出異常
- Object getBean(String, Class) 返回以給定名稱註冊的bean實例,並轉換為給定class類型
- Class getType(String name) 返回給定名稱的bean的Class,如果沒有找到指定的bean實例,則排除NoSuchBeanDefinitionException異常
- boolean isSingleton(String) 判斷給定名稱的bean定義是否為單例模式
- String[] getAliases(String name) 返回給定bean名稱的所有別名
2. FactoryBean
一般情況下,Spring通過反射機制利用
Spring為此提供了一個org.springframework.bean.factory.FactoryBean的工廠類接口,用戶可以通過實現該接口定製實例化Bean的邏輯。FactoryBean接口對於Spring框架來說佔用重要的地位,Spring自身就提供了70多個FactoryBean的實現。它們隱藏了實例化一些複雜Bean的細節,給上層應用帶來了便利。從Spring3.0開始,FactoryBean開始支持泛型,即接口聲明改為FactoryBean
以Bean結尾,表示它是一個Bean,不同於普通Bean的是:它是實現了FactoryBean
3.5 運行時注入
本節介紹Spring在運行時的兩種常見注入方式,@Value和@Autowired。
@Value
該註解的作用是將我們配置文件的屬性讀出來,有@Value(「${}」)
和@Value(「#{}」)
兩種方式。
1. @Value(「${}」):注入的是外部配置文件對應的property
在application.propertites配置屬性如下:
在程序中動態讀取server.port屬性,
@w=300
這樣server.port=8000就注入到了對應的參數中。
2. @Value(「#{}」):常用的方式是#{obj.property ? :default_value}
,注意與上一種方式不同的是,這種方式中的obj需要是一個對象。也可以在其中填寫SpEL表達式
。
Spring表達式語言全稱為「Spring Expression Language」,縮寫為「SpEL」,類似於Struts2x中使用的OGNL表達式語言,能在運行時構建複雜表達式、存取對象圖屬性、對象方法調用等等,並且能與Spring功能完美整合,如能用來配置Bean定義。
下面的例子中,首先定義UserBean並從property文件中讀取屬性,屬性值為mysql。
@w=400
接着在另一個Controller類中注入UserBean的屬性。
@w=300
@Autowired
Spring中常利用@Autowired
完成依賴注入(DI), 對IOC容器中的各個組件的依賴關係賦值。
下面的例子中,是常見的DAO、Service、Controller模型,採用Autowired可以方便的在Service層和Controller層中注入對應的Bean實例。
@Autowired實現原理就是:默認優先按類型去容器中找對應的組件,相當於anno.getBean(TestDao.class)去容器獲取id為testDao的bean, 並注入到TestService的bean中;
但是當容器中有多個testDao時,使用默認的@Autowired就會發生異常,IOC容器此時無法確定哪個bean作為依賴注入的對象,Spring引入了Qualifier和Primary來解決這個問題。
假定有兩個testDao,其bean id分別為testDao1和testDao2,此時可以使用@Autowired和@Qualifier
結合來指定注入哪一個bean,下面的例子中,指定bean id為testDao,注意還可以加上required=false
當容器中找不到這個bean時,也不會報錯,此時該對象注入失敗為null。
如果不使用@Qualifier
,可以使用@Primary
來指定默認的首選bean。此時通過getBean和autowired獲取到的都是@Primary
指定的bean。
當@Qualifier
和@Primary
共存時,@Qualifier
會按照bean id來獲取指定的bean,不會受到@Primary
的影響。此時使用getBean獲取到的就是@Primary
標識的bean。
擴展:
- @Resource
@Resource和Autowired一樣可以裝配bean
@Resource缺點: 不能支持@Primary功能,不能支持@Autowired(required = false)的功能
- @Inject
@Inject和Autowired一樣可以裝配bean, 支持@Primary功能, 可用於非spring框架.
@Inject缺點: 但不能支持@Autowired(required = false)
的功能,需要引入第三方包javax.inject
@w=350
Autowired屬於spring的, 不能脫離spring, @Resource和@Inject都是JAVA規範
推薦使用@Autowired。
3.6 @Bean Vs @Component
@Component
主要和ComponentScan結合,用於自動檢測和配置Bean,Bean和被註解的類是一一對應的關係。
@Bean
用於顯式聲明一個單獨的Bean,而不是讓Spring自動完成該過程,通過該註解可以將類的定義和Bean的聲明解耦。特別是使用第三方的庫時,只能通過@Bean來將某些類注入到容器中。
本文由『後端精進之路』原創,首發於博客 http://teckee.github.io/ , 轉載請註明出處
搜索『後端精進之路』關注公眾號,立刻獲取最新文章和價值2000元的BATJ精品面試課程。