1. 程式人生 > >Spring學習第一章、第二節:依賴注入(包括自動裝配,物件的注入)

Spring學習第一章、第二節:依賴注入(包括自動裝配,物件的注入)

依賴注入

前言

其實在第一章中,我們多次提到了關於依賴注入(DI)的內容,而上一章中講的Spring容器為什麼在Spring中叫做Ioc呢?
我們知道了Ioc叫做控制反轉,也就是說我們將控制權交給了Spring容器,我們回顧一下Spring對Spring Bean進行控制的過程:

  1. 首先,我們定義了Bean的配置檔案,和相應的實現類
  2. Spring會對Bean配置資訊進行讀取,將Bean資訊載入到Spring容器中的Bean定義登錄檔中
  3. 根據Bean登錄檔和對應的實現類例項化Bean物件,放到Spring容器中,供應用程式呼叫
    如圖:
    image

嗯,有沒有想過為什麼要進行控制反轉呢?
因為大多數應用程式都是由兩個或是更多的類通過彼此的合作來實現業務邏輯,這使得每個物件都需要獲取與其合作的物件(也就是它所依賴的物件)的引用。如果這個獲取過程要靠自身實現,那麼這將導致程式碼高度耦合並且難以維護和除錯。例如:

Class A中用到了Class B的物件b,一般情況下,需要在A的程式碼中顯式的new一個B的物件。

此時,A類中將new出B物件。
那麼有沒有辦法緩解這個問題呢?開發者們發現了一個好辦法:介面驅動(Interface Driven Design)

classA
{
    AInterface a;
    A(){}
    AMethod(){
        a = new AInterfaceImp();
    }
}

這時,我們還可以寫一個Factory來應對需要AInterface另一個或多個實現的需求(舉一個簡單工廠的栗子)

InterfaceImplFactory
{
   AInterface create(Object condition)
   {
if(condition == condA) { return new AInterfaceImpA(); } else if(condition == condB) { return new AInterfaceImpB(); } else { return new AInterfaceImp(); } } }

這種方式雖然讓程式碼更靈活,健壯性更高,但是本質上,最後還是會讓類本身實現了介面產生依賴,也就是說:AInterface a = new AInterfaceImp(); 這樣的程式碼遲早要執行,耦合關係的產生無法避免。
而Ioc的提出可以徹底解決這種耦合,它把耦合從程式碼中移出去,放到統一的XML 檔案中,通過一個容器在需要的時候把這個依賴關係形成,即把需要的介面實現注入到需要它的類中,這可能就是“依賴注入”說法的來源了。

下面,我們就來學習Spring如何通過DI實現Ioc
在此之前,我們補充一個知識點

Spring Bean 定義繼承

bean 定義可以包含很多的配置資訊,包括建構函式的引數,屬性值,容器的具體資訊例如初始化方法,靜態工廠方法名,等等。
子 bean 的定義繼承父定義的配置資料。子定義可以根據需要重寫一些值,或者新增其他值。
Spring Bean 定義的繼承與 Java 類的繼承無關,但是繼承的概念是一樣的。你可以定義一個父 bean 的定義作為模板和其他子 bean 就可以從父 bean 中繼承所需的配置。
當你使用基於 XML 的配置元資料時,通過使用父屬性,指定父 bean 作為該屬性的值來表明子 bean 的定義。

下面是配置檔案 Beans.xml,在該配置檔案中我們定義有兩個屬性 message1 和 message2 的 “helloWorld” bean。然後,使用 parent 屬性把 “helloIndia” bean 定義為 “helloWorld” bean 的孩子。這個子 bean 繼承 message2 的屬性,重寫 message1 的屬性,並且引入一個屬性 message3。

<?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-3.0.xsd">

   <bean id="helloWorld" class="com.tutorialspoint.HelloWorld">
      <property name="message1" value="Hello World!"/>
      <property name="message2" value="Hello Second World!"/>
   </bean>

   <bean id="helloIndia" class="com.tutorialspoint.HelloIndia" parent="helloWorld">
      <property name="message1" value="Hello India!"/>
      <property name="message3" value="Namaste India!"/>
   </bean>

</beans>

這裡是 HelloWorld.java 檔案的內容:

public class HelloWorld {
   private String message1;
   private String message2;
   public void setMessage1(String message){
      this.message1  = message;
   }
   public void setMessage2(String message){
      this.message2  = message;
   }
   public void getMessage1(){
      System.out.println("World Message1 : " + message1);
   }
   public void getMessage2(){
      System.out.println("World Message2 : " + message2);
   }
}

這裡是 HelloIndia.java 檔案的內容:

public class HelloIndia {
   private String message1;
   private String message2;
   private String message3;

