当前位置: 首页 > news >正文

鸿蒙架构之AOP

零、主要内容

  • AOP 简介
  • ArkTs AOP 实现原理
    • JS 原型链
    • AOP实现原理
  • AOP的应用场景
    • 统计类: 方法调用次数统计、方法时长统计
    • 防御式编程:参数校验
    • 代理模式实现
  • AOP的注意事项

一、AOP简介

对于Android、Java Web 开发者来说, AOP编程思想并不陌生。 AOP的使用核心在于要找到 Aspect(切面),然后再根据自己的需要,对某个“业务操作进”行 前置或者后置的处理,甚至可以替换“该业务操作”。 AOP的操作粒度就是方法级别, 一个方法包括 接收数据、处理数据和返回数据这么三个部分:
在这里插入图片描述
AOP 在这三个阶段都可以添加自己的逻辑处理。 Java中常见的AOP框架有很多:AspectJ、SpringAOP、Javassist、Guice、Byte Buddy等。ArkTs在4.0版本中也支持了AOP,那么ArkTs是如何实现AOP的呢?

二、ArkTs AOP 实现原理

接下来,我们首先要了解一下JS对象的在继承体系中的引用关系,这样才能够精准的选择合适的方法来进行切面编程。 然后我们在了解一下AOP是如何实现的。

2.1 JS 原型链

在这里插入图片描述
如上图所示:
水平维度:类通过prototype 引用着其原型对象, 通过constructor引种着其构造函数; 该类的构造函数中,关联着该类的静态方法;
竖直维度:类的原型对象通过__proto__指向父类原型对象;类的构造函数通过__proto__指向父类的构造函数;类的实例对象通过__proto__指向该类的原型对象;

那么对于实例对象a和对象b来说,其实例方法的定位如下图红色路径所示;对于类A和类B类说,其静态方法的的定位流程如下图蓝色路径所示:
在这里插入图片描述
通过上图,我们可以得出如下结论:
类的原型对象承载着该类对象的实例实例方法(非静态方法),并且通过__proto__ 指向父类的原型对象,通过constructor指向类(也就是类的构造函数,需要额外指出的是 类的静态方法存储在构造函数中)。 类(类的构造函数)通过__proto__指向父类(父类的构造构造函数)。

2.2 AOP实现原理

AOP的实现依赖于 插桩和替换来实现的, 其本质上将回调参数和原方法组合成一个新的函数,再用新的函数替换原方法,具体如下图所示:

“计算机科学中的所有问题都可以通过增加一个额外的间接层来解决”

在这里插入图片描述

2.2.1 AddBefore 原理的伪代码

// addBefore 的伪代码实现
static addBefore(targetClass, methodName , isStatic , before:Function) : void {// 根据是否静态方法,获取要插装的对象(是“类” ,还是“类的原型对象”)let target = isStatic ? targetClass : targetClass.prototype;// 根据方法名,获取原有的方法let origin = target[methodName];/*** 定义新的方法(包装一层),实现优先执行before的逻辑,然后执行原有方法origin,* 最后将返回结果给 外层调用者。*/let newFuncs = function(...args) {// 先执行before方法,再执行当前方法before(this,...args);return origin.bind(this)(...args);    }// 使用新函数生效target[methodName] = newFuncs;
}

2.2.2 AddAfter 原理的伪代码

// addAfter 的伪代码实现
static addAfter(targetClass, methodName , isStatic , after:Function) :void {let target = isStatic ? targetClass : target.protoType;let original = target[methodName];let newFuncs = function(...args) {let ret = origin.bind(this)(...args);return after(this,r,...args); }
}

2.2.3 Repalce 原理的伪代码

static replace(targetClass, methodName , isStatic , instead) :void {let target = isStatic ? targetClass : target.protoType;let newFuncs = function(...args) {return instead(this,...args); }target[methodName] = newFuncs;
}

三、AOP的应用场景

  • 统计类: 方法调用次数统计、方法时长统计
  • 防御式编程:参数校验、返回值校验
  • 继承体系中的精确Hook
  • 代理模式和IOC

3.1 统计类

3.1.1 方法调用次数统计

export class Test {hello () {console.log('hello world')    }
}

我们通过Aspect.addBefore实现对Test类 hello方法调用次数的统计。

function main() {let countHello = 0;util.Aspect.addBefore(Test,'hello',false , ()=> {countHello++;});let h = new Test();console.log(`countHello : ${countHello}`)h.hello();console.log(`countHello : ${countHello}`)
}

3.1.2 方法时长统计

function addTimePrinter(target:Object, methodName:string, isStatic:boolean) {let t1 = 0;let t2 = 0;util.Aspect.addBefore(targetClass, methodName, isStatic, () => {t1 = new Date().getTime();});util.Aspect.addAfter(targetClass, methodName, isStatic, () => {t2 = new Date().getTime();console.log("t2---t1 = " + (t2 - t1).toString());});
}

测试addTimePrinter的功能:

