天天看點

Spring IOC與DI、反射的了解 含小案例說明

閱覽了很多篇博文,對于Spring IOC和DI看到很多人介紹的感覺都很含糊,讀完之後還是覺得混淆的不行,也有的人認為DI是IOC的另一種說法…emm,可能個人了解不同吧,我的觀點并不然。但這種東西,見仁見智吧~下面來談一談Spring IOC的實作原理,當然會提及到DI。

學習過Spring的都知道,Spring的核心是IOC和AOP,在大半年前我學習Spring時,其實對這兩個概念并沒有了解到位,隻是覺得架構就是比葫蘆畫瓢,覺得不重要,其實并不是,無論是為了更好的了解和使用這個架構還是對于出去面試,都有重要的作用和意義。

什麼是Spring IOC

Inversion of Control,即“控制反轉”,不是什麼技術,而是一種設計思想。在Java開發中,Ioc意味着将你設計好的對象交給容器控制,而不是傳統的在你的對象内部直接控制。如何了解好Ioc呢?了解好Ioc的關鍵是要明确“誰控制誰,控制什麼,為何是反轉(有反轉就應該有正轉了),哪些方面反轉了?為什麼要反轉”

1. 誰控制誰,控制什麼:

傳統Java SE程式設計,我們直接在對象内部通過new進行建立對象,是程式主動去建立依賴對象;而IoC是有專門一個容器來建立這些對象,即由Ioc容器來控制對象的建立;
誰控制誰?
是以當然是IoC容器控制了對象;
控制什麼?
那就是主要控制了外部資源擷取(不隻是對象 還可以檔案等)。
           

2. 為何是反轉,哪些方面反轉了,為什麼要反轉?

有反轉就有正轉,傳統應用程式是由我們自己在對象中主動控制去直接擷取依賴對象,也就是正轉;
而反轉則是由容器來幫忙建立及注入依賴對象;
為何是反轉?
因為由容器幫我們查找及注入依賴對象,對象隻是被動的接受依賴對象,是以是反轉;
哪些方面反轉了?
依賴對象的擷取被反轉了。
           

這是摘取别人的話,我自己是了解了,這裡我覺得說的通俗點就是:Class A 如果需要用到 Class B的方法,那我們一開始肯定會在Class A中new一個Class B的對象。 B b = new B(); 對吧,這就是他所謂的“正轉”(因為我也不知道有沒有這種說法,是以暫時打個引号)

那麼反轉就是,我将B這個對象交給容器,放進容器裡面裝着了,當你A需要我B的時候,容器就會像打針一樣給你注入進去。是以就是反轉。

我覺得可以用一句話概括,正轉是“我要你”,反轉是“我給你” 化主動為被動(這裡是以A的角度)

為什麼要反轉?

在了解為什麼要反轉之前,我們要知道一個原則:依賴倒置原則。

什麼是依賴倒置原則?

假設我們設計一輛汽車:先設計輪子,然後根據輪子大小設計底盤,接着根據底盤設計車身,最後根據車身設計好整個汽車。這裡就出現了一個“依賴”關系:汽車依賴車身,車身依賴底盤,底盤依賴輪子。但這種設計維護性很低。

Spring IOC與DI、反射的了解 含小案例說明

那萬一上司說,我覺得這個輪胎太醜了,我要換一個,那我是不是不能随便換啊,因為依賴我的有一連串的東西,你讓我改輪子,那不直接改一輛車算了?

如果換一個思路

我們先設計汽車的大概樣子,然後根據汽車的樣子來設計車身,根據車身來設計底盤,最後根據底盤來設計輪子。這時候,依賴關系就倒置過來了:輪子依賴底盤,底盤依賴車身,車身依賴汽車。

Spring IOC與DI、反射的了解 含小案例說明

這時候,上司再說要改動輪子的設計,我們就隻需要改動輪子的設計,而不需要動底盤、車身、汽車的設計了。

這就是依賴倒置原則——把原本的高層建築依賴底層建築“倒置”過來,變成底層建築依賴高層建築。高層建築決定需要什麼,底層去實作這樣的需求,但是高層并不用管底層是怎麼實作的。

