重磅开源|AOP for Flutter开发利器——AspectD

问题背景

随着Flutter这一框架的快速发展,有越来越多的业务开始使用Flutter来重构或新建其产品。但在我们的实践过程中发现,一方面Flutter开发效率高,性能优异,跨平台表现好,另一方面Flutter也面临着插件,基础能力,底层框架缺失或者不完善等问题。

举个栗子,我们在实现一个自动化录制回放的过程中发现,需要去修改Flutter框架(Dart层面)的代码才能够满足要求,这就会有了对框架的侵入性。要解决这种侵入性的问题,更好地减少迭代过程中的维护成本,我们考虑的首要方案即面向切面编程。

那么如何解决AOP for Flutter这个问题呢?本文将重点介绍一个闲鱼技术团队开发的针对Dart的AOP编程框架AspectD。

AspectD:面向Dart的AOP框架

AOP能力究竟是运行时还是编译时支持依赖于语言本身的特点。举例来说在iOS中,Objective C本身提供了强大的运行时和动态性使得运行期AOP简单易用。在Android下,Java语言的特点不仅可以实现类似AspectJ这样的基于字节码修改的编译期静态代理,也可以实现Spring AOP这样的基于运行时增强的运行期动态代理。
那么Dart呢?一来Dart的反射支持很弱,只支持了检查(Introspection),不支持修改(Modification);其次Flutter为了包大小,健壮性等的原因禁止了反射。

因此,我们设计实现了基于编译期修改的AOP方案AspectD。

设计详图

典型的AOP场景

下列AspectD代码说明了一个典型的AOP使用场景:

aop.dartimport 'package:example/main.dart' as app;
import 'aop_impl.dart';void main()=> app.main();
aop_impl.dartimport 'package:aspectd/aspectd.dart';@Aspect()
@pragma("vm:entry-point")
class ExecuteDemo {@pragma("vm:entry-point")ExecuteDemo();@Execute("package:example/main.dart", "_MyHomePageState", "-_incrementCounter")@pragma("vm:entry-point")void _incrementCounter(PointCut pointcut) {pointcut.proceed();print('KWLM called!');}
}

面向开发者的API设计

PointCut的设计

@Call("package:app/calculator.dart","Calculator","-getCurTime")

PointCut需要完备表征以怎么样的方式(Call/Execute等),向哪个Library,哪个类(Library Method的时候此项为空),哪个方法来添加AOP逻辑。
PointCut的数据结构:

@pragma('vm:entry-point')
class PointCut {final Map<dynamic, dynamic> sourceInfos;final Object target;final String function;final String stubId;final List<dynamic> positionalParams;final Map<dynamic, dynamic> namedParams;@pragma('vm:entry-point')PointCut(this.sourceInfos, this.target, this.function, this.stubId,this.positionalParams, this.namedParams);@pragma('vm:entry-point')Object proceed(){return null;}
}

其中包含了源代码信息(如库名,文件名,行号等),方法调用对象,函数名,参数信息等。
请注意这里的@pragma('vm:entry-point')注解,其核心逻辑在于Tree-Shaking。在AOT(ahead of time)编译下,如果不能被应用主入口(main)最终可能调到,那么将被视为无用代码而丢弃。AOP代码因为其注入逻辑的无侵入性,显然是不会被main调到的,因此需要此注解告诉编译器不要丢弃这段逻辑。
此处的proceed方法,类似AspectJ中的ProceedingJoinPoint.proceed()方法,调用pointcut.proceed()方法即可实现对原始逻辑的调用。原始定义中的proceed方法体只是个空壳,其内容将会被在运行时动态生成。

Advice的设计

@pragma("vm:entry-point")
Future<String> getCurTime(PointCut pointcut) async{...return result;
}

此处的@pragma("vm:entry-point")效果同a中所述,pointCut对象作为参数传入AOP方法,使开发者可以获得源代码调用信息的相关信息,实现自身逻辑或者是通过pointcut.proceed()调用原始逻辑。

Aspect的设计

@Aspect()
@pragma("vm:entry-point")
class ExecuteDemo {@pragma("vm:entry-point")ExecuteDemo();...}

Aspect的注解可以使得ExecuteDemo这样的AOP实现类被方便地识别和提取,也可以起到开关的作用,即如果希望禁掉此段AOP逻辑,移除@Aspect注解即可。

