1. 程式人生 > >Spring基礎(一)_控制反轉(IOC)

Spring基礎(一)_控制反轉(IOC)

Spring-IOC

1、簡述

1.1 依賴注入DI

現實開發中,每一個應用都會由兩個或多個類組成,這些類之間相互協作完成特定的業務邏輯。根據傳統做法,每個物件負責管理與自己協作的物件的引用(也就是,每個物件中使用new例項化物件的方式建立協作的物件)——這將導致==高度耦合和難以測試的程式碼==。

public class ClassA{
    private ClassB b;//B類的依賴
    
    public ClassA(){
        this.b=new ClassB();//A與B緊耦合
    }
}

public class ClassB{}

DI 的出現就是為了解決物件之間的依賴關係所帶來的高耦合問題。【依賴注入 (DI,Dependency Injection)】:將所依賴的關係自動交給目標物件,而不是讓物件本身去獲取依賴。依賴注入所關注的是已經建立好的物件如何實現它們之間的依賴關係;至於這些物件怎麼被建立和管理,稍後會講述。

public class ClassA{
    private ClassB b;
    
    public ClassA(ClassB b){
        this.b=b;//B是被注入進來的
    }
}

public class ClassB{}

DI 的實現所帶來的好處是:和麵向介面實現鬆耦合。一個物件通過介面來表明依賴關係,這樣就可以在物件不確定的情況下,使用不同的具體實現進行替換——【鬆耦合】。

1.2 Bean

在 Spring 應用中,一個 Bean 物件對應一個物件,並存儲於 Spring 容器中,Spring 容器負責建立物件,裝配、配置物件,以及管理整個物件的生命週期,從生存到死亡。

1.2.1 Spring容器

容器是 Spring 框架的核心。Spring 容器使用 DI 管理構成應用的元件,它會建立相互協作的元件之間的關聯。Spring 自帶多個容器實現,主要分為兩種型別:

  • bean 工廠:由 org.springframework.beans.factory.BeanFactory 介面定義,是最簡單的容器,提供基本的 DI 支援;
  • 應用上下文:由 org.springframework.context.ApplicationContext 介面定義,基於 BeanFactory 構建,並提供應用框架級別的服務;
A. 使用應用上下文

Spring 自帶了多種型別的應用上下文。

型別 描述
AnnotationConfigApplication 從一個或多個基於 java 的配置類中載入 Spring 應用上下文
AnnotationConfigWebApplicationContext 從一個或多個基於 Java 的配置類中載入 Spring Web 應用上下文
ClasssPathXmlApplicationContext 從類路徑下的一個或多個 XML 配置檔案中載入上下文定義,把應用上下文的定義檔案作為類資源
FileSystemXmlApplicationContext 從檔案系統下的一個或多個XML配置檔案中載入上下文定義
XmlWebApplicationContext 從 Web 應用下的一個或多個 XML 配置檔案中載入上下文定義
B. Bean的生命週期

Java 中通過 new 例項化的物件,其生命週期是從被建立開始,直到不再被呼叫,該物件就由 Java 自動進行垃圾回收。

在 Spring 中,Bean 物件的生命週期相對複雜,其包含了以下過程:

  1. Spring 對 bean 進行例項化;
  2. Spring 將值和 bean 的引用注入到 bean 對應的屬性中;
  3. 如果 bean 實現了以下物件,會進行相應的操作:
    • 實現 BeanNameAware 介面,Spring 將 bean 的 ID 傳遞給 setBeanName() 方法;
    • 實現 BeanFactoryAware 介面,Spring 將呼叫 setBeanFactory() 方法,將 BeanFactory 容器傳入;
    • 實現 BeanPostProcessor 介面,Spring 將呼叫 postProcessBeforeInitialization() 方法;
    • 實現 InitializingBean 介面,Spring 將呼叫 afterPropertiesSet() 方法。如果 bean 使用 init-method 宣告初始化方法,該方法也會被呼叫;
  4. bean 建立完畢,可被應用使用;此時,它們一直駐留在應用上下文,直到該應用上下文被銷燬;
  5. 如果 bean 實現了 DisposableBean 介面,Spring 將呼叫 destory() 方法。同樣,如果 bean 使用 destory-method 聲明瞭銷燬方法,該方法也會被呼叫;

