Spring 04: IOC控制反轉 + DI依賴注入

Spring中的IOC

  • 一種思想,兩種實現方式

  • IOC (Inversion of Control):控制反轉,是一種概念和思想,指由Spring容器完成對象創建和依賴注入

    • 核心業務:(a)對象的創建 (b)依賴的注入
  • 2種實現方式

    • 基於xml實現IOC
    • 基於註解實現IOC
  • 基於xml的IOC在前3篇Spring部落格中簡單探討過了,後面將探討基於註解的IOC

基於註解的IOC

  • DI (Dependency Injection):基於註解的IOC被稱為DI,即依賴注入, 是IOC思想的一種具體實現方式
  • 根據IOC的核心業務即:(a)對象創建,(b)依賴注入,對註解進行分類研究

註解類型

a. 創建對象的註解

  • 包含:創建任意對象的註解 + 創建三層架構各層對象的註解

  • @Conponent可以創建任意對象

  • @Controller:專門用來創建控制器對象(Servlet),這種對象可以用來接收用戶的請求,可以返回處理結果給客戶端

  • @Service:專門用來創建業務邏輯層對象,負責向下訪問數據訪問層,並將處理結果返回給介面層

  • @Repository:專門用來創建數據訪問層對象,負責資料庫中的CRUD操作

b. 依賴注入的註解

  • 包含:負責簡單類型注入的註解 + 負責引用類型注入的註解

簡單類型的注入

  • @Value:用來給簡單類型(8 + 1)注入值

引用類型的注入

  • @Autowired:使用類型注入值,從整個Bean工廠中搜索同源類型的對象進行注入
    • 同源類型可以是如下3種情況
      • 1.被注入的屬性類型與待注入的數據類型是完全相同的類型
      • 2.被注入的屬性(可以作為:父類)類型與待注入的數據(可以作為:子類)類型可以是父子類關係
      • 3.被注入的屬性(可以作為:介面)類型與待注入的數據(可以作為:實現類)類型是可以是介面和實現類的關係
  • @Autowired + @Qualifier:使用名稱注入值,從整個Bean工廠中搜索相同名稱的對象進行注入

注意

考慮到演示程式碼的復用性,減少程式碼冗餘,並保證演示的清晰性,放在一起演示的程式碼:共用實體類 + 共用applicationContext.xml + 共用一個測試類。
不在一起演示的,另建一個新包並重新創建以上內容。
對實體類或配置文件的修改順序,遵循博文的演示順序。

  • @Conponent + @Value 放在一起演示
  • @Autowired:同源類型注入之完全相同類型 + 對應的(@Autowired + @Qualifier)名稱注入 放在一起演示
  • @Autowired:同源類型注入之父子類型 + 對應的(@Autowired + @Qualifier)名稱注入 放在一起演示
  • @Controller + @Service + @Repository 先不演示,在改造之前部落格(Spring部落格集中的Spring02)中的三層項目架構時再演示

– – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – — – — – –

@Conponent註解

實體類

  • Student實體類,並對實體類添加@Component註解
package com.example.s01;

import org.springframework.stereotype.Component;

@Component
public class Student {
    private String name;
    private int age;

