1.概述
切面編程(AOP)是一種通過預編譯方式和運行期間動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護的技術。核心思想是將程序的關注點與源代碼進行分離,通過在程序中插入自己的代碼來實現(xiàn)切入點,從而實現(xiàn)對業(yè)務邏輯代碼的隔離,降低它們之間的耦合度,提高程序的可維護性和可重用性,同時提高了開發(fā)的效率。
HarmonyOS主要通過插樁機制來實現(xiàn)切面編程,并提供了Aspect類,包括addBefore、addAfter和replace接口。
2.AOP與傳統(tǒng)方案對比
| 特性 | 鴻蒙AOP | 傳統(tǒng)方案 |
|---|---|---|
| 代碼侵入性 | 低(無需修改原代碼,只需添加橫切代碼) | 高(橫切代碼分散各處) |
| 可維護性 | 高(橫切代碼集中管控) | 低(橫切代碼需多處修改) |
| 可復用性 | 高(100%復用) | 低(復制粘貼) |
| 可讀性 | 高(橫切代碼集中管理,業(yè)務邏輯純凈) | 低(橫切代碼分散各處,代碼臃腫,冗余) |
| 動態(tài)調(diào)控 | 高(運行時啟用/禁用切面) | 低(需要修改代碼,重新編譯) |
3.接口
addBefore
- 在指定的類對象的原方法執(zhí)行前插入一個函數(shù)。
- 原方法執(zhí)行前,先執(zhí)行插入的函數(shù)邏輯,再執(zhí)行指定類對象的原方法。
?適用場景:參數(shù)校驗、日志記錄、性能統(tǒng)計等。
/**
* 在指定的類對象的原方法執(zhí)行前插入一個函數(shù)
* @param targetClass 指定的類對象
* @param methodName 指定的方法名,不支持read-only方法。
* @param isStatic 指定的原方法是否為靜態(tài)方法。true表示靜態(tài)方法,false表示實例方法。
* @param before 要插入的函數(shù)對象
* 如果:函數(shù)有參數(shù),則第一個參數(shù)是this對象
* (若isStatic為true,則為類對象即targetClass;
* 若isStatic為false,則為調(diào)用方法的實例對象),
* 其余參數(shù)是原方法參數(shù),函數(shù)也可以無參數(shù)。
*/
static addBefore(
targetClass: Object,
methodName: string,
isStatic: boolean,
before: Function
): void;
/**
* befor函數(shù)
* @param instance isStatic為true = targetClass
* isStatic為false = Object
* @param ...args 插入方法的參數(shù)
*/
before: (
instance: Object,
...args: ESObject[]
) => {
};
addAfter
- 在指定的類方法執(zhí)行后插入一個函數(shù)。最終返回值是插入函數(shù)執(zhí)行后的返回值。
- 原方法執(zhí)行后,執(zhí)行插入的函數(shù)邏輯,返回插入函數(shù)的返回值。
?適用場景:方法執(zhí)行監(jiān)控和統(tǒng)計、保存原數(shù)據(jù)。
/**
* 在指定的類對象的原方法執(zhí)行后插入一個函數(shù),最終返回值是插入函數(shù)執(zhí)行后的返回值
* @param targetClass 指定的類對象
* @param methodName 指定的方法名,不支持read-only方法。
* @param isStatic 指定的原方法是否為靜態(tài)方法。true表示靜態(tài)方法,false表示實例方法。
* @param after 要插入的函數(shù)對象
* 如果:函數(shù)有參數(shù),則第一個參數(shù)是this對象
* (若isStatic為true,則為類對象即targetClass;
* 若isStatic為false,則為調(diào)用方法的實例對象),
* 第二個參數(shù)是原方法的返回值(如果原方法沒有返回值,則為undefined),
* 其余參數(shù)是原方法參數(shù),函數(shù)也可以無參數(shù)。
*/
static addAfter(
targetClass: Object,
methodName: string,
isStatic: boolean,
after: Function
): void;
/**
* after函數(shù)
* @param instance isStatic為true = targetClass
* isStatic為false = Object
* @param returnValue 原方法的返回值,如果沒有則為undefined
* @param ...args 插入方法的參數(shù)
*/
after: (
instance: Object,
returnValue: any,
...args: ESObject[]
) => {
};
replace
- 將指定的類的方法的替換為另一個函數(shù)。
- 調(diào)用類的方法執(zhí)行時,只會執(zhí)行替換后的函數(shù)邏輯。最終返回值為替換的函數(shù)執(zhí)行完畢的返回值。
?適用場景:方法邏輯替換。
/**
* 替換指定類的方法,替換后,原方法將不再執(zhí)行,將執(zhí)行替換后的函數(shù),并且返回替換后的方法返回值
* @param targetClass 指定的類對象
* @param methodName 指定的方法名,不支持read-only方法。
* @param isStatic 指定的原方法是否為靜態(tài)方法。true表示靜態(tài)方法,false表示實例方法。
* @param instead 替換原方法的函數(shù)
* 如果:函數(shù)有參數(shù),則第一個參數(shù)是this對象
* (若isStatic為true,則為類對象即targetClass;
* 若isStatic為false,則為調(diào)用方法的實例對象),
* 其余參數(shù)是原方法參數(shù),函數(shù)也可以無參數(shù)。
*/
static replace(
targetClass: Object,
methodName: string,
isStatic: boolean,
instead: Function
): void;
/**
* instead函數(shù)
* @param instance isStatic為true = targetClass
* isStatic為false = Object
* @param ...args 替換方法的參數(shù)
*/
instead: (
instance: Object,
...args: ESObject[]
) => {
};
4.使用場景
場景1:方法參數(shù)校驗
場景2:統(tǒng)計方法執(zhí)行次數(shù)、時間
場景3:校驗方法返回值
場景4:替換方法實現(xiàn)
5.使用注意事項
- 目標類需要導入,沒有導出的場景,可以通過實例的constructor屬性獲取目標類。
- 目標方法名不能被混淆。
- 對父類作為目標類插樁會影響所有子類;對子類作為目標類插樁不會影響父類(無論方法是否是繼承自父類的),但是會影響子類的所有子類。
- 接口的第四個參數(shù)是回調(diào)函數(shù),回調(diào)函數(shù)中第一個參數(shù)是執(zhí)行方法調(diào)用的this對象。如果通過這個調(diào)用原方法,并且沒有退出機制,容易造成無限遞歸調(diào)用。如果需要調(diào)用原方法,需要在接口調(diào)用前將原方法存儲起來。不推薦的用法參考如下示例。
- 錯誤示例
class Test { foo() {} } util.Aspect.addBefore(Test, 'foo', false, (instance: Test) => { // 無限遞歸 instance.foo(); }); new Test().foo();- 正確示例
class Test { foo() {} } // 將原方法實現(xiàn)先保存起來 let originalFoo = new Test().foo; util.Aspect.addBefore(Test, 'foo', false, (instance: Test) => { // 如果原方法沒有使用this,則可以直接調(diào)用原方法originalFoo(); // 如果原方法中使用了this,應該使用bind綁定instance,但是會有編譯warningoriginalFoo.bind(instance); }); - 不推薦對struct的方法插樁/替換實現(xiàn)。
@Component struct Index { foo(){} build(){}; } util.Aspect.addBefore(Index, 'foo', false, ...); util.Aspect.replace(Index, 'build', false, ...); - 接口不限制對系統(tǒng)提供的類方法進行插樁。只要類和方法在運行時是實際存在的對象,并且方法的屬性描述符的writable字段為true,就可以使用對應接口進行插樁和替換。
說明
如果類方法的屬性描述符的writable字段為false,比如凍結(freeze) 的場景, 則不能調(diào)用接口操作這個類方法。
方法的屬性描述符的writable字段默認為true。
- 使用Aspect類接口進行插樁,對AoT和JIT編譯后的性能沒有明顯影響。