本文摘錄自筆者開源的 Java 學習&面試指南(Github 收穫146k star):JavaGuide 。
這篇文章會從下面從以下幾個問題展開對 IoC & AOP 的解釋
- 什麼是 IoC?
- IoC 解決了什麼問題?
- IoC 和 DI 的區別?
- 什麼是 AOP?
- AOP 解決了什麼問題?
- AOP 的應用場景有哪些?
- AOP 為什麼叫做切面編程?
- AOP 實現方式有哪些?
首先聲明:IoC & AOP 不是 Spring 提出來的,它們在 Spring 之前其實已經存在了,只不過當時更加偏向於理論。Spring 在技術層次將這兩個思想進行了很好的實現。
IoC (Inversion of control )
什麼是 IoC?
IoC (Inversion of Control )即控制反轉/反轉控制。它是一種思想不是一個技術實現。描述的是:Java 開發領域對象的創建以及管理的問題。
例如:現有類 A 依賴於類 B
- 傳統的開發方式 :往往是在類 A 中手動通過 new 關鍵字來 new 一個 B 的對象出來
- 使用 IoC 思想的開發方式 :不通過 new 關鍵字來創建對象,而是通過 IoC 容器(Spring 框架) 來幫助我們實例化對象。我們需要哪個對象,直接從 IoC 容器裏面去取即可。
從以上兩種開發方式的對比來看:我們 “喪失了一個權力” (創建、管理對象的權力),從而也得到了一個好處(不用再考慮對象的創建、管理等一系列的事情)
為什麼叫控制反轉?
- 控制 :指的是對象創建(實例化、管理)的權力
- 反轉 :控制權交給外部環境(IoC 容器)
IoC 解決了什麼問題?
IoC 的思想就是兩方之間不互相依賴,由第三方容器來管理相關資源。這樣有什麼好處呢?
- 對象之間的耦合度或者説依賴程度降低;
- 資源變的容易管理;比如你用 Spring 容器提供的話很容易就可以實現一個單例。
例如:現有一個針對 User 的操作,利用 Service 和 Dao 兩層結構進行開發
在沒有使用 IoC 思想的情況下,Service 層想要使用 Dao 層的具體實現的話,需要通過 new 關鍵字在UserServiceImpl 中手動 new 出 IUserDao 的具體實現類 UserDaoImpl(不能直接 new 接口類)。
很完美,這種方式也是可以實現的,但是我們想象一下如下場景:
開發過程中突然接到一個新的需求,針對IUserDao 接口開發出另一個具體實現類。因為 Server 層依賴了IUserDao的具體實現,所以我們需要修改UserServiceImpl中 new 的對象。如果只有一個類引用了IUserDao的具體實現,可能覺得還好,修改起來也不是很費力氣,但是如果有許許多多的地方都引用了IUserDao的具體實現的話,一旦需要更換IUserDao 的實現方式,那修改起來將會非常的頭疼。
使用 IoC 的思想,我們將對象的控制權(創建、管理)交由 IoC 容器去管理,我們在使用的時候直接向 IoC 容器 “要” 就可以了
IoC 和 DI 有區別嗎?
IoC(Inverse of Control:控制反轉)是一種設計思想或者説是某種模式。這個設計思想就是 將原本在程序中手動創建對象的控制權交給第三方比如 IoC 容器。 對於我們常用的 Spring 框架來説, IoC 容器實際上就是個 Map(key,value),Map 中存放的是各種對象。不過,IoC 在其他語言中也有應用,並非 Spring 特有。
IoC 最常見以及最合理的實現方式叫做依賴注入(Dependency Injection,簡稱 DI)。
老馬(Martin Fowler)在一篇文章中提到將 IoC 改名為 DI,原文如下,原文地址:https://martinfowler.com/articles/injection.html 。
老馬的大概意思是 IoC 太普遍並且不表意,很多人會因此而迷惑,所以,使用 DI 來精確指名這個模式比較好。
AOP(Aspect oriented programming)
這裏不會涉及太多專業的術語,核心目的是將 AOP 的思想説清楚。
什麼是 AOP?
AOP(Aspect Oriented Programming)即面向切面編程,AOP 是 OOP(面向對象編程)的一種延續,二者互補,並不對立。
AOP 的目的是將橫切關注點(如日誌記錄、事務管理、權限控制、接口限流、接口冪等等)從核心業務邏輯中分離出來,通過動態代理、字節碼操作等技術,實現代碼的複用和解耦,提高代碼的可維護性和可擴展性。OOP 的目的是將業務邏輯按照對象的屬性和行為進行封裝,通過類、對象、繼承、多態等概念,實現代碼的模塊化和層次化(也能實現代碼的複用),提高代碼的可讀性和可維護性。
AOP 為什麼叫面向切面編程?
AOP 之所以叫面向切面編程,是因為它的核心思想就是將橫切關注點從核心業務邏輯中分離出來,形成一個個的切面(Aspect)。
這裏順帶總結一下 AOP 關鍵術語(不理解也沒關係,可以繼續往下看):
- 橫切關注點(cross-cutting concerns) :多個類或對象中的公共行為(如日誌記錄、事務管理、權限控制、接口限流、接口冪等等)。
- 切面(Aspect):對橫切關注點進行封裝的類,一個切面是一個類。切面可以定義多個通知,用來實現具體的功能。
- 連接點(JoinPoint):連接點是方法調用或者方法執行時的某個特定時刻(如方法調用、異常拋出等)。
- 通知(Advice):通知就是切面在某個連接點要執行的操作。通知有五種類型,分別是前置通知(Before)、後置通知(After)、返回通知(AfterReturning)、異常通知(AfterThrowing)和環繞通知(Around)。前四種通知都是在目標方法的前後執行,而環繞通知可以控制目標方法的執行過程。
- 切點(Pointcut):一個切點是一個表達式,它用來匹配哪些連接點需要被切面所增強。切點可以通過註解、正則表達式、邏輯運算等方式來定義。比如
execution(* com.xyz.service..*(..))匹配com.xyz.service包及其子包下的類或接口。 - 織入(Weaving):織入是將切面和目標對象連接起來的過程,也就是將通知應用到切點匹配的連接點上。常見的織入時機有兩種,分別是編譯期織入(Compile-Time Weaving 如:AspectJ)和運行期織入(Runtime Weaving 如:AspectJ、Spring AOP)。
AOP 常見的通知類型有哪些?
- Before(前置通知):目標對象的方法調用之前觸發
- After (後置通知):目標對象的方法調用之後觸發
- AfterReturning(返回通知):目標對象的方法調用完成,在返回結果值之後觸發
- AfterThrowing(異常通知):目標對象的方法運行中拋出 / 觸發異常後觸發。AfterReturning 和 AfterThrowing 兩者互斥。如果方法調用成功無異常,則會有返回值;如果方法拋出了異常,則不會有返回值。
- Around (環繞通知):編程式控制目標對象的方法調用。環繞通知是所有通知類型中可操作範圍最大的一種,因為它可以直接拿到目標對象,以及要執行的方法,所以環繞通知可以任意的在目標對象的方法調用前後搞事,甚至不調用目標對象的方法
AOP 解決了什麼問題?
OOP 不能很好地處理一些分散在多個類或對象中的公共行為(如日誌記錄、事務管理、權限控制、接口限流、接口冪等等),這些行為通常被稱為 橫切關注點(cross-cutting concerns) 。如果我們在每個類或對象中都重複實現這些行為,那麼會導致代碼的冗餘、複雜和難以維護。
AOP 可以將橫切關注點(如日誌記錄、事務管理、權限控制、接口限流、接口冪等等)從 核心業務邏輯(core concerns,核心關注點) 中分離出來,實現關注點的分離。
以日誌記錄為例進行介紹,假如我們需要對某些方法進行統一格式的日誌記錄,沒有使用 AOP 技術之前,我們需要挨個寫日誌記錄的邏輯代碼,全是重複的的邏輯。
public CommonResponse<Object> method1() {
// 業務邏輯
xxService.method1();
// 省略具體的業務處理邏輯
// 日誌記錄
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 省略記錄日誌的具體邏輯 如:獲取各種信息,寫入數據庫等操作...
return CommonResponse.success();
}
public CommonResponse<Object> method2() {
// 業務邏輯
xxService.method2();
// 省略具體的業務處理邏輯
// 日誌記錄
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 省略記錄日誌的具體邏輯 如:獲取各種信息,寫入數據庫等操作...
return CommonResponse.success();
}
// ...
使用 AOP 技術之後,我們可以將日誌記錄的邏輯封裝成一個切面,然後通過切入點和通知來指定在哪些方法需要執行日誌記錄的操作。
// 日誌註解
@Target({ElementType.PARAMETER,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 描述
*/
String description() default "";
/**
* 方法類型 INSERT DELETE UPDATE OTHER
*/
MethodType methodType() default MethodType.OTHER;
}
// 日誌切面
@Component
@Aspect
public class LogAspect {
// 切入點,所有被 Log 註解標註的方法
@Pointcut("@annotation(cn.javaguide.annotation.Log)")
public void webLog() {
}
/**
* 環繞通知
*/
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 省略具體的處理邏輯
}
// 省略其他代碼
}
這樣的話,我們一行註解即可實現日誌記錄:
@Log(description = "method1",methodType = MethodType.INSERT)
public CommonResponse<Object> method1() {
// 業務邏輯
xxService.method1();
// 省略具體的業務處理邏輯
return CommonResponse.success();
}
AOP 的應用場景有哪些?
- 日誌記錄:自定義日誌記錄註解,利用 AOP,一行代碼即可實現日誌記錄。
- 性能統計:利用 AOP 在目標方法的執行前後統計方法的執行時間,方便優化和分析。
- 事務管理:
@Transactional註解可以讓 Spring 為我們進行事務管理比如回滾異常操作,免去了重複的事務管理邏輯。@Transactional註解就是基於 AOP 實現的。 - 權限控制:利用 AOP 在目標方法執行前判斷用户是否具備所需要的權限,如果具備,就執行目標方法,否則就不執行。例如,SpringSecurity 利用
@PreAuthorize註解一行代碼即可自定義權限校驗。 - 接口限流:利用 AOP 在目標方法執行前通過具體的限流算法和實現對請求進行限流處理。
- 緩存管理:利用 AOP 在目標方法執行前後進行緩存的讀取和更新。
- ……
AOP 實現方式有哪些?
AOP 的常見實現方式有動態代理、字節碼操作等方式。
Spring AOP 就是基於動態代理的,如果要代理的對象,實現了某個接口,那麼 Spring AOP 會使用 JDK Proxy,去創建代理對象,而對於沒有實現接口的對象,就無法使用 JDK Proxy 去進行代理了,這時候 Spring AOP 會使用 Cglib 生成一個被代理對象的子類來作為代理,如下圖所示:
當然你也可以使用 AspectJ !Spring AOP 已經集成了 AspectJ ,AspectJ 應該算的上是 Java 生態系統中最完整的 AOP 框架了。
Spring AOP 屬於運行時增強,而 AspectJ 是編譯時增強。 Spring AOP 基於代理(Proxying),而 AspectJ 基於字節碼操作(Bytecode Manipulation)。
Spring AOP 已經集成了 AspectJ ,AspectJ 應該算的上是 Java 生態系統中最完整的 AOP 框架了。AspectJ 相比於 Spring AOP 功能更加強大,但是 Spring AOP 相對來説更簡單,
如果我們的切面比較少,那麼兩者性能差異不大。但是,當切面太多的話,最好選擇 AspectJ ,它比 Spring AOP 快很多。
最後
自薦一個非常不錯的 Java 教程類開源項目:JavaGuide 。這是我在大三開始準備秋招面試的時候創建的,已經維護了 4 年多了,目前這個項目在 Github 上收到了 125k+ 的 star。
並且,這個項目還推出了 PDF 版本,目前已經更新到第五版了,完全免費:完結撒花!時隔 596 天,《JavaGuide 面試突擊版》5.0 來啦!。