鸿蒙架构之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(): void

  start(): void

  stop(): void

  release(): void
}

export class PlayManager {
  player?: IPlayer

  init(): 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()
  // 替换成MediaPlayer
  player = 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,但是会有编译warning
  oringalFoo.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的本质

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/799142.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

双时隙对讲模块:高效、低成本的数字对讲解决方案

商业对讲中&#xff0c;对讲机是一种提高工作效率的通讯工具&#xff0c;大大节省了工作时间和成本&#xff0c;使部门内和部门间的通讯更加便捷。市场行业需求&#xff0c;无线对讲机行业慢慢从模拟转向数字模式&#xff0c;在信息安全和频点利用上取得了重大提升。这为行业用…

为什么自养号测评推广是未来跨境电商的趋势,尤其是亚马逊

在当今跨境电商的激烈竞争中&#xff0c;测评补单的现象非但没有因平台的禁令而消退&#xff0c;反而愈发成为行业内的普遍现象&#xff0c;其背后的驱动力值得深入探讨。以下是对这一现象背后五大原因的优化阐述&#xff1a; 一、市场饱和与竞争加剧 随着跨境电商市场的日益…

Kafka:Kafka详解

Kafka 消息中间件 区别于rabbitmq,kafka更适用于量级较大的数据(100w级),主要在大数据领域使用 Kafka介绍 一个分布式流媒体平台,类似于消息队列或企业消息传递系统 Kafak的结构如下 producer:发布消息的对象 topic:Kafak将消息分门别类,每类的消息称为一个主题(Topic) …

mupdf 编译说明

进入官网下载源码&#xff1a;https://www.mupdf.com/releases 挑选需要的版本&#xff0c;下载解压&#xff0c;然后打开解决方案&#xff0c;进行编译

MySQL 数据库 - 事务

MySQL 数据库&#xff08;基础&#xff09;- 事务 事务简介 事务 是一组操作集合&#xff0c;他是一个不可分割的工作单位&#xff0c;事务会把所有的操作看作是一个整体一起向系统发送请求&#xff0c;即这些操作要么同时成功&#xff0c;要么同时失败。 比如&#xff1a;张…

tomcat的优化、动静分离

tomcat的优化 tomcat自身的优化 tomcat的并发处理能力不强&#xff0c;大项目不适应tomcat做为转发动态的中间件&#xff08;k8s集群&#xff0c;pytnon rubby&#xff09;&#xff0c;小项目会使用&#xff08;内部使用的&#xff09;动静分离 默认配置不适合生产环境&…

在 PostgreSQL 里如何处理数据的存储优化和查询优化的冲突?

&#x1f345;关注博主&#x1f397;️ 带你畅游技术世界&#xff0c;不错过每一次成长机会&#xff01;&#x1f4da;领书&#xff1a;PostgreSQL 入门到精通.pdf 文章目录 在 PostgreSQL 里如何处理数据的存储优化和查询优化的冲突一、存储优化与查询优化的概述&#xff08;一…

前端实现一键复制功能

1、下载插件 npm i vue-clipboard32.0.0 2、在需要复制的文件中引入插件并使用&#xff1a; JS: import useClipboard from "vue-clipboard3"; const { toClipboard } useClipboard(); HTML: <el-tooltip content"复制内容" placement"top&…

Java 客户端操作 Redis 命令(端口号映射方法,命令演示,注意事项)

文章目录 开放端口号问题引入依赖验证连接通用命令使用set 和 get 命令的使用exists 和 del 命令的使用keys 命令的使用expire 和 ttl 命令type 命令的使用 String 类型命令使用mset 和 mget 命令getrange 和 setrange 命令append 命令incr 和 decr 命令 list 类型命令使用lpus…

05 以物品与用户为基础个性化推荐算法的四大策略

《易经》&#xff1a;“九二&#xff1a;见龙在田&#xff0c;利见大人”。九二是指阳爻在卦中处于第二位&#xff0c;见龙指龙出现在地面上&#xff0c;开始崭露头角&#xff0c;但是仍须努力&#xff0c;应处于安于偏下的位置。 本节是模块二第一节&#xff0c;模块二讲解传…

【学习笔记】无人机(UAV)在3GPP系统中的增强支持(六)-人工智能控制的自主无人机用例

引言 本文是3GPP TR 22.829 V17.1.0技术报告&#xff0c;专注于无人机&#xff08;UAV&#xff09;在3GPP系统中的增强支持。文章提出了多个无人机应用场景&#xff0c;分析了相应的能力要求&#xff0c;并建议了新的服务级别要求和关键性能指标&#xff08;KPIs&#xff09;。…

开源浏览器引擎对比与适用场景:WebKit、Chrome、Gecko

WebKit与Chrome的Blink引擎对比 起源与关系&#xff1a; WebKit最初由苹果公司开发&#xff0c;用于Safari浏览器。后来&#xff0c;WebKit逐渐成为一个独立的开源项目&#xff0c;被多个浏览器厂商采用。Blink是Google基于WebKit项目分支出来的一个浏览器引擎&#xff0c;用于…

自主升级,平稳过渡!麒麟信安保障长沙市智慧交通发展中心CentOS迁移无忧

长沙市智慧交通发展中心围绕综合交通运输协调体系的构建&#xff0c;实施交通运行的监测、预测和预警&#xff0c;面向公众提供交通信息服务&#xff0c;开展多种运输方式的调度协调&#xff0c;提供交通行政管理和应急处置的信息保障。 该中心目前数据日交换量超2亿条&#x…

替换:show-overflow-tooltip=“true“ ,使用插槽tooltip,达到内容可复制

原生的show-overflow-tooltip“true” 不能满足条件&#xff0c;使用插槽自定义编辑&#xff1b; 旧code <el-table-column prop"reason" label"原因" align"center" :show-overflow-tooltip"true" /> <el-table-column pro…

Adminer-CVE-2021-21311

在其4.0.0到4.7.9版本之间&#xff0c;连接 ElasticSearch 和 ClickHouse 数据库时存在一处服务端请求伪造漏洞&#xff08;SSRF&#xff09;。 VPS开启HTTP服务 VPS 开启HTTP 再同时跑POC 确保能访问poc里的链接文件 第一是目标地址 第二个是跳转地址 第三个是监听地址 如果…

【C++】 List 基本使用

C List 基本使用 基本概念 list 是一个序列容器&#xff0c;它内部维护了一个双向链表结构。与 vector 或 deque 等基于数组的容器不同&#xff0c;list 在插入和删除元素时不需要移动大量数据&#xff0c;因此在这些操作上具有较高的效率。然而&#xff0c;访问列表中的特定…

无人机航电系统技术详解

一、系统概述 无人机航电系统&#xff08;Avionics System&#xff09;是无人机飞行与任务执行的核心部分&#xff0c;它集成了飞控系统、传感器、导航设备、通信设备等&#xff0c;为无人机提供了必要的飞行控制和任务执行能力。航电系统的设计和性能直接影响到无人机的安全性…

AIGC产品经理学习路径

基础篇&#xff08;课时 2 &#xff09; AIGC 行业视角 AIGC 的行业发展演进&#xff1a;传统模型/深度学习/大模型 AIGC 的产品设计演进&#xff1a;AI Embedded / AI Copilot / AI Agen AIGC 的行业产业全景图 AIGC 的产品应用全景图 AIGC 职业视角 AI 产品经理/ AIGC…

Linux:信号的概念与产生

信号概念 信号是进程之间事件异步通知的一种方式 在Linux命令行中&#xff0c;我们可以通过ctrl c来终止一个前台运行的进程&#xff0c;其实这就是一个发送信号的行为。我们按下ctrl c是在shell进程中&#xff0c;而被终止的进程&#xff0c;是在前台运行的另外一个进程。因…

Android Viewpager2 remove fragmen不生效解决方案

一、介绍 在如今的开发过程只&#xff0c;内容变化已多单一的fragment&#xff0c;变成连续的&#xff0c;特别是以短视频或者直播为主的场景很多。从早起的Viewpage只能横向滑动&#xff0c;到如今的viewpage2可以支持横向或者竖向滑动。由于viewpage2的adapter在设计时支持缓…