2、裝配Bean

在 Spring 中,物件無需自己查詢或建立與其所關聯的其他物件。相反,容器負責把需要相互協作的物件引用賦予各個物件。建立應用物件之間協作關係的行為稱為【裝配 (wiring)】。

裝配 bean 的三種機制

  1. 隱式的 bean 發現機制和自動裝配;
  2. 在 Java 中進行顯示配置;
  3. 在 XML 中進行顯示配置

儘管,Spring 中提供了多種方案來配置 bean,我們在配置時可視情況進行選擇合適的方式進行裝配我們的 bean 物件。建議是:儘可能使用自動配置機制;顯示配置越少越好。而且,使用選擇顯示配置時,JavaConfig 配置會比 XML 配置更加強大,型別更安全

2.1 自動化裝配

Spring 是從兩個方面實現自動裝配:

  1. 元件掃描 (component Scan):Spring 會自動發現應用上下文中所建立的 bean;
  2. 自動裝配 (autowiring):Spring 自動滿足 bean 之間到依賴;

2.1.1 建立元件和自動裝配

建立元件類時,常用的註解有:

  1. @Component :建立一個元件類,用於被 Spring 掃描並建立 Bean 物件;

    該註解可以為當前類設定 ID 值,@Component("ID_value") 。沒有設定 ID 值時,預設為類名的首字母為小寫。

  2. @Autowire :自動裝配,為 Bean 的屬性注入物件。

    Autowire 可以用在定義屬性的語句上、有參構造方法以及 set()方法上。使用註解,會在 Spring 應用上下文中尋找匹配的 bean 物件。

    在使用 @Autowire 註解時,需要注意兩個問題:

    • 如果當前 Bean 物件的依賴關係,==沒有匹配的其它 Bean==,Spring 應用上下文在建立該 Bean 時,會丟擲異常;使用註解的屬性 required=false,如果找不到匹配的 Bean,會處於未裝配狀態:null。
    • 如果當前 Bean 物件的依賴關係,==存在多個滿足匹配的其它 Bean==,Spring 也將丟擲異常;這涉及到 裝配的歧義性

2.1.2 元件掃描

上節簡單講述瞭如何建立一個元件類,以及如何實現自動裝配依賴關係。但這並不代表:在Spring容器中建立了一個 Bean 物件。要想建立一個 Bean 物件,需要配置 Spring 的元件掃描,命令 Spring 尋找帶 @Component 註解的類,並建立 Bean,因為 Spring 中元件掃描功能預設是不啟用。那麼,如何啟用元件掃描呢?——有兩種方式:

  1. 基於 Java 的配置

    需要建立一個配置類,該類與普通類的區別在於:使用註解 @Configuration 修飾。開啟元件掃描,需要使用另一個註解 @ComponentScan

    package soundsystem;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @ComponentScan
    public class CDPlayerConfig {
    
    }
  2. XML 檔案配置

    在 XML 中配置啟用元件掃描,需要使用 Spring context 名稱空間 的 <context:component-scan> 元素。

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:Context ="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <Context:component-scan base-package="soundsystem"/>
    </beans>

細心的小夥伴可能發現了,在 XML 檔案配置中,base-package 屬性是必須給定的。該屬性是指定元件掃描的基礎包,也就是指定哪些包是需要使用元件掃描。

在 Java 配置中,@ComponentScan 註解中可使用 basePackages 屬性和 basePackageClasses 屬性來指定元件掃描的基礎包。前者給定值是包路徑的 String 型別,後者是 .class 類檔案(類檔案所在的包會作為基礎包)。它們的值可以是單一值,也可以是複數形式。