控制反轉(IoC)就是依賴倒置原則的一種代碼設計的思路。具體采用的方法就是所謂的依賴注入(DI)。

是以,有的人認為DI是IOC的另一種說法,我覺得IOC是一種思路,而DI是實作這個思路的一個方法,這二者還是有差別的,而不是換一種說法【個人了解 如有錯誤希望能得到指正】

是以關系圖如下我覺得是比較準确的

Spring IOC與DI、反射的了解 含小案例說明

為什麼要用IoC

IoC 不是一種技術,隻是一種思想,一個重要的面向對象程式設計的法則,它能指導我們如何設計出松耦合、更優良的程式。傳統應用程式都是由我們在類内部主動建立依賴對象,進而導緻類與類之間高耦合,難于測試;有了IoC容器後,把建立和查找依賴對象的控制權交給了容器,由容器進行注入組合對象,是以對象與對象之間是松散耦合,這樣也友善測試,利于功能複用,更重要的是使得程式的整個體系結構變得非常靈活。

DI(Dependency Injection)即“依賴注入”

元件之間依賴關系由容器在運作期決定,形象的說,即由容器動态的将某個依賴關系注入到元件之中。**依賴注入的目的并非為軟體系統帶來更多功能,而是為了提升元件重用的頻率,并為系統搭建一個靈活、可擴充的平台。**通過依賴注入機制,我們隻需要通過簡單的配置,而無需任何代碼就可指定目标需要的資源,完成自身的業務邏輯,而不需要關心具體的資源來自何處,由誰實作。

了解DI的關鍵是:“誰依賴誰,為什麼需要依賴,誰注入誰,注入了什麼”

誰依賴于誰?
應用程式依賴于IoC容器;
為什麼需要依賴?
應用程式需要IoC容器來提供對象需要的外部資源
誰注入誰?
很明顯是IoC容器注入應用程式某個對象,應用程式依賴的對象
注入了什麼?
就是注入某個對象所需要的外部資源(包括對象、資源、常量資料)
           

小案例

這裡我用一個賬戶的業務實作來簡要說明一下IOC

Account類,就是一個賬戶實體類,這裡為了簡約顯示,就省略getter setter以及toString方法,當然,如果用lombok的話直接加注解就好咯~這裡就不展開了。

package com.fym.domain;

import java.io.Serializable;

/**
 * @Author fym
 * @Date 2020/8/18 19:07
 */
public class Account implements Serializable {
    private Integer id;
    private String name;
    private Float money;

}
           

賬戶的業務層接口:

package com.fym.service;

import com.fym.domain.Account;

import java.util.List;

/**
 * @Author fym
 * @Date 2020/8/18 19:06
 * 賬戶的業務層接口
 */
public interface IAccountService {
//    查詢所有
    List<Account> findAllAccount();

    Account findAccountById(Integer accountId); //查詢一個

    void saveAccount(Account account); //儲存

    void updateAccount(Account account);//更新

    void deleteAccount(Integer accountId);
}
           

業務層實作類

package com.fym.service.impl;

import com.fym.dao.IAccountDao;
import com.fym.domain.Account;
import com.fym.service.IAccountService;

import java.util.List;

/**
 * @Author fym
 * @Date 2020/8/18 19:11
 * 賬戶的業務層實作類
 */
public class AccountServiceImpl implements IAccountService {
    private IAccountDao accountDao;

    public Account findAccountById(Integer accountId) {
        return accountDao.findAccountById(accountId);
    }
}
           

持久層

package com.fym.dao;

import com.fym.domain.Account;

import java.util.List;

/**
 * @Author fym
 * @Date 2020/8/18 19:12
 * 賬戶的持久層接口
 */
public interface IAccountDao {
    
    Account findAccountById(Integer accountId); //查詢一個
}
           

持久層的實作類

package com.fym.dao.impl;

import com.fym.dao.IAccountDao;
import com.fym.domain.Account;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;

import java.sql.SQLException;
import java.util.List;

/**
 * @Author fym
 * @Date 2020/8/18 19:14
 */
public class AccountDaoImpl implements IAccountDao {

    private QueryRunner runner;

    public void setRunner(QueryRunner runner) {
        this.runner = runner;
    }