   public void setMessage1(String message){
      this.message1  = message;
   }

   public void setMessage2(String message){
      this.message2  = message;
   }

   public void setMessage3(String message){
      this.message3  = message;
   }

   public void getMessage1(){
      System.out.println("India Message1 : " + message1);
   }

   public void getMessage2(){
      System.out.println("India Message2 : " + message2);
   }

   public void getMessage3(){
      System.out.println("India Message3 : " + message3);
   }
}

Bean 定義模板

你可以建立一個 Bean 定義模板,不需要花太多功夫它就可以被其他子 bean 定義使用。在定義一個 Bean 定義模板時,你不應該指定類的屬性,而應該指定帶 true 值的抽象屬性,如下所示:

<?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-3.0.xsd">

   <bean id="beanTeamplate" abstract="true">
      <property name="message1" value="Hello World!"/>
      <property name="message2" value="Hello Second World!"/>
      <property name="message3" value="Namaste India!"/>
   </bean>

   <bean id="helloIndia" class="com.tutorialspoint.HelloIndia" parent="beanTeamplate">
      <property name="message1" value="Hello India!"/>
      <property name="message3" value="Namaste India!"/>
   </bean>

</beans>

父 bean 自身不能被例項化,因為它是不完整的,而且它也被明確地標記為抽象的。當一個定義是抽象的,它僅僅作為一個純粹的模板 bean 定義來使用的,充當子定義的父定義使用。

正文

Spring依賴注入

每個基於應用程式的 java 都有幾個物件,這些物件一起工作來呈現出終端使用者所看到的工作的應用程式。當編寫一個複雜的 Java 應用程式時,應用程式類應該儘可能獨立於其他 Java 類來增加這些類重用的可能性,並且在做單元測試時,測試獨立於其他類的獨立性。依賴注入(或有時稱為佈線)有助於把這些類粘合在一起,同時保持他們獨立。

就像之前提到的,我們在使用一個類中可能會依賴另一個類的例項:假設你有一個包含文字編輯器元件的應用程式,並且你想要提供拼寫檢查。標準程式碼看起來是這樣的

public class TextEditor {
   private SpellChecker spellChecker;  
   public TextEditor() {
      spellChecker = new SpellChecker();
   }
}

而在控制反轉的場景中,我們會做類似這樣的事:

public class TextEditor {
   private SpellChecker spellChecker;
   public TextEditor(SpellChecker spellChecker) {
      this.spellChecker = spellChecker;
   }
}

我們發現,TextEditor 不應該擔心 SpellChecker 的實現。SpellChecker 將會獨立實現,並且在 TextEditor 例項化的時候將提供給 TextEditor,整個過程是由 Spring 框架的控制。(包括例項化的過程,嗯,這裡應該就只有它的引用),實現這個過程就得需要依賴注入出場了!

而Spring中依賴注入主要有兩種方式:

序號 依賴注入型別 & 描述
1 Constructor-based dependency injection 當容器呼叫帶有多個引數的建構函式類時,實現基於建構函式的 DI,每個代表在其他類中的一個依賴關係。
2 Setter-based dependency injection基於 setter 方法的 DI 是通過在呼叫無引數的建構函式或無引數的靜態工廠方法例項化 bean 之後容器呼叫 beans 的 setter 方法來實現的

你可以混合這兩種方法,基於建構函式和基於 setter 方法的 DI,不過更推薦使用Setter方法。程式碼是 DI 原理的清洗機,當物件與它們的依賴關係被提供時,解耦效果更明顯。物件不查詢它的依賴關係,也不知道依賴關係的位置或類,而這一切都由 Spring 框架控制的。
如下圖:
image

基於構造器的依賴注入

public class B {

	public B() {
		System.out.println("B()");
	}
	
	public void f1() {
		System.out.println("B's f1()");
	}
}
public class A {
	private B b;
	public A(B b) {//配置構造器
		System.out.println("A(B)");
		this.b = b;
	}
	public A() {
		System.out.println("A()");
	}
	public void execute() {
		System.out.println(" execute()");
		b.f1();
	}
	
	
}
<bean id="b1" class="ioc2.B"/>
	<bean id="a1" class="ioc2.A">
		<constructor-arg index="0" ref="b1"></constructor-arg>
	</bean>

構造器方法注入:constructor-arg
index:選擇構造器中第n個引數

其他情況:

public class Foo {
   public Foo(int year, String name) {
      // ...
   }
}
<beans>
   <bean id="exampleBean" class="examples.ExampleBean">
      <constructor-arg type="int" value="2001"/>
      <constructor-arg type="java.lang.String" value="Zara"/>
   </bean>

</beans>