@ComponentScan(basePackages={"package1","package2",...})//使用String型別表示,是型別不安全的;當重構程式碼時,容易發生錯誤
//@ComponentScan(basePackageClasses={Xxx1.class,Xxx2.class,...})
public class CDPlayerConfig{
    
}

2.2 顯式裝配

大多數情況下,通過元件掃描和自動裝配實現 Spring 的自動化配置更為推薦。但有些情況,比如:將第三方庫中的元件裝配到應用中,使用 @Component@Autowired 無法進行註解,這就必須採用顯式裝配。顯式裝配的方案有:Java 和 XML。

2.2.1 Java 配置

在 2.1.2 元件掃描 中,已經提及如何建立一個 Java 配置類,就不在重複講述。在配置類中,通過方法形式和註解 @Bean 建立 Bean 物件。Java 配置的好處是:在建立 Bean 的過程中,可以使用 Java 程式碼。

在 Java 配置類中宣告 Bean,需要編寫一個方法,這個方法會返回 建立所需型別的例項,然後給這個方法新增 @Bean 註解;預設情況下,@Bean 註解會設定與方法名一樣的 ID 值,可以使用 name 屬性指定不同的名字。

package cn.book.main;

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

@Configuration
public class StudentConfig {
    
    @Bean
    //@Bean(name="stu")
    public Student getStu(){
        return new Student();
    }
}
package cn.book.main;

public class Student {
    private String name;
    private int age;
    
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Student() {
    }
    
    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

這個簡單的例子中,通過無參構造方法建立例項,是宣告 Bean 最簡單的方法,因為沒有為 Bean 注入依賴關係。在配置類中,實現依賴注入的方式都是通過有參構造方法建立,只是獲取要注入的 Bean 的形式有兩種:

  1. 引用配置類中建立 Bean 的方法;

     @Bean
    public String getStuName(){
        return "Tom";
    }
    
    @Bean
    public int getStuAge(){
        return 18;
    }
    
    @Bean
    public Student getStu(){
        return new Student(getStuName(),getStuAge());
    }

    注意:

    • 只能注入配置類中的 Bean 物件;
    • Bean 物件是單例的。方法被呼叫時,spring 會攔截呼叫的方法,如果容器中已建立該方法返回的 Bean 物件,則直接賦予,而不會再執行方法內的操作。
  2. 通過方法引數傳遞;

    Java 或 XML 配置中建立的 Bean 物件、元件掃描發現的 Bean 物件,都可以通過方法引數傳遞並注入。

    @Bean
    public Student getStu(String name,int age){
     return new Student(name,age);
    }

2.2.2 XML 配置

在使用 XML 裝配 Bean 之前,需要建立一個新的配置規範,這意味著要建立一個 XML 檔案,並且以 <beans> 元素為根。下面是最為簡單的 Spring 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 
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context ">

</beans>
A、宣告 Bean

無參構造器宣告 bean

XML 配置中使用 <bean> 元素來宣告一個 bean,該元素類似於 Java 配置中的 @Bean 註解。

<bean id="" class="" /> <!--這個元素將會呼叫類的預設構造器來建立 bean-->
  • id:bean 的 ID 值;可以不指定,會預設為:包名.類名#0。0為計數值,用來區分相同的 bean ,如果有相同的 bean ,計數值 + 1;
  • class:指定建立 bean 的類,使用全限定類名;

有參構造器宣告 bean

要使用有參構造器建立 bean ,需要使用<bean><constructor-arg>元素,此方式在宣告 bean 的同時,並注入其它依賴關係。該元素中有五個屬性:

  • name:指定引數名稱,與構造方法中的引數名一致
  • value:賦予引數的值;注入常量值,可以是基本型別和String;
  • type:引數的型別
  • index:指定引數在構造方法中的順序號(從0開始)
  • ref:要注入的 bean 的 ID 值;
<bean id="" class="">
    <constructor-arg name="" value="" type="" index=""/>
    <constructor-arg name="" type="" index="" ref="" />
</bean>

注意