    public Student() {
        System.out.println("Student無參構造方法被執行,實例對象被創建....");
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

applicationContext.xml

  • 對要掃描的包,添加包掃描
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="//www.springframework.org/schema/beans"
       xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
       xmlns:context="//www.springframework.org/schema/context"
       xsi:schemaLocation="//www.springframework.org/schema/beans //www.springframework.org/schema/beans/spring-beans.xsd //www.springframework.org/schema/context //www.springframework.org/schema/context/spring-context.xsd">

    <!-- 添加包掃描 -->
    <context:component-scan base-package="com.example.s01"/>
</beans>

測試1

  • 創建實體類對象的時機:和基於xml的IOC一樣,當創建Spring容器時,創建實體類對象

  • 具體流程:創建Spring容器時,讀取Spring核心配置文件:applicationContext.xml,進行包掃描,對於被掃描到的包,如果包中的實體類添加了@Component註解,則創建實體類對象

package com.example.test;

import com.example.s01.Student;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestComponent {
    //測試Component註解
    @Test
    public void testComponent(){
        //創建Spring容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("s01/applicationContext.xml");
        //獲取容器中的bean對象
        Student student = (Student) ac.getBean("student");
        System.out.println(student);
    }
}

測試輸出1

Student無參構造方法被執行,實例對象被創建....
Student{name='null', age=0}

Process finished with exit code 0

注意

實體類

  • 修改註解為@Component(“stu”)
@Component("stu")
public class Student {
	//...
}

測試2

  • 在獲取Spring容器中的對象時根據指定的名稱:”stu”來獲取。註解未做特殊指定時,則遵循用類名的駝峰命名法來取
public class TestComponent {
    //測試Component註解
    @Test
    public void testComponent(){
        //創建Spring容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("s01/applicationContext.xml");
        //獲取容器中的bean對象
        Student student = (Student) ac.getBean("stu");
        System.out.println(student);
    }
}

測試輸出2

  • 與測試輸出1完全相同,不再贅述

@value註解

實體類

  • 為Student實體類的簡單類型的屬性添加@Value註解
@Component("stu")
public class Student {
    @Value("荷包蛋")
    private String name;
    @Value("20")
    private int age;
    //....
}

測試3

  • 和測試1完全相同,不再贅述

測試輸出3

Student無參構造方法被執行,實例對象被創建....
Student{name='荷包蛋', age=20}

Process finished with exit code 0

– – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – — – — – –

@Autowired註解

同源類型注入3種情況之一:完全相同的類型的注入

實體類

  • 在新的包下構建的兩個實體類:School實體類 + Student實體類
  • School實體類
package com.example.s02;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class School {
    @Value("nefu")
    private String name;
    @Value("哈爾濱")
    private String address;

    public School() {
        System.out.println("School無參構造方法執行,實例對象被創建....");
    }

    @Override
    public String toString() {
        return "School{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}
  • Student實體類新增對School實例對象的引用,其他內容和之前的Student類相同
package com.example.s02;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class Student {
    //.....
    
    @Autowired
    private School school;
    
	//.....
}

applicationContext.xml

  • 添加包掃描,頭文件不再贅述
    <!-- 添加包掃描 -->
    <context:component-scan base-package="com.example.s02"/>

測試4

package com.example.test;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestAutowired {
    //測試同源注入:完全相同的類型
    @Test
    public void testAutowired(){
        //創建Spring容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("s02/applicationContext.xml");
        //從容器中獲取Student實例對象
        System.out.println("學生對象: " + ac.getBean("student"));
    }
}

測試輸出4

School無參構造方法執行,實例對象被創建....
Student無參構造方法被執行,實例對象被創建....
學生對象: Student{name='荷包蛋', age=20, school=School{name='nefu', address='哈爾濱'}}

Process finished with exit code 0

對應的名稱注入

實體類

  • School實體類:將School的@Component註解改為@Component(“theSchool”)
@Component("theSchool")
public class School {
	//.....
}
  • Student實體類:新增@Qualifier註解,並必須在其後指定Bean工廠中已經註冊的實體類對象的名稱(類名的駝峰命名或自定義名稱)
@Component
public class Student {
    //.....
    
    @Autowired
    @Qualifier("theSchool")
    private School school;
    
	//.....
}

測試5和測試輸出5

  • 分別和測試4和測試輸出4完全相同,不再贅述

注意

  • 只使用@Qualifier註解標籤且後面跟的Bean工廠中註冊的實體類對象的名稱正確時,無法完成依賴名稱註冊,用名稱進行注入時,這兩個註解標籤都要出現

實體類

  • Student實體類
@Component
public class Student {
    //.....
    
    @Qualifier("theSchool")
    private School school;
    
	//.....
}

測試6

  • 與測試4完全相同,不再贅述

測試輸出6

  • 沒有報錯,但是根據名稱進行依賴注入的操作並沒有真正將引用類型的數據注入到Student實例中,引用類型school為null
School無參構造方法執行,實例對象被創建....
Student無參構造方法被執行,實例對象被創建....
學生對象: Student{name='荷包蛋', age=20, school=null}

Process finished with exit code 0

– – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – — – — – –

@Autowired註解

同源類型注入3種情況之二:父子類型的注入

實體類

  • 構建一個新的實體類包,含有3個實體類:SubSchool,School,Student

  • 新增實體類SubSchool,為School類的子類

package com.example.s03;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class SubSchool extends School{
    @Value("nefu附小")
    private String name;
    @Value("香坊區")
    private String address;

    @Override
    public String toString() {
        return "SubSchool{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }

    public SubSchool() {
        System.out.println("SubSchool無參構造方法被執行,實例對象被創建....");
    }
}
  • Student實體類內容不變
package com.example.s03;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class Student {
    //......

    @Autowired
    private School school;
	//......
}
  • School實體類內容不變
package com.example.s03;

import org.springframework.stereotype.Component;

@Component
public class SubSchool extends School{
    //......
}

applicationContext.xml

  • 添加包掃描
    <!-- 添加包掃描 -->
    <context:component-scan base-package="com.example.s03"/>

測試7

package com.example.test;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestAutowiredExtend {
    //測試同源類型注入:父子類型
    @Test
    public void testAutowiredExtend(){
        //創建Spring容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("s03/applicationContext.xml");
        //從容器中獲取Student實例
        System.out.printf("Student實例: " + ac.getBean("student"));
    }
}

測試輸出7

  • 為什麼”School無參構造方法執行,實例對象被創建….”被輸出2次?
  • 原因:一次是構建School對象時,一次在構建SubSchool對象時(子類構造方法中調用父類無參構造方法)
School無參構造方法執行,實例對象被創建....
Student無參構造方法被執行,實例對象被創建....
School無參構造方法執行,實例對象被創建....
SubSchool無參構造方法被執行,實例對象被創建....
Student實例: Student{name='荷包蛋', age=20, school=School{name='nefu', address='哈爾濱'}}
Process finished with exit code 0

注意

  • 為什麼SubSchool和School實體類對象都被註冊了,在上述測試中,只是School的實體類對象被注入Student對象?
  • 原因:在同源類型的注入中,若進行父子類型的依賴注入,不是按照名稱注入時,會按照註冊的實體類對象的名稱二次選擇
  • 二次選擇的原則:註冊的實體類對象的名稱和待注入的目標屬性名稱相同的,優先被選擇為注入數據

實體類

  • School修改為
@Component("schoolFu")
public class School {
	//......
}
  • SubSchool修改為
@Component("school")
public class SubSchool extends School{
	//......
}

測試8

  • 測試7完全相同,不再贅述

測試輸出8

  • 此時被注入到Student實例對象中的是SubSchool實例對象
School無參構造方法執行,實例對象被創建....
Student無參構造方法被執行,實例對象被創建....
School無參構造方法執行,實例對象被創建....
SubSchool無參構造方法被執行,實例對象被創建....
Student實例: Student{name='荷包蛋', age=20, school=SubSchool{name='nefu附小', address='香坊區'}}
Process finished with exit code 0

對應的名稱注入

實體類

  • 為Student實體類新增註解@Qualifier(“schoolFu”)
@Component
public class Student {
    @Autowired
    @Qualifier("schoolFu")
    private School school;
	//......
}

測試9

  • 和測試7完全相同,不再贅述

測試輸出9

  • 此時被注入到Student實例對象中的是School實例對象,因為@Qualifier(“schoolFu”)指定的注入數據和School實體類的註冊類型相同,根據指定名稱完成注入
School無參構造方法執行,實例對象被創建....
Student無參構造方法被執行,實例對象被創建....
School無參構造方法執行,實例對象被創建....
SubSchool無參構造方法被執行,實例對象被創建....
Student實例: Student{name='荷包蛋', age=20, school=School{name='nefu', address='哈爾濱'}}
Process finished with exit code 0