一、 AspectPro Plugin是什么?
aspect-pro-plugin
是一款轻量级的鸿蒙编译时AOP 插件。
- 让你的应用3分钟支持鸿蒙编译时aop能力。
- 支持ets、ts、js 语法解析。
- 支持自定义配置规则 (参考aspectProPluginConfig.txt)。
- 支持replace自动导包。
- 丰富插桩demo示例 (函数耗时、函数替换、隐私函数调用检测、装饰器函数...)。
二、为什么要继续开发 AspectPro Plugin?
24年8月我们随着AspectPro 一起开源了aspect-pro-plugin
1.0 版本, 此版本在稳定性、兼容性、效率 以及能力上都有很大优化空间, 因此我们继续开发了V2版本。
##### PK | ##### AspectProV1 | ##### AspectProV2 |
---|---|---|
易用性 | 中,需要开发plugin | 高,无需开发plugin |
稳定性 | 低,基于正则 | 高,基于语义解析 |
兼容性 | 高,基于公开api | 中,基于非公开api |
效率 | 低,基于源码需还原 | 高,基于编译中间结果,无需还原 |
精准度 | 中,基于文件和正则 | 高,基于文件和语义 |
AST**** | 不支持,基于源代码 | 支持,基于sourcefile |
还没看过AspectPro的同学,可以先补下背景知识...
Github链接:github.com/HuolalaTech...
三、AspectPro Plugin前言篇
在方案实践前,我们充分调研了鸿蒙AOP 当前的的能力,进而梳理出影响方案选型的关键信息。
关键点:
- 鸿蒙暂未对外开放ArkTs对应的AST工具, 因此ArkTs部分语法无法直接解析。
- 修改CompileArkTs的build产物, 无法生效于方舟字节文件。
- Hvigor 开放的 api 有限,无法通过公开直接拿到ets_loader 处理后的内容。
- 基于transformLib直接修改方舟字节码,上手难度较大且暂无相关demo。
基于上述调研结论且根据已知的ArkTs兼容Ts 生态。

我们确定了方案实践思路:
思路:
- 基于Hvigor 自定义Plugin 在编译期插入 (参考plugin开发与发布)
- 使用TS Compile Api进行AST 解析 (辅助工具 TypeScript AST Viewer)
方案还存在一个难点需要攻坚:
攻坚点:
- Hvigor 自定义Plugin中如何获取 "符合TS语法规范的 ETS文件? "
四、AspectPro Plugin实践篇
整个方案实践围绕着一个攻坚点展开, 经历了多次探索,一度深陷其中,为了大家少走弯路,我们先看结论....
4.1 那些我们实践过的方案
##### 方案 | ##### 方案原理 | ##### 是否推荐 | ##### 备注 |
---|---|---|---|
强转ETS | 1.代码经常2次变动,基于代码diff & 相似度对比, 回滚中间的一次变动。![]() |
不推荐稳定性差 | 难精准处理![]() ![]() |
修改编译工具ets_loader | 1.ets_loader负责编译ArkTs源码![]() ![]() |
不推荐修改编译工具影响范围大 | 实现transform:![]() ![]() |
自定义compile plugin | 1.使用OhosModuleContextImpl非公开api在compile阶段插入自定义逻辑 (loadCompilePlugin)![]() ![]() |
当前推荐基于非公开api,影响范围小 | 此方案是和华为官方沟通后得出。需要说明:目前hvigor官方未对外提供 ast 解析能力,此方案非官方的。 |
~~推荐~~~~公开~~~~api~~~~~~ |
基于当前现状,我们决定采用自定义compile plugin 方案,
同时为了降低使用成本我们对compile plugin进一步封装,开放了 "aspect-pro-plugin": "2.0.0" ,
基于此plugin 只需3分钟即可让你的应用支持Aop能力。
...想快速看效果的,可以直接看第六节 : "AspectPro Plugin 使用篇"
4.2 Compile Plugin 方案实现
首先声明:
- 目前hvigor官方未对外提供ast解析能力,此方案虽然是和官方同学多次沟通后实现的,虽不是官方方案.但已是我们不断尝试后当前可落地的最佳实践。
方案原理:
利用Hvigor非公开api 在ets_loader -> ark compile 编译成abc 中间, 插入sourcefile的 compile plugin 。