  1. bean 元素中的引數名稱要與構造方法中的引數名一致;
  2. bean 元素中的引數順序可以與構造方法中的引數順序不一致;可以使用 index 屬性指定在構造方法的順序;
  3. 使用 type 屬性時,對於引用型別需要使用包名+類名;基本型別可以不用該屬性;
B、注入

在 XML 配置檔案中,注入 bean 的方式有三種:有參構造器注入 <constructor-arg>、屬性注入 <property> 以及自動注入 <autowire>

屬性注入

屬性注入的實質是:呼叫 set() 方法。在宣告 bean 的元素中,使用 <property> 元素。

<bean id="" class=" ">
        <property name="" value=""/>
        <property name="" ref=""/>
</bean>

自動注入

自動注入方式使用 bean 元素中的屬性 autowire,該屬性有三個值 byName、byType、constructor,根據提供的值進行自動匹配注入。

  • byName:在當前 XML 檔案中,查詢 bean 元素的 id 值與需要注入 bean 的屬性名相同的物件,進行匹配。

  • byType:在當前 XML 檔案中,查詢 bean 標籤的物件型別與需要注入 bean 的屬性型別相同的物件,進行匹配;此時,不需要關注 bean 標籤的 id 值是否與需要注入的屬性名一致。

  • constructor:【1】根據需要注入物件的有參構造器的形參名進行查詢 ,找到匹配 bean 的 id 值則注入;否則,【2】根據需要注入物件的有參構造器的形參型別進行查詢,找到型別匹配的 bean 標籤則注入。

  • byName 和 byType 實際上是呼叫 set() 方法賦值;constructor 則是呼叫有參構造方法;
  • byName 和 byType 可以結合 property 標籤使用;可以結合 constructor-org 標籤使用,相當於呼叫多參的有參構造方法;

C、集合裝配

Spring 中實現了對集合的裝配,包括:Array、List、Set以及Map,它們對應的元素為:<array><list><set> 以及<map>,集合配置方式比較接近,這裡舉例 List 和 Map 集合的配置方式

<list value-type=""><!--建立List,並宣告儲存值的型別-->
    <value type=""></value><!--集合包含的值,可宣告資料型別-->
    <ref bean=""/><!--引用bean,使用bean的ID-->
</list>

<map key-type="" value-type=""><!--建立Map,並宣告儲存鍵-值的型別-->
    <entry key="" value=""/><!--集合包含的值-->
    <entry key-ref="" value-ref=""/><!--引用到鍵或值的bean,使用bean的ID-->
</map>

2.2.3 混合配置

當我們在裝配 bean 時,如果同時採用 JavaConfig 和 XML 配置 bean 時,而它們的 bean 相互關聯,這時,就需要將不同的配置檔案組合在一起。

A、JavaConfig 中引用 XML 配置

多個 Java 配置組合

使用註解 @Import 可以將其它 JavaConfig 配置類引入,

//在配置類中引用另一個配置類
@Configuration
@Import(XxxConfig1.class)
public class Xxxconfig2{

}

//當然,也可以建立一個新的配置類,只用於組合配置類
@Configuration
@Import(XxxConfig1.class,XxxConfig2.class)
public class Config{
    
}

JavaConfig 配置中引用 XML 配置

@Configuration
@ImportResource("classpath:*/*/*.xml")
public class Config{

}
B、XML 配置中引用 JavaConfig 配置
<bean class="*.*.Config" /><!--引入 JavaConfig 配置-->

<import resource="*/*/*.xml" /><!--引入 XML 配置-->

3、高階裝配

3.1 環境與profile

應用中存在不同的環境,應用在不同的環境中需要配置不一樣的 Bean,如果需要切換環境時,原環境的 Bean 在新環境中不一定可用,這時需要在新環境中配置新的 Bean,在 Spring 中,可以根據環境建立 Bean 或者不建立 Bean,這個就是 Profile 配置 。

跨環境配置的幾個例子:資料庫配置、加密演算法以及外部系統的整合。

在這裡,我們不討論如何配置不同的環境,只關注如何使用 Profile 決定 Bean 的建立。現假設,我們應用中存在下面三個環境,環境名稱為:dev、qa、prod。現在,我們要為指定的環境裝配 Bean。

3.1.1 配置Profile

JavaConfig 中配置

@Profile("Envionment_name") 註解,括號內指定環境名稱,指定某個 Bean 屬於哪一個 Profile。當指定的環境為啟用狀態時,該 Bean 被建立,否則不建立。

@Configuration
//@Profile("dev")  //profile應用在類上,當環境啟用時,該配置類才會被建立
public class ProfileConfig{

