日志埋点、参数校验、耗时统计……这些和业务无关却又散落各处的代码,正在悄悄拖垮你的项目可维护性。HarmonyOS 提供的 util.Aspect 类,用三个接口就能把这些"横切关注点"从业务代码里干净地剥离出来。
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 起可在元服务中使用)。
import
{ util }
from
'@kit.ArkTS'
;util.Aspect 一共提供三个静态接口,对应三种插桩方式:
| 接口 | 作用 | | --- | --- | | addBefore() | 在原方法执行 前 插入自定义函数 | | addAfter() | 在原方法执行 后 插入自定义函数,可拿到原方法返回值 | | replace() | 直接 替换 原方法的实现 |
static addBefore ( targetClass : Object, methodName : string , isStatic : boolean , before : Function): void
| 参数 | 说明 | | --- | --- | | targetClass | 目标类对象(注意是类本身,不是实例) | | methodName | 原方法名, 不支持 read-only 方法 | | isStatic | 原方法是否为静态方法, true 静态 / false 实例方法 | | before | 要插入的函数。第一个参数是 this 对象(实例方法为实例、静态方法为类对象),其余参数是原方法的参数 |
看一个完整示例,同时覆盖实例方法和静态方法两种场景:
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
;
}
}
util.
Aspect
.
addBefore
(
MyClass
,
'foo'
,
false
,
(
instance: MyClass, arg:
string
) =>
{
console
.
info
(
'[Before] arg is '
+ arg);
instance.
msg
=
'msg111'
;
});
util.
Aspect
.
addBefore
(
MyClass
,
'bar'
,
true
,
(
target:
Object
, arg:
string
) =>
{
console
.
info
(
'[Before] arg is '
+ arg);
Reflect
.
set
(target,
'data'
,
'data111'
);
});
let
asp =
new
MyClass
();
let
result = asp.
foo
(
'123'
);
console
.
info
(
'result is '
+ result);
let
res =
MyClass
.
bar
(
'456'
);
console
.
info
(
'res is '
+ res);插入函数先于原方法执行 ,所以在 addBefore 里修改成员变量,会直接影响原方法读到的值。
静态方法插桩时拿到的是类对象,修改静态成员需要用 Reflect.set() 。
static addAfter ( targetClass : Object, methodName : string , isStatic : boolean , after : Function): void
参数和 addBefore 基本一致,区别在于插入函数 after 的参数列表: 第二个参数是原方法的返回值 (原方法无返回值时为 undefined ),之后的参数才是原方法的参数。
⚠️ 最大的坑在这里: addAfter 插桩后,方法的最终返回值 = 插入函数的返回值。 如果你只是想做些记录工作,一定要记得把原返回值透传回去,否则调用方会拿到 undefined 。
import
{ util }
from
'@kit.ArkTS'
;
import
{
WebHandler
}
from
'./someThirdParty'
;
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
ret;
});
new
WebHandler
().
getWebAddrHttps
();static replace ( targetClass : Object, methodName : string , isStatic : boolean , instead : Function): void
replace 更激进——原方法不会执行,直接由 instead 函数取而代之。适合测试桩、功能降级、Hook 第三方库行为等场景:
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
());性能监控是 AOP 最经典的应用。 addBefore + addAfter 组合使用,给任意方法加上耗时统计,业务代码零改动:
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 就够:
let
countFoo =
0
;
util.
Aspect
.
addBefore
(
Test
,
'foo'
,
false
,
() =>
{
countFoo++;
});
new
Test
().
foo
();
console
.
info
(
'countFoo = '
+ countFoo);addBefore 回调的第一个参数是 this 对象,可以直接读取实例的成员变量做防御性校验,异常时直接抛错阻断方法执行:
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
();校验逻辑集中在一处,业务类里完全看不到防御代码的影子。
想监控 window.createWindow() 、 rcp.createSession() 这类系统接口的调用?直接插桩会失败—— util.Aspect 只能作用于 ArkTS 层的类或对象方法,无法作用于 native 层实现的系统模块 。
官方的解法是:先封装一层 ArkTS 包装类(代理),再对包装类插桩:
import
{
window
}
from
'@kit.ArkUI'
;
import
{ util }
from
'@kit.ArkTS'
;
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,
() =>
{
});
}
}
util.
Aspect
.
addBefore
(
WindowWrap
,
'createWindow'
,
false
, (
instance
:
WindowWrap
):
void
=>
{
console
.
info
(
'[Aspect] createWindow 被调用'
);
});
WindowWrap
.
getInstance
().
createWindow
();同理,如果想在应用拉起时记录目标包名、监控 rcp 网络会话创建,都可以套用这个"代理 + 插桩"模式。
isStatic 传错是最常见错误。 实例方法传 false 、静态方法传 true ,传反了插桩不生效或行为异常。
addAfter 必须 return 原返回值。 插入函数的返回值会覆盖原方法的返回值,忘记 return ret 会让调用方拿到 undefined ,这类 bug 排查起来很隐蔽。
不支持 read-only 方法。 对只读方法插桩不会有效果。
重复插桩的执行顺序:后插桩的先执行。 对同一个方法连续调用多次 addBefore ,后插入的函数会先执行(类似栈结构): util. Aspect . addBefore ( Test , 'printData' , true , () => console . info ( 'before 1' )); util. Aspect . addBefore ( Test , 'printData' , true , () => console . info ( 'before 2' )); Test . printData ();
无法插桩 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?
华为官方文档:@ohos.util(util 工具函数)- Aspect
华为最佳实践:应用切面编程设计
华为 FAQ:ArkTS 中如何使用切面编程
华为示例:加载时间及次数统计展示
转载声明:本文为转载整理,原文版权归原作者及原站点所有。
原文来源:Juejin HarmonyOS
原文作者:Keya;如有侵权,请联系管理员删除。