最後並且也是最好的傳遞建構函式引數的方式,使用 index 屬性來顯式的指定建構函式引數的索引。下面是基於索引為 0 的例

<beans>

   <bean id="exampleBean" class="examples.ExampleBean">
      <constructor-arg index="0" value="2001"/>
      <constructor-arg index="1" value="Zara"/>
   </bean>

</beans>

最後,如果你想要向一個物件傳遞一個引用,你需要使用 標籤的 ref 屬性,如果你想要直接傳遞值,那麼你應該使用如上所示的 value 屬性。

基於Setter方法的依賴注入

Spring會把配置檔案中屬性名的第一個字母大寫來解析Setter方法。

public class B {

	public B() {
		System.out.println("B()");
	}
	public void f1() {
		System.out.println("B's f1()");
	}

}
public class A {
	private B b;
	public void setB(B b) {
		System.out.println("setB()");
		this.b = b;
	}
	public A() {
		System.out.println("A()");
	}
	
	public void execute() { 
		
		System.out.println("execute()");
		b.f1();
	}
}
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="b1" class="ioc.B"/>
<bean id="a1" class="ioc.A">
	<property name="b" ref="b1"></property>
</bean> 
</beans>

property元素:表示使用set方法注入依賴關係
name:指定屬性名
ref:指定屬性值

main(){//表示main函式
AbstractApplicationContext ac = 
				new ClassPathXmlApplicationContext(
						"ioc.xml");
		A a1 = ac.getBean("a1",A.class);
		a1.execute();
		}

測試下結果就發現B已經被容器注入了!

擴充套件:使用 p-namespace 實現 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-3.0.xsd">
   <bean id="john-classic" class="com.example.Person">
      <property name="name" value="John Doe"/>
      <property name="spouse" ref="jane"/>
   </bean>
   <bean name="jane" class="com.example.Person">
      <property name="name" value="John Doe"/>
   </bean>
</beans>

上述 XML 配置檔案可以使用 p-namespace 以一種更簡潔的方式重寫,如下所示:

<?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:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

   <bean id="john-classic" class="com.example.Person"
      p:name="John Doe"
      p:spouse-ref="jane"/>
   </bean>

   <bean name="jane" class="com.example.Person"
      p:name="John Doe"/>
   </bean>

</beans>

在這裡,你不應該區別指定原始值和帶有 p-namespace 的物件引用。-ref 部分表明這不是一個直接的值,而是對另一個 bean 的引用。

自動裝配

你已經學會如何使用元素來宣告 bean 和通過使用 XML 配置檔案中的和元素來注入 。
Spring 容器可以在不使用和 元素的情況下自動裝配相互協作的 bean 之間的關係,這有助於減少編寫一個大的基於 Spring 的應用程式的 XML 配置的數量。

自動裝配模式 (較官方的版本)
下列自動裝配模式,它們可用於指示 Spring 容器為來使用自動裝配進行依賴注入。你可以使用元素的 autowire 屬性為一個 bean 定義指定自動裝配模式:

模式 描述
no 這是預設的設定,它意味著沒有自動裝配,你應該使用顯式的bean引用來連線。你不用為了連線做特殊的事。在依賴注入章節你已經看到這個了。
byName 由屬性名自動裝配。Spring 容器看到在 XML 配置檔案中 bean 的自動裝配的屬性設定為 byName。然後嘗試匹配,並且將它的屬性與在配置檔案中被定義為相同名稱的 beans 的屬性進行連線。
byType 由屬性資料型別自動裝配。Spring 容器看到在 XML 配置檔案中 bean 的自動裝配的屬性設定為 byType。然後如果它的型別匹配配置檔案中的一個確切的 bean 名稱,它將嘗試匹配和連線屬性的型別。如果存在不止一個這樣的 bean,則一個致命的異常將會被丟擲。
constructor 類似於 byType,但該型別適用於建構函式引數型別。如果在容器中沒有一個建構函式引數型別的 bean,則一個致命錯誤將會發生。
autodetect Spring首先嚐試通過 constructor 使用自動裝配來連線,如果它不執行,Spring 嘗試通過 byType 來自動裝配

呃,簡單點說吧:

  • 自動裝配:spring容器依據某種規則自動建立物件之間的依賴關係
  • 預設情況下,容器不會自動裝配
  • 可以通過指定autowire屬性告訴容器進行自動裝配(本質上,容器仍然是set或是構造器注入)

我們舉較常用的栗子說明下:
我們想將Waiter注入到Restrurant中

public class Waiter {

	public Waiter() {
		System.out.println("Waiter()");
	}

}

<bean id="waiter" class="ioc2.Waiter"></bean>
<bean id="rest" class="ioc2.Restaurant" autowire="byName"></bean>
<bean id="rest"