    @Bean(destroyMethod="shutdown") //使用在方法級別上,可以將不同環境的 Bean 放在同一配置類中
    @Profile("dev")
    public DataSource dataSource(){
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:schema.sql")
            .addScript("classpath:test-data.sql")
            .build();
    }
}

XML 中配置

在 XML 配置中,可以通過 <Beans> 元素的 profile 屬性,在 XML 配置 Bean。下面是一個例子

<beans profile="dev"> <!--為dev配置一個 Bean-->
    <jdbc:embedded-database id="dataSource" type="H2">
        <jdbc:script location="classpath:schema.sql" />
        <jdbc:script location="classpath:test-data.sql" />
    </jdbc:embedded-database>
</beans>

<beans profile="prod"> <!--為prod配置一個 Bean-->
    <jee:jndi-lookup id="dataSource"
                     lazy-init="true"
                     jndi-name="jdbc/myDatabase"
                     resource-ref="true"
                     proxy-interface="javax.sql.DataSource" />
</beans>
3.1.2 啟用Profile

Spring 在確定哪個 Profile 處於啟用狀態時,需要依賴兩個獨立的屬性:spring.profiles.activespring.profiles.default。前者會根據指定值來確定哪個 Profile 是啟用的;後者是當沒有指定 active 屬性的值時,預設啟用的 Profile。Spring 中設定這兩個屬性的方式

  • 作為 DispatcherServlet 的初始化引數;
  • 作為 Web 應用的上下文引數;
  • 作為 JNDI 條目;
  • 作為環境變數;
  • 作為 JVM 的系統屬性;
  • 在記成測試類上,使用 @ActiveProfiles 註解設定;

下面的例子中,使用 DispatcherServlet 的引數將 spring.profiles.default 設定 profile。在 Web 應用中,設定 spring.profiles.default 的 web.xml 檔案如下

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    
    <!--為上下文設定預設的 profile-->
    <context-param>
        <param-name>spring.profiles.default</param-name>
        <param-value>dev</param-value>
    </context-param>
    
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>cn.book.main.servlet.DispatcherServlet</servlet-class>
        
        <!--為Servlet設定預設的 profile-->
        <init-param>
            <param-name>spring.profiles.default</param-name>
            <param-value>dev</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/appServlet</url-pattern>
    </servlet-mapping>
    
</web-app>

注意

spring.profiles.activespring.profiles.default 屬性中,profile 使用的是複數形式,可以同時啟用多個 Profile——通過列出多個 profile 名稱,並以逗號分隔。

3.2 條件化 Bean

如果我們定義的 bean ,但不希望它們被 Spring 容器即刻被建立,而是希望當類路徑下包含某個庫,或者是建立了其它 Bean,亦或者要求設定了某個特定環境變數後,該 Bean 才被建立。此時,我們就需要使用條件化配置。

要實現一個條件化 Bean,在裝配 Bean 的方法上( 使用@Bean ),引用另一個註解 @Conditional(*.class),注意:括號內給定的是一個類檔案。該註解會根據括號內給定類的返回結果判斷是否建立 Bean,如果為true,會建立 Bean,否則不建立。

但是,這只是定義了一個要條件化的 Bean,該 Bean 需要滿足怎樣的條件,需要自己實現。上面說到,@Conditional 註解需要傳入一個類檔案,該類在建立時,要實現 Condition 介面,並重寫 matches() 方法。下面是一個簡單的例子

package cn.book.main.pojo;

//Bean 類
public class TestCondition {