AOP代码的编译

包含原始工程中的main入口

从上文可以看到,aop.dart引入import 'package:example/main.dart' as app;,这使得编译aop.dart时可包含整个example工程的所有代码。

Debug模式下的编译

在aop.dart中引入import 'aop_impl.dart';这使得aop_impl.dart中内容即便不被aop.dart显式依赖,也可以在Debug模式下被编译进去。

Release模式下的编译

在AOT编译(Release模式下),Tree-Shaking逻辑使得当aop_impl.dart中的内容没有被aop中main调用时,其内容将不会编译到dill中。通过添加@pragma("vm:entry-point")可以避免其影响。

当我们用AspectD写出AOP代码,透过编译aop.dart生成中间产物,使得dill中既包含了原始项目代码,也包含了AOP代码后,则需要考虑如何对其修改。在AspectJ中,修改是通过对Class文件进行操作实现的,在AspectD中,我们则对dill文件进行操作。

Dill操作

dill文件,又称为Dart Intermediate Language,是Dart语言编译中的一个概念,无论是Script Snapshot还是AOT编译,都需要dill作为中间产物。

Dill的结构

我们可以通过dart sdk中的vm package提供的dump_kernel.dart打印出dill的内部结构。

dart bin/dump_kernel.dart /Users/kylewong/Codes/AOP/aspectd/example/aop/build/app.dill /Users/kylewong/Codes/AOP/aspectd/example/aop/build/app.dill.txt

Dill变换

dart提供了一种Kernel to Kernel Transform的方式,可以通过对dill文件的递归式AST遍历,实现对dill的变换。

基于开发者编写的AspectD注解,AspectD的变换部分可以提取出是哪些库/类/方法需要添加怎样的AOP代码,再在AST递归的过程中通过对目标类的操作,实现Call/Execute这样的功能。

一个典型的Transform部分逻辑如下所示:

  @overrideMethodInvocation visitMethodInvocation(MethodInvocation methodInvocation) {methodInvocation.transformChildren(this);Node node = methodInvocation.interfaceTargetReference?.node;String uniqueKeyForMethod = null;if (node is Procedure) {Procedure procedure = node;Class cls = procedure.parent as Class;String procedureImportUri = cls.reference.canonicalName.parent.name;uniqueKeyForMethod = AspectdItemInfo.uniqueKeyForMethod(procedureImportUri, cls.name, methodInvocation.name.name, false, null);}else if(node == null) {String importUri = methodInvocation?.interfaceTargetReference?.canonicalName?.reference?.canonicalName?.nonRootTop?.name;String clsName = methodInvocation?.interfaceTargetReference?.canonicalName?.parent?.parent?.name;String methodName = methodInvocation?.interfaceTargetReference?.canonicalName?.name;uniqueKeyForMethod = AspectdItemInfo.uniqueKeyForMethod(importUri, clsName, methodName, false, null);}if(uniqueKeyForMethod != null) {AspectdItemInfo aspectdItemInfo = _aspectdInfoMap[uniqueKeyForMethod];if (aspectdItemInfo?.mode == AspectdMode.Call &&!_transformedInvocationSet.contains(methodInvocation) && AspectdUtils.checkIfSkipAOP(aspectdItemInfo, _curLibrary) == false) {return transformInstanceMethodInvocation(methodInvocation, aspectdItemInfo);}}return methodInvocation;}

通过对于dill中AST对象的遍历(此处的visitMethodInvocation函数),结合开发者书写的AspectD注解(此处的_aspectdInfoMap_和aspectdItemInfo),可以对原始的AST对象(此处methodInvocation)进行变换,从而改变原始的代码逻辑,即Transform过程。

AspectD支持的语法

不同于AspectJ中提供的BeforeAroundAfter三种预发,在AspectD中,只有一种统一的抽象即Around。
从是否修改原始方法内部而言,有Call和Execute两种,前者的PointCut是调用点,后者的PointCut则是执行点。

Call

import 'package:aspectd/aspectd.dart';@Aspect()
@pragma("vm:entry-point")
class CallDemo{@Call("package:app/calculator.dart","Calculator","-getCurTime")@pragma("vm:entry-point")Future<String> getCurTime(PointCut pointcut) async{print('Aspectd:KWLM02');print('${pointcut.sourceInfos.toString()}');Future<String> result = pointcut.proceed();String test = await result;print('Aspectd:KWLM03');print('${test}');return result;}
}