方案实践步骤:
- step 1: 定义你的Hvigor Plugin 并在entry/hvigorfile.ts中应用
javascript
// 开发阶段 - 建议依赖local plugin(鸿蒙的一个module) 效率更高
import { aspectProPluginV2 } from '../local_plugin/src/main/AspectProPluginV2';
export default {
system: hapTasks,
plugins: [aspectProPluginV2('../local_plugin/src/main/compile/AspectCompilePlugin.ts')]
}
- step 2: AspectProPluginV2中通过OhosHapContext.loadCompilePlugin( AspectCompilePlugin)
javascript
/**
* @param yourPluginAbsPath 自定义编译插件文件相对路径(相对于HllHvigorPlugin)
* (本示例文件结构则传:'../local_plugin/src/main/compile/AspectCompilePlugin.ts')
*/
export function aspectProPluginV2(yourPluginAbsPath:string): HvigorPlugin {
return {
pluginId: 'aspectProPluginV2',
async apply(node: HvigorNode): Promise<void> {
hvigor.nodesEvaluated(async () => {
const hapContext = node.getContext(OhosPluginId.OHOS_HAP_PLUGIN) as OhosHapContext;
hapContext?.loadCompilePlugin(yourPluginAbsPath);
});
}
};
}
- step 3: 在AspectCompilePlugin中通过ets_loader中的this.share获取到ets转化为ts的sourcefile
ini
const sourceFiles = this.share.getSourceFiles();
- step 4: 获取ets_loader中的ts对象,按你的aop需求 利用ts compiler aip 修改sourcefile
csharp
//@ts-ignore
let ts = require(path.join(this.share.projectConfig.etsLoaderPath,
'node_modules', 'typescript'));
// 按需依次处理:此处演示多个AOP插桩逻辑 (可简单理解为:Android apply plugin)
updateSourcefile = SlowMethodAopImp.doTransform(ts, ModuleSourceFile.source, modulePath)
updateSourcefile = ReplaceMethodAopImp.doTransform(ts, updateSourcefile, modulePath)
// ...
- step 5: 将修改后的sourcefile赋值给this.share.sourcefile, 系统编译工具执行编译生成产物
ini
ModuleSourceFile.source = updateSourcefile;
以上是方案实现的核心流程,完整实现方案源码我们也开源,有兴趣的可以看第八节 : "相关链接"
五、AspectPro Plugin展示篇
基于我们这套方案可快速实现 Android gradle plugin + ASM 相关功能,比如:
场景一:函数执行耗时统计
伪代码:
javascript/** * AOP插入代码统计方法执行耗时 * step一: * 函数开头插入 const startTime = Date.now(); * step 二: * 函数结束插入 Logger.w(tag, `${methodId} took ${(Date.now() - startTime)} ms`) */ export class SlowMethodTransform { static doTransform(ts) { return (context) => { return (sourceFile) => { const visit = (node) => { if (!MethodTransformApi.supportsFunctions(node, ts)) { return ts.visitEachChild(node, visit, context); } if (MethodTransformApi.isSimpleOrEmptyMethod(node, ts)) { return ts.visitEachChild(node, visit, context); } let updatedNode = this.generateUpdatedMethod(node, ts, sourceFile); return ts.visitEachChild(updatedNode, visit, context); }; let updatedSourceFile = ts.visitNode(sourceFile, visit); return ImportTransformApi.addImportStatement(updatedSourceFile, ts, importName, importPath); }; }; }
效果:
场景二:函数调用替换
伪代码:
typescript/** * AOP Transform : 方法调用替换 * step 一: * 遍历找到函数调用, 比如router.pushUrl * step 二: * 将目标函数替换, 比如将router.pushUrl替换为 this.getUIContext().getRouter().pushUrl */ export class AspectProTransform { static doTransform ( ts, allReplaceRules: ReplaceRule[] ) { return ( context ) => { return ( sourceFile ) => { const importsToAdd = new Set <string>(); const visit = ( node ) => { if (!node) { return node; } let updatedNode = node; for ( let rule of allReplaceRules) { if (!rule) { continue ; } if ( this . isFunctionCallMatch (node, rule. pattern , ts)) { updatedNode = this . replaceFunctionCall (node, rule. pattern , rule. replacement , ts); if (rule. imports ) { for ( let imp of rule. imports ) { importsToAdd. add (imp); } } } } return ts. visitEachChild (updatedNode, visit, context); }; let updatedSourceFile = ts. visitNode (sourceFile, visit); for ( let imp of importsToAdd) { if (!imp || typeof imp !== 'string' || !imp. includes ( SPLIT_FLAG ) || imp. split ( SPLIT_FLAG ). length !== 2 ) { return updatedSourceFile; } const [importName, importPath] = imp. split ( SPLIT_FLAG ); updatedSourceFile = ImportTransformApi . addImportStatement (updatedSourceFile, ts, importName, importPath); } return updatedSourceFile; }; }; }
效果:
场景三:特定函数修改('装饰器')
伪代码:
javascript/** * AOP Transform : 修改"特定装饰器修饰"的方法 * step 一: * 遍历找到函数调用,找到"特定装饰器修饰" * step 二: * 修改函数 (本例子仅:增加日志) */ export class DescriptorMethodTransform { static doTransform(ts) { return (context) => { return (sourceFile) => { const visit = (node) => this.myVisitNode(ts, node, context); let updatedSourceFile = ts.visitNode(sourceFile, visit); return ImportTransformApi.addImportStatement(updatedSourceFile, ts, importName, importPath); }; }; } static myVisitNode(ts, node, context) { if (ts.isClassDeclaration(node) && this.hasDecorator(node, MY_CLASS_DESCRIPTOR, ts)) { // 遍历类的方法,查找是否有AUTO_CATCH_DESCRIPTOR装饰器的方法 const newMembers = ts.factory.createNodeArray(node.members.map(member => this.aopWhenMethodDeclarationMatch(ts, member))); return ts.factory.updateClassDeclaration( node, node.modifiers, node.name, node.typeParameters, node.heritageClauses, newMembers ); } return ts.visitEachChild(node, (childNode) => this.myVisitNode(ts, childNode, context), context); } static aopWhenMethodDeclarationMatch(ts, member) { if (ts.isMethodDeclaration(member)) { if (this.hasDecorator(member, AUTO_CATCH_DESCRIPTOR, ts)) { return this.addConsoleDebugStatement(ts, member); } } return member; } static hasDecorator(node, decoratorName, ts) { return node.modifiers && node.modifiers.some(modifier => { let expression = modifier.expression; if (ts.isCallExpression(modifier.expression)) { expression = modifier.expression.expression; } return ts.isIdentifier(expression) && expression.text === decoratorName; }); } }
效果:
场景四:隐私api监控
伪代码:
typescript/** * AOP Transform : 查找隐私Api调用 * step 一: * 遍历找到隐私函数调用, 比如hilog.fatal (此处仅演示 - 因为demo依赖的Logger中调用了此aip) * step 二: * 打印出函数堆栈信息 */ export class PrivacyMethodTransform { static doTransform(ts, allReplaceRules: ReplaceRule[]) { return (context) => { return (sourceFile) => { const visit = (node, curClassName, curMethodName) => { if (!node) { return node; } if (ts.isClassDeclaration(node) && node.name && node.name.getText) { curClassName = node.name.getText(); } else if (ts.isMethodDeclaration(node) && node.name && node.name.getText) { curMethodName = node.name.getText(); } for (let rule of allReplaceRules) { if (!rule) { continue; } if (this.isFunctionCallMatch(node, rule.pattern, ts)) { const { line } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart()); const className = curClassName || 'UnknownClass'; const methodName = curMethodName || 'UnknownMethod'; console.log(`PrivacyMethodTransform-> [Found privacy API <${rule.pattern}>call in ${className}.${methodName} at line:${line + 1}]`); } } return ts.visitEachChild(node, (childNode) => visit(childNode, curClassName, curMethodName), context); }; return ts.visitNode(sourceFile, (node) => visit(node, undefined, undefined)); ; }; }; } static isFunctionCallMatch(node, pattern: string, ts): boolean { if (!node || !pattern || typeof pattern !== 'string' || !pattern.includes(SPLIT_FLAG)) { return false; } const patternParts = pattern.split(SPLIT_FLAG); if (patternParts.length < 2) { return false; } try { if (ts.isCallExpression(node)) { if (!node.expression) { return false; } const expr = node.expression; if (ts.isPropertyAccessExpression(expr) && expr.expression && ts.isIdentifier(expr.expression) && expr.name && ts.isIdentifier(expr.name)) { const isMatch = expr.expression.getText() === patternParts[0] && expr.name.getText() === patternParts[1]; return isMatch; } } } catch (e) { return false; } return false; } }
效果:
相关配置说明
ruby
# 配置规则说明 - plugin 按行读取配置文件 (默认读取hvigor-file同级目录 所有.js 、.ts 、 .ets文件)
# -hook path | file : 配置需要hook 处理的文件目录 | 文件
# -keep path | file : 配置需要额外 keep 的目录 | 文件 (非必需, 当-hook 的文件目录中有一些特殊文件,不需要处理时,配合使用)
# -target pattern replacement [import xxx';import xxx] : 配置替换的原始函数和对目标函数 [import aaa;import bbb] 同时需要新导入的依赖
# 比如:
#-hook ./src/main/ets/test/TestClass1.ts
-hook ./src/main/ets/pages/Index.ets
#-hook ./src/main/ets/entryability/EntryAbility.ets
-keep ./src/main/ets/hook/
#-target router:pushUrl this.getUIContext().getRouter():pushUrl
-target router:pushUrl this.getUIContext().getRouter():pushUrl [-import Logger:@huolala/logger/src/main/com.wp/Logger]
# 支持三方库代码替换 (需要注意三方库的路径)
#-hook ../oh_modules/.ohpm/@[email protected]/oh_modules/@huolala/logger/src/main/com.wp/Logger.ts
#-target Logger:i Logger:e
六、AspectPro Plugin 使用篇
只需三步让你的鸿蒙应用支持 Aop能力
第一步:
javascript
1.添加并使用aspect-pro-plugin插件
1.1 在工程hvigor-package.json文件中添加
"dependencies": {
"aspect-pro-plugin": "2.0.0"
}
1.2 在entry或其他模块的 hvigorfile.ts文件中使用
import { aspectProPluginV2 } from 'aspect-pro-plugin';
export default {
system: hapTasks,
plugins: [aspectProPluginV2(require.resolve('aspect-pro-plugin'))]
}
第二步:
bash
2.在工程目录下创建aop/aopConfig.json文件, 配置你的aop实现类的绝对路径
{
"aopConfigs": [
{
"name": "YourSlowMethodAop",
"path": "/Users/xxx/HarmonyOs/openSource/AspectPro/entry/src/main/yourAop/YourSlowMethodAop.ts"
},
{
"name": "YourEmptyAop",
"path": "./src/main/yourAop/YourSlowMethodAop.ts"
}
]
}
第三步:(如果之前未使用过Ts Compile Api - 可以先参考源码快速尝鲜,再看第7节 扩展篇,一步步实现)
typescript
3.在你的YourSlowMethodAop.ts这个实现 doTransform()方法实现你的aop插桩逻辑并导出
export class YourSlowMethodAop {
/**
* 3.1 此方法必须按下述格式实现
* @param ts 鸿蒙ets_loader中的ts对象
* @param sourcefile 鸿蒙ets_loader处理后的 ts sourcefile对象
* @param modulePath 当前工程路径
* @returns sourcefile
*/
static doTransform(ts, sourcefile, modulePath: string) {
try {
// TOOD 实现你的插桩逻辑
if (sourcefile.fileName.includes("EntryAbility.ets")) {
console.log(`YourSlowMethodAop -> doTransform() ----> 开始处理目标文件:${sourcefile.fileName}`);
let result = ts.transform(sourcefile, [YourSlowMethodTransform.doTransform(ts)]);
return result.transformed[0];
}
} catch (e) {
console.log(`YourSlowMethodAop -> doTransform() exp:${e} ,sourcefile:${sourcefile.fileName}`);
}
return sourcefile;
}
}
// 3.2 必须导出, plugin 内部使用require方式import
//@ts-ignore
module.exports = YourSlowMethodAop;
是不是很简单:

至此,你的应用就有了 AOP能力...

七、Transform 扩展篇
考虑到部分同学可能对Ts Compile Api 比较陌生, 因此接下来一步步介绍使用步骤,方便大家快速上手
Step 1 : transform流程介绍
Transform流程: AST(Sourcefile) -> Ts Compile Api <visit、update、transform> -> updateSourceFile
AopTransform步骤 | 关键 API | 作用 |
---|---|---|
1.创建 Program | ts.createProgram() | 管理编译上下文和依赖 |
2.获取 AST | program.getSourceFile() | 获取文件对应的 AST |
3.遍历 AST | ts.visitEachChild() | 递归访问每个节点 |
4.修改节点 | ts.factory.updateXxx() | 修改节点(增、删、改) |
5.执行变换 | ts.transform() 源码 | 应用变换生成新 AST |
6.获取新AST | transformResult.transformed[0] | 获取变化后的 AST |
Step 2 : transform难点分析
上述流程只有第4点 是依赖具体Aop逻辑 "动态"变化的, 其他都是标准操作, 因此关键是掌握修改节点。
咱们都熟悉 TS 语法,所以可以很熟练的进行日常开发: 编写源码(ts) -> 编译器经过<扫描器、Token流、解析器> 生成 -> AST(Sourcefile)
而直接修改AST这种"逆向编码 "的语法并不熟悉且不同场景Aop逻辑又是 "动态"变化的 ,因为上手难 。
解决方案:
- 使用TypeScript AST Viewer |AST Explorer等工具查看源码的 "AST结构" & 生成AST代码。
- 使用封装库 ts-morph(ts simple ast), 对ts compile Api进行封装,提供了更方便的api。
Step 3 : YourSlowMethodTransform.doTransform实现
scala// 1.ts.transform export function transform<T extends Node>(source: T | T[], transformers: TransformerFactory<T>[], compilerOptions?: CompilerOptions): TransformationResult<T> {} // 2.TransformationResult export interface TransformationResult<T extends Node> // 3.TransformerFactory export type TransformerFactory<T extends Node> = (context: TransformationContext) => Transformer<T>; export type Transformer<T extends Node> = (node: T) => T;
3.1 看了上面源码大家应该可以理解, ****doTransform函数返回一个 TransformerFactory类型的函数即可。
typescriptstatic doTransform(ts, sourcefile, modulePath: string) { return (context: ts.TransformationContext) => { return (sourceFile: ts.SourceFile) => { // 实际转换逻辑 return sourceFile; }; };
3.2 实际转换逻辑分2步 遍历和修改 , 可参考TS源码中的 customTransforms.ts
- 递归遍历每个node节点
- 判断节点类型SyntaxKind,如果是你的目标类型,则进行下一步修改操作
javascriptconst before: ts.TransformerFactory<ts.SourceFile> = context => { return file => ts.visitEachChild(file, visit, context); function visit(node: ts.Node): ts.VisitResult<ts.Node> { switch (node.kind) { case ts.SyntaxKind.FunctionDeclaration: return visitFunction(node as ts.FunctionDeclaration); default: return ts.visitEachChild(node, visit, context); } } function visitFunction(node: ts.FunctionDeclaration) { ts. addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, "@before", true); return node; } };
3.3 具体怎么修改了? 比如ts.addSyntheticLeadingComment 我怎么知道调哪些api实现修改?
利用 TypeScript ****AST Viewer 生成需要插入代码的AST语句
3.4 首先:以最简单的代码插入 & 修改举例
scssonForeground(): void { Logger.d(TAG, "1.onForeground->a() method invoked"); }
- 我们先实现插入const startTime = Date.now();
inifunction visit(node) { if (ts.isMethodDeclaration(node) && node.name && node.name.text === 'onForeground') { // 1.直接copy TypeScript AST Viewer 生成的代码 const timeStampStatement = insertTimeStamp(ts, "startTime"); // 2.将生成的代码插入到函数体中 const newStatements = ts.factory.createNodeArray([timeStampStatement, ...node.body!.statements]); const newBody = ts.factory.updateBlock(node.body, newStatements); // 3.最后调用ts.updateXXX 更新即可 return ts.factory.updateMethodDeclaration( node, node.decorators, node.modifiers, node.asteriskToken, node.name, node.questionToken, node.typeParameters, node.parameters, node.type, newBody ); } return ts.visitEachChild(node, visit, context); }
- 进一步 :先实现方法修改Logger.d修改为Logger.w
inifunction visit(node) { if (ts.isMethodDeclaration(node) && node.name && node.name.text === 'onForeground') { // 1.直接copy TypeScript AST Viewer 生成的代码 const timeStampStatement = insertTimeStamp(ts, "startTime"); // 2.修改 Logger.d 为 Logger.w const updateStatements = updateLoggerD2W(node, ts); // 3.将生成的代码插入到函数体中 const newStatements = ts.factory.createNodeArray([timeStampStatement, ...updateStatements]); const newBody = ts.factory.updateBlock(node.body, newStatements); // 4.最后调用ts.updateXXX 更新即可 return ts.factory.updateMethodDeclaration( node, node.decorators, node.modifiers, node.asteriskToken, node.name, node.questionToken, node.typeParameters, node.parameters, node.type, newBody ); } return ts.visitEachChild(node, visit, context); } function updateLoggerD2W(node, ts) { return node.body.statements.map(statement => { if (ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression)) { const callExpr = statement.expression; if (ts.isPropertyAccessExpression(callExpr.expression) && callExpr.expression.name.text === 'd' && callExpr.expression.expression.getText() === 'Logger') { const newPropertyAccess = ts.factory.updatePropertyAccessExpression(callExpr.expression, callExpr.expression.expression, ts.factory.createIdentifier('w')); const newCallExpr = ts.factory.updateCallExpression(callExpr, newPropertyAccess, callExpr.typeArguments, callExpr.arguments); return ts.factory.updateExpressionStatement(statement, newCallExpr); } } return statement; }); }
- 再进一步 :把函数内容全替换成自定义的 AppStorage.setOrCreate('IsForeground', true);
markdown1. 还是去 TypeScript AST Viewer中 copy 要替换代码的AST语句 2. 定位到函数后,直接用新的AST statement 替换之前的, 然后向外依次updateBolck、updateMethod、update node. 3. 
dartfunction visit(node) { if (ts.isMethodDeclaration(node) && node.name && node.name.text === 'onForeground') { // 1.直接copy TypeScript AST Viewer 生成的代码 const newStatements = replaceAllStatements(ts); // 2.更新函数体 const newBody = ts.factory.createBlock([newStatements], true); // 3.更新node return ts.factory.updateMethodDeclaration( node, node.decorators, node.modifiers, node.asteriskToken, node.name, node.questionToken, node.typeParameters, node.parameters, node.type, newBody ); } return ts.visitEachChild(node, visit, context); } function replaceAllStatements(ts) { return ts.factory.createExpressionStatement( ts.factory.createCallExpression( ts.factory.createPropertyAccessExpression( ts.factory.createIdentifier('AppStorage'), ts.factory.createIdentifier('setOrCreate') ), undefined, [ ts.factory.createStringLiteral('IsForeground'), ts.factory.createTrue() ] ) ); }
OK,经过上述步骤相信大家都可以开始一步步实现自己的Aop,我们简单总结下步骤:
- 利用 TypeScript ****AST Viewer 查看源码结构 & 生成目标源码对应的AST代码块.
- 在自定义的Visit函数中遍历 node ,根据第一步源码结构找到目标node.
- Copy 第一步生成的 目标源码对应的AST代码块.
- 如果是****更新操作: 调ts.factory.updateXXX, 从内到外依次更新 ,最后返回更新后的 node .
- 如果是****替换操作: 调ts.factory.createXXX, 从内到外依次更新 ,最后返回替换后的 node .
... 如果你熟悉其他的ts ast 库(比如 ts-morph) 也可以安装相关依赖后使用.