    public TestCondition() {
        System.out.println("Bean 被建立了");
    }
}

該類實現 Condition 介面,並重寫 matches() 方法,在方法內可以編寫判斷程式碼,並返回 boolean 值。

package cn.book.main.condition;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class IfCreatCondition implements Condition {
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        return false;
    }
}

配置類,裝配 Bean。

package cn.book.resource;

import cn.book.main.condition.IfCreatCondition;
import cn.book.main.pojo.TestCondition;
import org.springframework.context.annotation.*;

@Configuration
public class HumanJobConfig {
    
    @Bean
    @Conditional(IfCreatCondition.class)
    public TestCondition getCondition(){
        return new TestCondition();
    }
}

測試類,如果 IfCreatCondition 類返回 true,則 Bean 被建立;否則不會被建立。

package cn.book.test;

import cn.book.resource.HumanJobConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=cn.book.resource.HumanJobConfig.class)
public class HumanJobTest {

    @Test
    public void Test(){

        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(HumanJobConfig.class);
    }
}

上面只是演示了實現條件化 Bean 的流程,我們的條件可以再複雜。大家應該注意到了,matches() 中有兩個引數:ConditionContextAnnotatedTypeMetadata。通過這兩個物件,我們可以實現符合 IOC 和 DI 的條件。接下來,就來了解這兩個物件:

ConditionContext 是一個介面,它有以下方法

方法 描述
getRegistry 返回 BeanDefinitionRegistry 檢查 bean 定義;
getBeanFactory 返回 ConfigurableListableBeanFactory 檢查 bean 是否存在,甚至 檢查 bean 的屬性;
getEnvironment 返回 Environment 檢查環境變數是否存在以及它的值是什麼;
getResourceLoader 返回 ResourceLoader 所載入的資源;
getClassLoader 返回 ClassLoader 載入並檢查類是否存在;

AnnotatedTypeMetadata 也是一個介面,能夠檢查帶有 @Bean 註解的方法上還有什麼註解。它有以下方法:

方法 描述
boolean isAnnotated(String annotationType) 檢查帶 @Bean 的方法上是否存在其它特定的註解
Map<String,Object> getAnnotationAttributes(String annotationType) 獲得指定註解的 Bean
Map<String,Object> getAnnotationAttributes(String annotationType, boolean classValueAsString) ==未了解==
MultiValueMap<String,Object> getAllAnnotationAttributes(String annotationType) 獲得指定註解的所有 Bean
MultiValueMap<String,Object> getAllAnnotationAttributes(String annotationType, boolean classValueAsString) ==未了解==

3.3 處理自動裝配的歧義性

自動化裝配中,僅當只有一個 Bean 滿足時,才能裝配成功。當多個 bean 滿足裝配時,Spring 會產生異常:NoUniqueBeanDefinitionException。最常見的情況是:==當一個介面有多個實現類,呼叫時使用介面物件引用子類==。

比如:Human介面有兩個實現類:Man類和 Woman類

package cn.book.main.entity;

public interface Human {
}
package cn.book.main.entity;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
public class Man implements Human {
    public Man() {
        System.out.println("I am man");
    }
}
package cn.book.main.entity;

import org.springframework.stereotype.Component;

@Component
public class Woman implements Human {
    public Woman() {
        System.out.println("I am woman");
    }
}

配置類

package cn.book.resource;

import cn.book.main.pojo.TestCondition;
import org.springframework.context.annotation.*;

@Configuration
@ComponentScan("cn.book.main.entity")
public class HumanConfig {

}

測試類,自動注入一個Human介面。此時,spring會產生:NoUniqueBeanDefinitionException

package cn.book.test;

import cn.book.main.entity.Human;
import cn.book.resource.HumanJobConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=cn.book.resource.HumanJobConfig.class)
public class HumanJobTest {

    @Autowired
    private Human human;

    @Test
    public void Test(){
        System.out.println(human.getClass());
    }
}