    public Account findAccountById(Integer accountId) {
        try {
            return runner.query("select * from account where id = ? ",new BeanHandler<Account>(Account.class),accountId);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}
           

這裡我們需要看什麼呢?主要看看業務層實作類,裡面有一句

為啥?因為它要用我持久層的查找賬戶的一條資訊這個方法呀,是以我業務層就是依賴持久層,那一開始我們的寫法應該是怎樣的?我需要dao這個對象是不是,那我得new出來呀

上面代碼什麼意思?

業務層調用持久層,并且此時業務層在依賴持久層的接口和實作類。如果此時沒有持久層實作類,編譯将不能通過。這種編譯期依賴關系,應該在我們開發中杜絕。我們需要優化代碼解決。

再來一個另外的例子

比如JDBC裡的,注冊驅動時,我們為什麼不使用 DriverManager 的 register 方法,而是采用 Class.forName 的方式?

public class JdbcDemo1 {
    public static void main(String[] args) throws Exception {
        //1.注冊驅動
        // DriverManager.registerDriver(new com.mysql.jdbc.Driver());
        Class.forName("com.mysql.jdbc.Driver"); //反射---------------下面會講
        //2.擷取連接配接
        //3.擷取預處理 sql 語句對象
        //4.擷取結果集
        //5.周遊結果集
        }
}
           

原因就是:

我們的類依賴了資料庫的具體驅動類(MySQL),如果這時候更換了資料庫品牌(比如 Oracle),需要修改源碼來重新資料庫驅動。這顯然不是我們想要的。 我們的類中不再依賴具體的驅動類,此時就算删除 MySQL 的驅動 jar 包,依然可以編譯(運作就不要想了,沒有驅動不可能運作成功的)。

這個就是我們需要解決的問題,我們都知道要高内聚,低耦合,那如果我直接這樣寫,耦合度就高了是不,那我們就是要解決這個問題

是以,知道我們IoC的作用了嗎?

削減計算機程式的耦合(解除我們代碼中的依賴關系)。

那麼我們是怎麼從第二句

改變成第一句

的呢?那這就是接下來的内容,如何進行配置

配置

基于xml的配置

在resources目錄下建立一個bean.xml配置檔案

1. 給配置檔案導入限制

找到spring架構手冊,進入core後ctrl F找到關鍵詞xmlns,導入到bean.xml即可

Spring IOC與DI、反射的了解 含小案例說明
2. 配置service和dao

bean 标簽:用于配置讓 spring 建立對象,并且存入 ioc 容器之中

id 屬性:對象的唯一辨別。

class 屬性:指定要建立對象的全限定類名

<!--把對象的建立交給spring來管理-->
<bean id="accountService" class="com.fym.service.impl.AccountServiceImpl"></bean>
<bean id="accountDao" class="com.fym.dao.impl.AccountDaoImpl"></bean>
           

那我們要把dao注入到service裡是不是 property裡的ref屬性就是reference,ref="accountDao"說明注入的是id為accountDao的bean

<!--業務層對象 Service-->
<bean id="accountService" class="com.fym.service.impl.AccountServiceImpl">
    <!--注入dao-->
    <property name="accountDao" ref="accountDao"></property>
</bean>
           

那同樣的呀,dao裡面不是有個QueryRunner嘛?一樣的道理,我們也把它給注進dao裡面

<!--配置dao對象-->
<bean id="accountDao" class="com.fym.dao.impl.AccountDaoImpl">
    <!--注入QueryRunner-->
    <property name="runner" ref="runner"></property>
</bean>
           

那QueryRunner你不得給它資料源呀?那我們也把資料源給注進QueryRunner,資料源就是我們連接配接資料庫的那些東西。

<!--配置QueryRunner對象-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
    <!--注入資料源-->
    <constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>

<!--配置資料源-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <!--連接配接資料庫的必備資訊-->
    <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/spring"></property>
    <property name="user" value="root"></property>
    <property name="password" value="123456"></property>
</bean>
           
3. 測試配置是否成功
/**
 * 模拟一個表現層,用于調用業務層
 */
public class Client {
//    擷取spring容器的ioc核心容器,并根據id擷取對象
    /**
     * 核心容器的兩個接口:
     * ApplicationContext: 單例對象适用       實際開發中更多的是使用該接口
     *      它在建構核心容器時,建立對象采取的政策是立即加載的方式,隻要一讀取完配置檔案就馬上建立配置檔案中配置的對象
     * BeanFactory: 多例對象适用
     *      它在建構核心容器時,建立對象采取的政策是延遲加載的方式,什麼時候根據id擷取對象了,什麼時候才真正的建立對象~
     * */
    public static void main(String[] args) {
        //1.擷取核心容器對象
        ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
        //2.根據id擷取bean對象的兩種方法
        IAccountService as=(IAccountService)ac.getBean("accountService");
        IAccountDao adao=ac.getBean("accountDao",IAccountDao.class);

        System.out.println(as);
        System.out.println(adao);
    }

}
           
4. 運作結果
Spring IOC與DI、反射的了解 含小案例說明

說明

ioc 解耦隻是降低他們的依賴關系,但不會消除。

例如:我們的業務層仍會調用持久層的方法。 那這種業務層和持久層的依賴關系,在使用spring之後,就讓spring來維護了。

簡單的說,就是坐等架構把持久層對象傳入業務層,而不用我們自己去擷取。

IOC底層實作原理——反射

我們在Spring的配置檔案中經常能看到如上所寫的那些bean标簽

那麼通過這樣配置,Spring是怎麼幫我們執行個體化對象,并且放到容器中去了了?對,就是通過反射

Spring通過配置進行執行個體化對象,并放到容器中的過程是怎樣的呢?我們看看僞代碼【為什麼是僞代碼呢…僞代碼更好了解,源碼好長,最近時間緊迫,還沒來得及剖析源碼,當然知道源碼是超級加分的!】

//解析<bean .../>元素的id屬性得到該字元串值為“courseDao”
String idStr = "accountDao";
//解析<bean .../>元素的class屬性得到該字元串值為“com.qcjy.learning.Dao.impl.CourseDaoImpl”
String classStr = "com.fym.dao.impl.AccountDaoImpl";
//利用反射知識,通過classStr擷取Class類對象
Class<?> cls = Class.forName(classStr);
//執行個體化對象
Object obj = cls.newInstance();
//container表示Spring容器
container.put(idStr, obj);
           

通過解析xml檔案,擷取到id屬性和class屬性裡面的内容,利用反射原理擷取到配置裡面類的執行個體對象,存入到Spring的bean容器中。

當一個類裡面需要應用另一類的對象時,Spring的配置如下所示:

<!--業務層對象 Service-->
<bean id="accountService" class="com.fym.service.impl.AccountServiceImpl">
    <!--注入dao-->
    <!-- 控制調用findAccountById()方法,将容器中的courseDao bean作為傳入參數 -->
    <property name="accountDao" ref="accountDao"></property>
</bean>
           

我們繼續用僞代碼的形式來模拟實作一下Spring底層處理原理:

//解析<property .../>元素的name屬性得到該字元串值為“accountDao”
String nameStr = "accountDao";
//解析<property .../>元素的ref屬性得到該字元串值為“accountDao”
String refStr = "accountDao";
//生成将要調用setter方法名
String setterName = "set" + nameStr.substring(0, 1).toUpperCase()
		+ nameStr.substring(1);
//擷取spring容器中名為refStr的Bean,該Bean将會作為傳入參數
Object paramBean = container.get(refStr);
//擷取setter方法的Method類,此處的cls是剛才反射代碼得到的Class對象
Method setter = cls.getMethod(setterName, paramBean.getClass());
//調用invoke()方法,此處的obj是剛才反射代碼得到的Object對象
setter.invoke(obj, paramBean);
           

隻要在代碼或配置檔案中看到類的完整路徑(包.類),其底層原理基本上使用的就是Java的反射機制

參考

汽車例子:https://blog.csdn.net/sinat_28007043/article/details/106111498

部分講解:https://www.cnblogs.com/xdp-gacl/p/4249939.html

反射:https://blog.csdn.net/mlc1218559742/article/details/52774805

小案例代碼是自己當時學習Spring的時候看着視訊寫的,現在也忘了看的是哪個視訊啦哈哈哈

感謝能看到這裡,不知道這篇文章對你有沒有幫助,希望一起加油吧!