export class View {onDraw() {// ...             }static cinit() {// ... }
}function main() {// 测试静态方法的时长统计addTimePrinter(Test,'cinit',true);View.cinit();// 测试实例方法的时长统计addTimePrinter(Test,'onDraw',true);new View().cinit();
}

3.2 防御式编程

  • 校验参数
  • 纠正返回值

3.2.1 校验参数

export class P004_View {children:P004_View[];constructor(children:Array<P004_View>) {this.children = children}getViewByIndex(index:number):P004_View {return this.children[index];}
}

上述View类的实例方法 getViewByIndex 的入参是一个index, 为了避免索引越界情况,我们可以通过Aspect类addBefore,增加一层”参数校验“的逻辑。

util.Aspect.addBefore(P004_View,"getViewByIndex",false, (view:P004_View, index:number)=> {if(view.children) {throw Error('view.children is undefined !')}if(index <= 0) {throw Error('index can not be negative !')}if((view.children as P004_View[]).length <= index) {throw Error('index is too big !')}
})

3.2.2 纠正返回值

export class P004_Random {        static randomSmallerThan50():number {return Math.floor(Math.random() * 52);}
}

randomSmallerThan50 方法的返回值期望是[0,50], 但是目前返回之返回是[0,51] , 我们可以使用Aspect类的addAfter方法,对返回值进行修正

export function testRandom() {util.Aspect.addAfter(P004_Random,'randomSmallerThan50',true,(target:P004_Random,ret:number)=> {if(ret > 50) {return P004_Random.randomSmallerThan50()} else {console.log(`P004_Random_randomSmallerThan50_addAfter ${ret}`)return ret;}})P004_Random.randomSmallerThan50()
}

3.3 子类实例方法替换

export class AirCraft {fly() {console.log('fight....')}
}export class USA_AirCraft extends AirCraft{}export class CN_AirCraft extends AirCraft{}

我们也可以通过Aspect类实现对子类的某个方法的 插桩或者替换。 下面是替换USA_AirCraft类的fly方法的代码:

export function testAirCraft() {let cn = new CN_AirCraft()let usa = new USA_AirCraft();cn.fly()usa.fly()util.Aspect.replace(USA_AirCraft,"fly",false,()=> {console.log('runaway....')})cn.fly()usa.fly();
}

3.4 控制反转(IOC)

AOP 也可以实现 控制反转。 如下图所示, PlayerManager 封装了播放器IPlayer接口,IPlayer 有ijkPlayer和mediaPlayer两个子类。 我们可以通过AOP 替换PlayerManager中的init() start() 等方法,来实现 两种Player对象的切换 。
在这里插入图片描述
上图中UML中的类,对应代码如下:

interface IPlayer {init(): voidstart(): voidstop(): voidrelease(): void
}export class PlayManager {player?: IPlayerinit(): void {}start(): void {}stop(): void {}release(): void {}
}export class IjkPlayer implements IPlayer {init(): void {console.log('IjkPlayer init ...')}start(): void {console.log('IjkPlayer start ...')}stop(): void {console.log('IjkPlayer stop ...')}release(): void {console.log('IjkPlayer release ...')}
}export class MediaPlayer implements IPlayer {init(): void {console.log('MediaPlayer init ...')}start(): void {console.log('MediaPlayer start ...')}stop(): void {console.log('MediaPlayer stop ...')}release(): void {console.log('MediaPlayer release ...')}
}

接下来,我们通过Aspect的replace方法来实现 player对象的替换:

/*
* 该方法 根据methodName,返回一个函数。该函数中会 当前player的对应的方法,并返回。 
*/
export function providePlayer(methodName: string, playerFetcher: ()=>IPlayer) {return (manager: PlayManager) => {if (methodName === 'init') {return playerFetcher().start()} else if (methodName === 'init') {return playerFetcher().start()} else if (methodName === 'start') {return playerFetcher().start()} else if (methodName === 'stop') {return playerFetcher().start()} else if (methodName === 'release') {return playerFetcher().release()}}
}export function testPlayer() {let player:IPlayer = new IjkPlayer()// 通过replace, 替换对应的方法。util.Aspect.replace(PlayManager, "init", false, providePlayer("init",()=> player))util.Aspect.replace(PlayManager, "start", false, providePlayer("start",()=> player))util.Aspect.replace(PlayManager, "stop", false, providePlayer("stop",() => player))util.Aspect.replace(PlayManager, "release", false, providePlayer("release",()=> player))let playManager = new PlayManager()playManager.init()// 替换成MediaPlayerplayer = new MediaPlayer()playManager.start()
}

四、AOP注意事项

1.插桩的目标类通常需要导入进来,对于没有导出的场景,如果有实例,可以通过实例的constructor属性获取目标类。(这里告诉我们导入的类是一个类对象)