當確實發生裝配的歧義性時,Spring 提供了以下方案:

  1. 將可選 Bean 中的某一個設為首選(primary) 的 Bean;
  2. 使用限定符(qualifier)限定到符合的、唯一的 Bean;
3.3.1 標示首選 Bean

標示首選需要使用關鍵字 primary,它在 JavaConfig 中是註解 @Primary ,在 XML 是 bean 元素中的屬性 primary。

JavaConfig 配置

@Primary 註解配合 @Component@Bean 註解組合使用,在需要設定為首選的元件類和 Bean 物件上。

@Component 註解配合使用

@Component
@Primary
public class Man{

}

或者,與 @Bean 註解配合使用

@Configuration
public class JavaConfig{

    @Bean
    @Primary
    public Human getMan(){
        return new Man();
    }
}

在 XML 中設定 Bean 為首選項的配置為:

<bean id="man" class="Man" primary="true"/>

缺點

  • 不能設定多個首選 Bean;
  • 不夠靈活,存在歧義性時,只能裝配使用設定首選的Bean;
3.3.2 限定符限定裝配

限定符 @qualifier 註解,主要作用是在可選的 Bean 進行縮小範圍選擇,直到找到滿足的 Bean。它的有兩個作用:

  1. @Autowired@Inject 協同使用,在注入的時候指定想要注入的是哪個 Bean;

    @qualifier("") 括號內所設定的引數時要注入 Bean 的 ID 值。

  2. @Component@Bean 協同使用,為 Bean 設定限定符;

    @qualifier("") 括號內是為 Bean 設定的限定符,在注入時使用 qualifier 中引用。

3.3.3 限定符註解

如果使用註解 @qualifier 限定符依舊無法解決 bean 的裝配歧義性問題時,而且,在 Spring 中無法重複使用相同的 @qualiifer 註解,在這種情況下,可以自定義註解來區分 bean。那麼,如何自定義註解呢?

import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,
        ElementType.METHOD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface 註解名 {
}

注意:

  1. 自定義註解不能使用在類上;
  2. 使用自定義註解時,需要同時放在 宣告Bean的地方 和 注入 Bean 的地方;

4、Bean 作用域

在預設情況下,Spring 應用上下文中所有的 Bean 都是以單例形式建立的。也就是,不管一個 Bean 被注入多少次,每次注入的 Bean 都是同一個例項。

如果一個例項需要保持無狀態並在應用中重複使用,單例作用域是不可行且不安全的。在 Spring 定義了多種作用域,Spring 會基於這些作用域建立 Bean,這些作用域包括:

  • 單例(Singleton):在整個應用,只會建立 Bean 的一個例項;
  • 原型(Prototype):每次注入或通過 Spring 應用上下文獲取時,都會建立一個新的 Bean 例項;
  • 會話(Session):在 Web 應用中,為每個會話建立一個 Bean 例項;
  • 請求(Request):在 Web 應用中,為每個請求建立一個 Bean 例項;

單例是預設的作用。如果想要選擇其它作用域,要使用 @Scope註解。註解內使用以下表示作用域的引數:

  1. ConfigurableBeanFactory.SCOPE_PROTUTYPE 或者 "prototype"
  2. ConfigurableBeanFactory.SCOPE_SESSION 或者 "session"
  3. ConfigurableBeanFactory.SCOPE_REQUEST 或者 "request"

如果使用 XML 配置,在 <bean> 元素中的屬性 scope 設定 bean 的作用域。

4.1 會話和請求作用域

==學習到 Web 部分內容再深入學習==

5、執行時值注入

前面在裝配 Bean,講到在建立 Bean 時,將常量(比如int型別、String型別)直接給定,這是將值硬編碼到 Bean 中。有時,為了避免硬編碼值,想讓這些值在執行時在確定,Spring 提供了兩種在執行時求值的方式:

  • 屬性佔位符
  • Spring 表示式語言

5.1 注入外部值

