閱覽了很多篇博文,對于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的角度)
為什麼要反轉?
在了解為什麼要反轉之前,我們要知道一個原則:依賴倒置原則。
什麼是依賴倒置原則?
假設我們設計一輛汽車:先設計輪子,然後根據輪子大小設計底盤,接着根據底盤設計車身,最後根據車身設計好整個汽車。這裡就出現了一個“依賴”關系:汽車依賴車身,車身依賴底盤,底盤依賴輪子。但這種設計維護性很低。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLzgzN0UTO0kDM3EDNwEjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
那萬一上司說,我覺得這個輪胎太醜了,我要換一個,那我是不是不能随便換啊,因為依賴我的有一連串的東西,你讓我改輪子,那不直接改一輛車算了?
如果換一個思路
我們先設計汽車的大概樣子,然後根據汽車的樣子來設計車身,根據車身來設計底盤,最後根據底盤來設計輪子。這時候,依賴關系就倒置過來了:輪子依賴底盤,底盤依賴車身,車身依賴汽車。
這時候,上司再說要改動輪子的設計,我們就隻需要改動輪子的設計,而不需要動底盤、車身、汽車的設計了。
這就是依賴倒置原則——把原本的高層建築依賴底層建築“倒置”過來,變成底層建築依賴高層建築。高層建築決定需要什麼,底層去實作這樣的需求,但是高層并不用管底層是怎麼實作的。
控制反轉(IoC)就是依賴倒置原則的一種代碼設計的思路。具體采用的方法就是所謂的依賴注入(DI)。
是以,有的人認為DI是IOC的另一種說法,我覺得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即可
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. 運作結果
說明
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的時候看着視訊寫的,現在也忘了看的是哪個視訊啦哈哈哈
感謝能看到這裡,不知道這篇文章對你有沒有幫助,希望一起加油吧!