HarmonyOS 开发实战:用 util.Aspect 玩转 AOP 切面编程
日志埋点、参数校验、耗时统计......这些和业务无关却又散落各处的代码,正在悄悄拖垮你的项目可维护性。HarmonyOS 提供的
util.Aspect类,用三个接口就能把这些"横切关注点"从业务代码里干净地剥离出来。
一、为什么需要 AOP
先看一段典型的"坏味道"代码:
javascript
class OrderService {
createOrder(params: OrderParams) {
console.info(`[Log] createOrder called, params: ${JSON.stringify(params)}`); // 日志
const start = Date.now(); // 耗时统计开始
if (!params.userId) { // 参数校验
throw new Error('userId is required');
}
// ...真正的业务逻辑可能只有几行
console.info(`[Log] createOrder cost: ${Date.now() - start}ms`); // 耗时统计结束
}
}
日志、校验、性能统计这些逻辑和业务本身毫无关系,却被迫和业务代码搅在一起。这类问题在编程领域被称为横切关注点(Cross-cutting Concerns) ,而 AOP(Aspect Oriented Programming,面向切面编程)就是专门解决它的范式:不修改业务源码,在方法执行前后动态插入通用逻辑。
HarmonyOS 在 ArkTS 中通过运行时插桩机制 实现 AOP,核心就是 @kit.ArkTS 里 util 模块下的 Aspect 类(API 11 起支持,API 12 起可在元服务中使用)。
javascript
import { util } from '@kit.ArkTS';
二、util.Aspect 的三个核心接口
util.Aspect 一共提供三个静态接口,对应三种插桩方式:
| 接口 | 作用 |
|---|---|
addBefore() |
在原方法执行前插入自定义函数 |
addAfter() |
在原方法执行后插入自定义函数,可拿到原方法返回值 |
replace() |
直接替换原方法的实现 |
2.1 addBefore:方法前插桩
php
static addBefore(targetClass: Object, methodName: string, isStatic: boolean, before: Function): void
| 参数 | 说明 |
|---|---|
targetClass |
目标类对象(注意是类本身,不是实例) |
methodName |
原方法名,不支持 read-only 方法 |
isStatic |
原方法是否为静态方法,true 静态 / false 实例方法 |
before |
要插入的函数。第一个参数是 this 对象(实例方法为实例、静态方法为类对象),其余参数是原方法的参数 |
看一个完整示例,同时覆盖实例方法和静态方法两种场景:
typescript
import { util } from '@kit.ArkTS';
class MyClass {
msg: string = 'msg000';
foo(arg: string): string {
console.info('foo arg is ' + arg);
return this.msg;
}
static data: string = 'data000';
static bar(arg: string): string {
console.info('bar arg is ' + arg);
return MyClass.data;
}
}
// 对实例方法 foo 前插桩:回调第一个参数是实例对象
util.Aspect.addBefore(MyClass, 'foo', false, (instance: MyClass, arg: string) => {
console.info('[Before] arg is ' + arg);
instance.msg = 'msg111'; // 甚至可以在方法执行前修改成员变量
});
// 对静态方法 bar 前插桩:回调第一个参数是类对象本身
util.Aspect.addBefore(MyClass, 'bar', true, (target: Object, arg: string) => {
console.info('[Before] arg is ' + arg);
Reflect.set(target, 'data', 'data111'); // 静态成员需要通过 Reflect 修改
});
let asp = new MyClass();
let result = asp.foo('123');
// 输出顺序:[Before] arg is 123 → foo arg is 123
console.info('result is ' + result); // result is msg111(原方法读到的是被修改后的 msg)
let res = MyClass.bar('456');
// 输出顺序:[Before] arg is 456 → bar arg is 456
console.info('res is ' + res); // res is data111
两个值得记住的细节:
- 插入函数先于原方法执行 ,所以在
addBefore里修改成员变量,会直接影响原方法读到的值。 - 静态方法插桩时拿到的是类对象,修改静态成员需要用
Reflect.set()。
2.2 addAfter:方法后插桩
php
static addAfter(targetClass: Object, methodName: string, isStatic: boolean, after: Function): void
参数和 addBefore 基本一致,区别在于插入函数 after 的参数列表:第二个参数是原方法的返回值 (原方法无返回值时为 undefined),之后的参数才是原方法的参数。
⚠️ 最大的坑在这里:addAfter 插桩后,方法的最终返回值 = 插入函数的返回值。 如果你只是想做些记录工作,一定要记得把原返回值透传回去,否则调用方会拿到 undefined。
typescript
import { util } from '@kit.ArkTS';
import { WebHandler } from './someThirdParty';
// 校验第三方方法的返回值必须以 https 开头
util.Aspect.addAfter(WebHandler, 'getWebAddrHttps', false, (instance: WebHandler, ret: string) => {
if (!ret.startsWith('https')) {
throw new Error("Handler's method 'getWebAddrHttps': return value does not start with 'https'");
}
// 校验通过,千万别忘了把原返回值 return 回去!
return ret;
});
new WebHandler().getWebAddrHttps();
2.3 replace:替换方法实现
php
static replace(targetClass: Object, methodName: string, isStatic: boolean, instead: Function): void
replace 更激进------原方法不会执行,直接由 instead 函数取而代之。适合测试桩、功能降级、Hook 第三方库行为等场景:
typescript
class Test {
name: string = 'HarmonyOS';
getName(): string {
return this.name;
}
}
util.Aspect.replace(Test, 'getName', false, (instance: Test) => {
instance.name = '被替换了';
return 'replace 后的返回值';
});
let test = new Test();
console.info(test.getName()); // 输出:replace 后的返回值(原 getName 根本没执行)
三、四个实战场景
场景一:方法耗时与调用次数统计
性能监控是 AOP 最经典的应用。addBefore + addAfter 组合使用,给任意方法加上耗时统计,业务代码零改动:
typescript
import { util } from '@kit.ArkTS';
let begin: number = 0;
let count: number = 0;
let totalCost: number = 0;
function addTimePrinter(targetClass: Object, methodName: string, isStatic: boolean) {
// 方法执行前:记录起始时间
util.Aspect.addBefore(targetClass, methodName, isStatic, () => {
begin = new Date().getTime();
});
// 方法执行后:累计耗时与次数
util.Aspect.addAfter(targetClass, methodName, isStatic, (instance: Object, ret: Object) => {
count += 1;
totalCost += new Date().getTime() - begin;
console.info(`[Perf] ${methodName} 第 ${count} 次调用,累计耗时 ${totalCost}ms`);
return ret; // 透传原返回值
});
}
// 使用:给图片下载方法加上统计
addTimePrinter(DownloadImage, 'instanceMethod', false);
DownloadImage().instanceMethod(this.url);
这也是华为官方"加载时间及次数统计"示例的实现思路,给关键路径方法批量挂上统计,一份数据支撑性能优化。
场景二:方法调用计数
只想统计某个方法被调用了多少次?一行 addBefore 就够:
javascript
let countFoo = 0;
util.Aspect.addBefore(Test, 'foo', false, () => {
countFoo++;
});
new Test().foo();
console.info('countFoo = ' + countFoo); // countFoo = 1
场景三:成员变量 / 参数合法性校验
addBefore 回调的第一个参数是 this 对象,可以直接读取实例的成员变量做防御性校验,异常时直接抛错阻断方法执行:
typescript
export class Person {
name: string;
age: number;
constructor(n: string, a: number) {
this.name = n;
this.age = a;
}
getInfo(): string {
return 'name: ' + this.name + ', age: ' + this.age.toString();
}
}
util.Aspect.addBefore(Person, 'getInfo', false, (instance: Person) => {
if (instance.name.length === 0) {
throw new Error('empty name');
}
if (instance.age < 0) {
throw new Error('invalid age');
}
});
new Person('c', -1).getInfo(); // 抛出 Error: invalid age,getInfo 不会执行
校验逻辑集中在一处,业务类里完全看不到防御代码的影子。
场景四:对系统 SDK 接口插桩(封装代理)
想监控 window.createWindow()、rcp.createSession() 这类系统接口的调用?直接插桩会失败------util.Aspect 只能作用于 ArkTS 层的类或对象方法,无法作用于 native 层实现的系统模块。
官方的解法是:先封装一层 ArkTS 包装类(代理),再对包装类插桩:
typescript
import { window } from '@kit.ArkUI';
import { util } from '@kit.ArkTS';
// 1. 封装代理类
export class WindowWrap {
private static instance: WindowWrap = new WindowWrap();
private constructor() {}
public static getInstance() {
return WindowWrap.instance;
}
public createWindow() {
let config: window.Configuration = {
name: 'test',
windowType: window.WindowType.TYPE_DIALOG
};
window.createWindow(config, () => {
// do something
});
}
}
// 2. 对代理类插桩
util.Aspect.addBefore(WindowWrap, 'createWindow', false, (instance: WindowWrap): void => {
console.info('[Aspect] createWindow 被调用');
});
// 3. 业务中统一走代理调用
WindowWrap.getInstance().createWindow();
同理,如果想在应用拉起时记录目标包名、监控 rcp 网络会话创建,都可以套用这个"代理 + 插桩"模式。
四、避坑指南
实际使用中容易踩的坑,集中整理一下:
-
isStatic传错是最常见错误。 实例方法传false、静态方法传true,传反了插桩不生效或行为异常。 -
addAfter必须 return 原返回值。 插入函数的返回值会覆盖原方法的返回值,忘记return ret会让调用方拿到undefined,这类 bug 排查起来很隐蔽。 -
不支持 read-only 方法。 对只读方法插桩不会有效果。
-
重复插桩的执行顺序:后插桩的先执行。 对同一个方法连续调用多次
addBefore,后插入的函数会先执行(类似栈结构):javascriptutil.Aspect.addBefore(Test, 'printData', true, () => console.info('before 1')); util.Aspect.addBefore(Test, 'printData', true, () => console.info('before 2')); Test.printData(); // 输出顺序:before 2 → before 1 → 原方法 -
无法插桩 native 层方法。 系统 SDK 中很多接口(如 rcp、window)是 native 实现,必须通过封装代理类间接插桩。
-
插桩是全局生效的运行时行为。 一旦执行
addBefore/addAfter/replace,对该类的所有实例都生效,且没有官方提供的"取消插桩"接口。建议在应用初始化阶段统一注册,避免在页面生命周期里反复插桩导致逻辑叠加。 -
元服务场景需 API 12+。 普通应用 API 11 起可用,元服务中从 API version 12 开始支持。
五、总结
util.Aspect 用三个接口覆盖了 AOP 的核心诉求:
addBefore:方法前拦截------参数校验、调用计数、执行前状态准备;addAfter:方法后增强------返回值校验、耗时统计、结果记录(记得透传返回值);replace:方法替换------测试打桩、行为 Hook。
它不能替代所有 AOP 框架(比如没有切面取消机制、无法插桩 native 方法),但对于日志埋点、性能监控、防御性校验这类高频需求,不需要引入任何三方库,几十行代码就能让业务代码回归纯粹。下次再想在业务方法里写 Date.now() 的时候,不妨想想:这段逻辑,是不是该交给 Aspect?
参考资料:
End