回顧一下,在我們使用 JDBC 時,會建立一個屬性檔案 *.properties 檔案放置連線資料庫所需的配置引數。假設,在 Spring 中該檔案依舊存在,我們如何在配置類或配置檔案中解析並取值?

JavaConfig 配置類

  1. 通過註解 @PropertySource 中的value屬性設定屬性檔案路徑;
  2. 自動注入 Environment 物件;
  3. 通過 Environment 物件獲取屬性值;
@Configuration
@PropertySource(value = "classpath:/JDBC.properties")
public class JdbcConfig {

    @Autowired
    Environment env;
    
    @Bean
    public JdbcParams getJdbc(){
        return new JdbcParams(
                env.getProperty("jdbc.driver"),
                env.getProperty("jdbc.url"),
                env.getProperty("jdbc.username"),
                env.getProperty("jdbc.password")
        );
    }
}
public class JdbcParams {
    
    private String driver;
    private String url;
    private String username;
    private String password;

    public JdbcParams() {
    }

    public JdbcParams(String driver, String url, String username, String password) {
        this.driver = driver;
        this.url = url;
        this.username = username;
        this.password = password;
    }
}

Environmen 介面的用法,通過 Environment 介面可以呼叫以下方法:

方法 描述
String getProperty(String key) 根據指定值獲取屬性,屬性沒有定義返回null;
String getProperty(String key, String defaultValue) 根據指定值獲取屬性,如果沒有屬性值,則返回defaultValue;
T getProperty(String key, Class<T> type) 返回指定型別的屬性值;type為指定型別的.class
T getProperty(String key, Class<T> type,T defaultValue) 返回指定型別的屬性值;type為指定型別的.class,如果沒有屬性值,則返回defaultValue;
getRequiredProperty(String key) 根據指定值獲取屬性,屬性沒有定義丟擲異常;
containProperty(String key) 檢查屬性檔案是否存在某個屬性;
T getPropertyAsClass(String key,Class<T> type) 將屬性檔案解析為指定的類檔案;
String[] getActiveProfiles() 返回啟用 profile 名稱的陣列;
String[] getDefaultProfiles() 返回預設 profile 名稱的陣列;
boolean acceptsProfiles(String... profiles) 如果 environment 支援給定的 profile 的話,就返回 true;

5.2 佔位符注入值

Spring 支援將屬性定義到外部的屬性檔案中,並使用佔位符將值插入到 Bean 中。在 Spring 裝配中,佔位符的形式為使用 ${...} 包裝的屬性名稱。

為了使用佔位符,需要配置一個 PropertySourcePlaceholderConfigurer Bean,它能夠基於 Environment 及其屬性源來解析佔位符。下面來看看,JavaConfig 配置和 XMl 配置中使用佔位符的用法

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;

@Configuration
//宣告屬性源,並將屬性檔案載入到Spring
@PropertySource(value = "classpath:/JDBC.properties")
public class StudentCongif {

     二、
    //(1)使用佔位符解析屬性
    @Bean
    public JdbcParams getJdbc(
            @Value("${jdbc.driver}") String driver,
            @Value("${jdbc.url}") String url,
            @Value("${jdbc.username}") String username,
            @Value("${jdbc.password}") String password){
        return new JdbcParams(driver,url,username,password);
    }

    //(2)還需要配置一個PropertySourcesPlaceholderConfigurer 的 bean
    @Bean
    public PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
        return new PropertySourcesPlaceholderConfigurer();
    }
}
<!--建立 PropertySourceHolderConfigurer -->
<context:property-placeholder location="classpath:/JDBC.properties"/>

<!--    使用佔位符進行值注入-->  
<bean id="jdbc" class="cn.book.main.valueInject.JdbcParams" 
    c:driver="${jdbc.driver}"
    c:url="${jdbc.url}"
    c:username="${jdbc.username"
    c:password="${jdbc.password}"/>

解析外部屬效能夠將值的處理推遲到執行時,但它的關注點在於根據名稱解析來自 Spring Environment 和屬性源的屬性