 // 类实例对象的constructor ,指向类对象。 util.Aspect.addBefore(this.context.constructor, 'startAbility', false,(instance: Object, wantParam: Want) => {console.info('UIAbilityContext startAbility: want.bundleName is ' + wantParam.bundleName);});

2.需要明确插桩的影响范围(可以根据JS原型链去理解)。
3. addBefore 注意事项:

util.Aspect.addBefore(Test, 'foo', false, (instance: Test) => { // 该函数的参数 第一个是一个对象,后续参数 则需要参考 源于函数声明....
});
// 如果想要调用原有的函数,可以使用一个变量进行传递:
let oringalFoo = new Test().foo;
util.Aspect.addBefore(Test, 'foo', false, (instance: Test) => { // 该函数的参数 第一个是一个对象,后续参数 则需要参考 原函数声明// 方式一:如果原方法没有使用this,则可以直接调用原方法oringalFoo();// 方式二:如果原方法中使用了this,应该使用bind绑定instance,但是会有编译warningoringalFoo.bind(instance);
});

4.addAfter 注意事项:

util.Aspect.addAfter(Test, 'foo', false, (instance: Test, ret: string) => { // 该函数的参数 第一个是一个对象,第二个参数是 原函数的返回值console.log('execute foo');return ret;  // 一定要将原方法的返回值 传递出去
});

5.struct 不能插桩和替换; 方法的属性为只读时,不可以插桩和替换; 构造函数也不能被插桩和替换;

五、参考链接

鸿蒙官网-应用切面编程设计
es6的class&继承,揭开静态属性的原理和calss的本质

在这里插入图片描述

相关文章:

  • WordPress 主题技巧:给文章页增加“谁来过”模块。
  • 处理uniapp刷新后,点击返回按钮跳转到登录页的问题
  • 手机数据恢复篇:如何从 Android 设备内恢复数据
  • 【leetcode】 字符串相乘(大数相乘、相加)
  • STM32的TIM1之PWM互补输出_死区时间和刹车配置
  • 内容安全(深度行为检测技术、IPS、AV、入侵检测方法)
  • arcgis怎么选取某个指定区域地方的数据,比如从全国乡镇数据选取长沙市乡镇数据
  • Blackbox AI:你的智能编程伙伴
  • SQL概述及其规则与规范
  • 【BUG】已解决:NOAUTH Authentication required
  • ctfshow-web入门-php特性(web127-web131)
  • VulnHub:CK00
  • Python编程工具PyCharm和Jupyter Notebook的使用差异
  • LeetCode-随机链表的复制
  • gin框架 POST 请求参数绑定 JSON数据ShouldBind 使用注意事项 - 结构体必须定义json标签
  • HashMap ConcurrentHashMap
  • Node.js 新计划:使用 V8 snapshot 将启动速度提升 8 倍
  • Phpstorm怎样批量删除空行?
  • SegmentFault 技术周刊 Vol.27 - Git 学习宝典:程序员走江湖必备
  • Sequelize 中文文档 v4 - Getting started - 入门
  • storm drpc实例
  • VirtualBox 安装过程中出现 Running VMs found 错误的解决过程
  • 前端技术周刊 2019-01-14:客户端存储
  • 实战:基于Spring Boot快速开发RESTful风格API接口
  • 通过几道题目学习二叉搜索树
  • 学习笔记DL002:AI、机器学习、表示学习、深度学习,第一次大衰退
  • 在Docker Swarm上部署Apache Storm:第1部分
  • ​虚拟化系列介绍(十)
  • ###项目技术发展史
  • #WEB前端(HTML属性)
  • ${factoryList }后面有空格不影响
  • (1)(1.11) SiK Radio v2(一)
  • (4)事件处理——(7)简单事件(Simple events)
  • (ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_READ_ONLY)讲解
  • (第三期)书生大模型实战营——InternVL(冷笑话大师)部署微调实践
  • (二十六)Java 数据结构
  • (附源码)基于SSM多源异构数据关联技术构建智能校园-计算机毕设 64366
  • (附源码)计算机毕业设计大学生兼职系统
  • (规划)24届春招和25届暑假实习路线准备规划
  • (十)DDRC架构组成、效率Efficiency及功能实现
  • (转)fock函数详解
  • (转)Linux NTP配置详解 (Network Time Protocol)
  • (转)大道至简,职场上做人做事做管理
  • ./include/caffe/util/cudnn.hpp: In function ‘const char* cudnnGetErrorString(cudnnStatus_t)’: ./incl
  • .gitignore不生效的解决方案
  • .NET Core SkiaSharp 替代 System.Drawing.Common 的一些用法
  • .NET gRPC 和RESTful简单对比
  • .NET IoC 容器(三)Autofac
  • .NET/C# 利用 Walterlv.WeakEvents 高性能地定义和使用弱事件
  • .NET/C#⾯试题汇总系列:集合、异常、泛型、LINQ、委托、EF!(完整版)
  • .skip() 和 .only() 的使用
  • :class的用法及应用
  • @Autowired多个相同类型bean装配问题
  • @KafkaListener注解详解(一)| 常用参数详解
  • @RequestParam,@RequestBody和@PathVariable 区别