Execute

import 'package:aspectd/aspectd.dart';@Aspect()
@pragma("vm:entry-point")
class ExecuteDemo{@Execute("package:app/calculator.dart","Calculator","-getCurTime")@pragma("vm:entry-point")Future<String> getCurTime(PointCut pointcut) async{print('Aspectd:KWLM12');print('${pointcut.sourceInfos.toString()}');Future<String> result = pointcut.proceed();String test = await result;print('Aspectd:KWLM13');print('${test}');return result;}

Inject

仅支持Call和Execute,对于Flutter(Dart)而言显然很是单薄。一方面Flutter禁止了反射,退一步讲,即便Flutter开启了反射支持,依然很弱,并不能满足需求。
举个典型的场景,如果需要注入的dart代码里,x.dart文件的类y定义了一个私有方法m或者成员变量p,那么在aop_impl.dart中是没有办法对其访问的,更不用说多个连续的私有变量属性获得。另一方面,仅仅对方法整体进行操作可能是不够的,我们可能需要在方法的中间插入处理逻辑。
为了解决这一问题,AspectD设计了一种语法Inject,参见下面的例子:
flutter库中包含了一下这段手势相关代码:

@overrideWidget build(BuildContext context) {final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};if (onTapDown != null || onTapUp != null || onTap != null || onTapCancel != null) {gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(() => TapGestureRecognizer(debugOwner: this),(TapGestureRecognizer instance) {instance..onTapDown = onTapDown..onTapUp = onTapUp..onTap = onTap..onTapCancel = onTapCancel;},);}

如果我们想要在onTapCancel之后添加一段对于instance和context的处理逻辑,Call和Execute是不可行的,而使用Inject后,只需要简单的几句即可解决:

import 'package:aspectd/aspectd.dart';@Aspect()
@pragma("vm:entry-point")
class InjectDemo{@Inject("package:flutter/src/widgets/gesture_detector.dart","GestureDetector","-build", lineNum:452)@pragma("vm:entry-point")static void onTapBuild() {Object instance; //Aspectd IgnoreObject context; //Aspectd Ignoreprint(instance);print(context);print('Aspectd:KWLM25');}
}

通过上述的处理逻辑,经过编译构建后的dill中的GestureDetector.build方法如下所示:

此外,Inject的输入参数相对于Call/Execute而言,多了一个lineNum的命名参数,可用于指定插入逻辑的具体行号。

构建流程支持

虽然我们可以通过编译aop.dart达到同时编译原始工程代码和AspectD代码到dill文件,再通过Transform实现dill层次的变换实现AOP,但标准的flutter构建(即flutter_tools)并不支持这个过程,所以还是需要对构建过程做细微修改。
在AspectJ中,这一过程是由非标准Java编译器的Ajc来实现的。在AspectD中,通过对flutter_tools打上应用Patch,可以实现对于AspectD的支持。

kylewong@KyleWongdeMacBook-Pro fluttermaster % git apply --3way /Users/kylewong/Codes/AOP/aspectd/0001-aspectd.patch
kylewong@KyleWongdeMacBook-Pro fluttermaster % rm bin/cache/flutter_tools.stamp
kylewong@KyleWongdeMacBook-Pro fluttermaster % flutter doctor -v
Building flutter tool...

实战与思考

基于AspectD,我们在实践中成功地移除了所有对于Flutter框架的侵入性代码,实现了同有侵入性代码同样的功能,支撑上百个脚本的录制回放与自动化回归稳定可靠运行。

从AspectD的角度看,Call/Execute可以帮助我们便捷实现诸如性能埋点(关键方法的调用时长),日志增强(获取某个方法具体是在什么地方被调用到的详细信息),Doom录制回放(如随机数序列的生成记录与回放)等功能。Inject语法则更为强大,可以通过类似源代码诸如的方式,实现逻辑的自由注入,可以支持诸如App录制与自动化回归(如用户触摸事件的录制与回放)等复杂场景。

进一步来说,AspectD的原理基于Dill变换,有了Dill操作这一利器,开发者可以自由地对Dart编译产物进行操作,而且这种变换面向的是近乎源代码级别的AST对象,不仅强大而且可靠。无论是做一些逻辑替换,还是是Json<-->模型转换等,都提供了一种新的视角与可能。

写在最后

AspectD作为闲鱼技术团队新开发的面向Flutter的AOP框架,已经可以支持主流的AOP场景并在Github开源,欢迎使用。


原文链接
本文为云栖社区原创内容,未经允许不得转载。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue-cli 3.X 构建工具零基础快速上手

文章目录一、环境准备1. 安装node2. 配置镜像二、安装Vue CLI2.1. 查看当前vuecli版本2.2. 安装最新版本2.3. 安装指定版本三、创建web项目3.1. 指定创建的项目名称3.2. 安装序列图3.3. 安装序列图简述一、环境准备 声明&#xff1a;命令均在在cmd窗口执行 1. 安装node 2. 配置…

分布式数据库选型——数据水平拆分方案

概述 水平拆分的概念随着分布式数据库的推广已为大部分人熟知。分库分表、异构索引、小表广播、这些功能几乎是产品功能需求标配。然而有些客户使用分布式数据库后的体验不尽如意。本文尝试从数据的角度总结分布式数据的复制&#xff08;replication&#xff09;和分区&#x…

从代码到 Docker、Kubernetes、Istio、Knative……,或许是时候重新思考从代码到云的编程了...

作者 | Lakmal Warusawithana译者 |弯月 责编 | 徐威龙封图| CSDN 下载于视觉中国早些时候&#xff0c;开发人员只需编写程序、构建&#xff0c;然后运行。如今&#xff0c;开发人员还需要考虑各种运行方式&#xff0c;作为可执行文件在机器上运行&#xff08;很有可能是虚拟机…

容器服务Windows Kubernetes使用阿里云日志服务来收集容器日志

目前&#xff0c;容器服务Windows Kubernetes支持将业务容器产生的stdout输出、日志文件同步到阿里云日志服务&#xff08;SLS&#xff09;进行统一管理。 支撑组件安装 在Windows Kubernetes集群安装界面勾选使用日志服务&#xff0c;集群会安装支持日志收集的必要组件logta…

Java-Arrays类

public class ArrayDemo06 {public static void main(String[] args) {int[] a {1,4,2,6,5,8,7};System.out.println(a); //hashCode [I1b6d3586// 打印数组元素 Arrays.toStringSystem.out.println(Arrays.toString(a)); // [1, 4, 2, 6, 5, 8, 7]// 自己写一个方法去打印…

看完这一篇,你就对 Spring Security 略窥门径了 | 原力计划

作者 | BoCong-Deng来源 | CSDN 博客&#xff0c;责编 | 夕颜头图 | CSDN 下载自东方 IC出品 | CSDN&#xff08;ID:CSDNnews&#xff09;写在前面开发Web应用&#xff0c;对页面的安全控制通常是必须的。比如&#xff1a;对于没有访问权限的用户需要转到登录表单页面。要实现访…

Java-冒泡排序

public class ArrayDemo07 {public static void main(String[] args) {int[] a {3,5,1,7,8,4};int[] sort sort(a);System.out.println(Arrays.toString(sort)); // [1, 3, 4, 5, 7, 8]}/*每次将 最大 或 最小的数 后移*/public static int[] sort(int[] array){// 临时变量…

借助 Cloud Toolkit 快速创建 Dubbo 工程

Cloud Toolkit 是一个 IDE 插件&#xff0c;帮助开发者更高效地开发、测试、诊断并部署应用。在最新版的插件中&#xff0c;提供了快速创建 Dubbo 工程的功能&#xff0c;下面就来快速体验下吧。 Dubbo 采用全 Spring 配置方式&#xff0c;透明化接入应用&#xff0c;对应用没…

vue-cli-service不是内部或外部命令,也不是可运行的程序

报错信息: “不是内部或外部命令&#xff0c;也不是可运行的程序” 步骤一: 检查package.json 中是否有 vue-cli-server,没有则需安装 步骤二 : 执行npm install命令 npm run serve

另一种声音:容器是不是未来?

作者 | Ian Eyberg译者 | 天道酬勤 责编 | 徐威龙封图| CSDN 下载于视觉中国CSDN 云计算旨在为读者提供更多角度的声音&#xff0c;本文仅代表作者个人观点&#xff0c;不代表CSDN云计算任何立场。前几天作者看到了这则推文&#xff0c;可以这么说&#xff0c;是它促使我开始就…

Java-稀疏数组

public class ArrayDemo08 {public static void main(String[] args) {// 1. 创建一个二维数组 11*11 0: 没有棋子 1: 黑棋 2: 白棋int[][] array1 new int[11][11];array1[1][2] 1;array1[2][3] 2;// 输出原始的数组System.out.println("输出原始的数组");for…

揭秘|每秒千万级的实时数据处理是怎么实现的?

1、设计背景 闲鱼目前实际生产部署环境越来越复杂&#xff0c;横向依赖各种服务盘宗错节&#xff0c;纵向依赖的运行环境也越来越复杂。当服务出现问题的时候&#xff0c;能否及时在海量的数据中定位到问题根因&#xff0c;成为考验闲鱼服务能力的一个严峻挑战。 线上出现问题…

Vue3.x 使用ref和reactive、toRef

文章目录一、使用ref1. 引入2. 定义ref变量3. 赋值.value4. return 返回值5. 将数据渲染到页面二、使用reactive 和 toRef1. 引入2. 定义reactive变量3. 赋值变量.对象4. return 返回值5. 将数据渲染到页面三、效果图源码3.1. 效果图3.2. 源码一、使用ref 1. 引入 从vue里面引…

同学,要不要来挑战双11零点流量洪峰?

阿里妹导读&#xff1a;双十一的零点&#xff0c;整个电商系统的请求速率到达峰值。如果将这些请求流量只分配给少部分 server&#xff0c;这些机器接收到的请求速率会远超过处理速率&#xff0c;新来的任务来不及处理&#xff0c;就会产生请求任务堆积。 今年的中间件性能挑战…

GitHub 疑遭中间人攻击,最大暗网托管商再被黑!

整理 | 伍杏玲出品 | 程序人生&#xff08;ID&#xff1a;coder_life&#xff09;近期&#xff0c;在全球关注新冠肺炎疫情之际&#xff0c;黑客却频频动作&#xff0c;发动攻击&#xff1a;GitHub 疑遭中间人攻击&#xff0c;无法访问从26日下午开始&#xff0c;有网友表示国内…

Java-什么是面向对象

https://www.bilibili.com/video/BV12J41137hu?p60

Jenkins Pipeline脚本优化:为Kubernetes应用部署增加状态检测

引言 在软件部署的世界中&#xff0c;Jenkins已经成为自动化流程的代名词。不断变化的技术环境要求我们持续改进部署流程以满足现代应用部署的需要。在本篇博客中&#xff0c;作为一位资深运维工程师&#xff0c;我将分享如何将Jenkins Pipeline进化至不仅能支持部署应用直至R…

Apache Flink 结合 Kafka 构建端到端的 Exactly-Once 处理

文章目录&#xff1a; Apache Flink 应用程序中的 Exactly-Once 语义Flink 应用程序端到端的 Exactly-Once 语义示例 Flink 应用程序启动预提交阶段在 Flink 中实现两阶段提交 Operator总结 Apache Flink 自2017年12月发布的1.4.0版本开始&#xff0c;为流计算引入了一个重要的…

一文教你如何使用 MongoDB 和 HATEOAS 创建 REST Web 服务

作者 | Ion Pascari译者 | 天道酬勤 责编 | 徐威龙封图| CSDN 下载于视觉中国最近&#xff0c;作者在把HATEOAS实现到REST Web服务时遇到了一件有趣的事情&#xff0c;而且他也很幸运地尝试了一个名为MongoDB的NoSQL数据库&#xff0c;他发现该数据库在许多不需要管理实务的不同…

使用NGINX作为HTTPS正向代理服务器

NGINX主要设计作为反向代理服务器&#xff0c;但随着NGINX的发展&#xff0c;它同样能作为正向代理的选项之一。正向代理本身并不复杂&#xff0c;而如何代理加密的HTTPS流量是正向代理需要解决的主要问题。本文将介绍利用NGINX来正向代理HTTPS流量两种方案&#xff0c;及其使用…