1.概述
切面编程(AOP)是一种通过预编译方式和运行期间动态代理实现程序功能的统一维护的技术。核心思想是将程序的关注点与源代码进行分离,通过在程序中插入自己的代码来实现切入点,从而实现对业务逻辑代码的隔离,降低它们之间的耦合度,提高程序的可维护性和可重用性,同时提高了开发的效率。
HarmonyOS主要通过插桩机制来实现切面编程,并提供了Aspect类,包括addBefore、addAfter和replace接口。
2.AOP与传统方案对比
特性 | 鸿蒙AOP | 传统方案 |
---|---|---|
代码侵入性 | 低(无需修改原代码,只需添加横切代码) | 高(横切代码分散各处) |
可维护性 | 高(横切代码集中管控) | 低(横切代码需多处修改) |
可复用性 | 高(100%复用) | 低(复制粘贴) |
可读性 | 高(横切代码集中管理,业务逻辑纯净) | 低(横切代码分散各处,代码臃肿,冗余) |
动态调控 | 高(运行时启用/禁用切面) | 低(需要修改代码,重新编译) |
3.接口
addBefore
- 在指定的类对象的原方法执行前插入一个函数。
- 原方法执行前,先执行插入的函数逻辑,再执行指定类对象的原方法。
适用场景:参数校验、日志记录、性能统计等。
/**
* 在指定的类对象的原方法执行前插入一个函数
* @param targetClass 指定的类对象
* @param methodName 指定的方法名,不支持read-only方法。
* @param isStatic 指定的原方法是否为静态方法。true表示静态方法,false表示实例方法。
* @param before 要插入的函数对象
* 如果:函数有参数,则第一个参数是this对象
* (若isStatic为true,则为类对象即targetClass;
* 若isStatic为false,则为调用方法的实例对象),
* 其余参数是原方法参数,函数也可以无参数。
*/
static addBefore(
targetClass: Object,
methodName: string,
isStatic: boolean,
before: Function
): void;
/**
* befor函数
* @param instance isStatic为true = targetClass
* isStatic为false = Object
* @param ...args 插入方法的参数
*/
before: (
instance: Object,
...args: ESObject[]
) => {
};
addAfter
- 在指定的类方法执行后插入一个函数。最终返回值是插入函数执行后的返回值。
- 原方法执行后,执行插入的函数逻辑,返回插入函数的返回值。
适用场景:方法执行监控和统计、保存原数据。
/**
* 在指定的类对象的原方法执行后插入一个函数,最终返回值是插入函数执行后的返回值
* @param targetClass 指定的类对象
* @param methodName 指定的方法名,不支持read-only方法。
* @param isStatic 指定的原方法是否为静态方法。true表示静态方法,false表示实例方法。
* @param after 要插入的函数对象
* 如果:函数有参数,则第一个参数是this对象
* (若isStatic为true,则为类对象即targetClass;
* 若isStatic为false,则为调用方法的实例对象),
* 第二个参数是原方法的返回值(如果原方法没有返回值,则为undefined),
* 其余参数是原方法参数,函数也可以无参数。
*/
static addAfter(
targetClass: Object,
methodName: string,
isStatic: boolean,
after: Function
): void;
/**
* after函数
* @param instance isStatic为true = targetClass
* isStatic为false = Object
* @param returnValue 原方法的返回值,如果没有则为undefined
* @param ...args 插入方法的参数
*/
after: (
instance: Object,
returnValue: any,
...args: ESObject[]
) => {
};
replace
- 将指定的类的方法的替换为另一个函数。
- 调用类的方法执行时,只会执行替换后的函数逻辑。最终返回值为替换的函数执行完毕的返回值。
适用场景:方法逻辑替换。
/**
* 替换指定类的方法,替换后,原方法将不再执行,将执行替换后的函数,并且返回替换后的方法返回值
* @param targetClass 指定的类对象
* @param methodName 指定的方法名,不支持read-only方法。
* @param isStatic 指定的原方法是否为静态方法。true表示静态方法,false表示实例方法。
* @param instead 替换原方法的函数
* 如果:函数有参数,则第一个参数是this对象
* (若isStatic为true,则为类对象即targetClass;
* 若isStatic为false,则为调用方法的实例对象),
* 其余参数是原方法参数,函数也可以无参数。
*/
static replace(
targetClass: Object,
methodName: string,
isStatic: boolean,
instead: Function
): void;
/**
* instead函数
* @param instance isStatic为true = targetClass
* isStatic为false = Object
* @param ...args 替换方法的参数
*/
instead: (
instance: Object,
...args: ESObject[]
) => {
};
4.使用场景
场景1:方法参数校验
场景2:统计方法执行次数、时间
场景3:校验方法返回值
场景4:替换方法实现
5.使用注意事项
- 目标类需要导入,没有导出的场景,可以通过实例的constructor属性获取目标类。
- 目标方法名不能被混淆。
- 对父类作为目标类插桩会影响所有子类;对子类作为目标类插桩不会影响父类(无论方法是否是继承自父类的),但是会影响子类的所有子类。
- 接口的第四个参数是回调函数,回调函数中第一个参数是执行方法调用的this对象。如果通过这个调用原方法,并且没有退出机制,容易造成无限递归调用。如果需要调用原方法,需要在接口调用前将原方法存储起来。不推荐的用法参考如下示例。
- 错误示例
class Test { foo() {} } util.Aspect.addBefore(Test, 'foo', false, (instance: Test) => { // 无限递归 instance.foo(); }); new Test().foo();
- 正确示例
class Test { foo() {} } // 将原方法实现先保存起来 let originalFoo = new Test().foo; util.Aspect.addBefore(Test, 'foo', false, (instance: Test) => { // 如果原方法没有使用this,则可以直接调用原方法originalFoo(); // 如果原方法中使用了this,应该使用bind绑定instance,但是会有编译warningoriginalFoo.bind(instance); });
- 不推荐对struct的方法插桩/替换实现。
@Component struct Index { foo(){} build(){}; } util.Aspect.addBefore(Index, 'foo', false, ...); util.Aspect.replace(Index, 'build', false, ...);
- 接口不限制对系统提供的类方法进行插桩。只要类和方法在运行时是实际存在的对象,并且方法的属性描述符的writable字段为true,就可以使用对应接口进行插桩和替换。
说明
如果类方法的属性描述符的writable字段为false,比如冻结(freeze) 的场景, 则不能调用接口操作这个类方法。
方法的属性描述符的writable字段默认为true。
- 使用Aspect类接口进行插桩,对AoT和JIT编译后的性能没有明显影响。