报名参加CloudWeGo黑客松,奖金直推双丰收! 了解详情
写点什么

详解 Dart 中如何通过注解生成代码

  • 2020-08-11
  • 本文字数:4219 字

    阅读完需:约 14 分钟

详解Dart中如何通过注解生成代码

背景

最近在项目中使用到了 Dart 中的注解代码生成技术,这跟之前 Java 中 APT+JavaPoet 生成代码那套技术还是有一些不同的地方,比如


  • Flutter 中在禁用了 dart:mirror,无法使用反射情况下如何得到类相关信息?

  • Dart 的文件不限制是 class,可以是 function、class,因而在注解扫描的范围不同的情况下如何拿到层层信息而不仅仅是 toplevel 信息?

  • 提取到注解信息时又是如何生成复杂的模板代码?


在 Flutter 中究竟是如何解决上面的问题呢?下面将一步步揭开这神秘的面纱。

一个简单的例子

先从一个简单的例子感受下 dart 中如何通过注解生成代码


  • 声明一个注解,并使用注解



在 Dart 中构造器用 const 修饰就好,可以看出 Dart 的注解声明起来比较简单,不像 java 中还得有运行类型如 RunTime、Source 等


  • 解析注解的生成器


在 Dart 中我们一般使用 source_gen 中的 GeneratorForAnnotation,该类继承自 Generator 这个跟 Java APT 中的 processor 职责类似,需要在 GeneratorForAnnotation 的泛型中填入我们需要处理的注解



  • 触发生成器的 Builer


有了上面的生成注解的生成器,我们还需要 Builder 来触发



  • 创建配置文件 build.yaml



  • 运行 builder


由于 Flutter 禁用了 dart:mirror 无法使用反射,因此只能在通过命令在编译期触发,执行如下命令,将会看到生成的代码




是不是感受到了 Dart 注解生成代码的奇特之处了,有像 Java 中 AnnotationProcessor Tool 的 Generator,但是又多了 Builder 和 build.yaml,那么这些是如何相互配合运行生成注解的呢?

宏观概念

使用望远镜宏观概览整个过程


当我们使用 buildrunner 的 build 之后 触发 build,会去读取 build.yaml 文件的配置信息,这个信息最终会被 buildconfig.dart 中的 BuildConfig 类读取到,然后通过读取到 builder,上面例子的 testBuilder,触发了其中的注解生成器(TestGenerator),来对抽象语法树进行信息提取(由于 source_gen 封装了语法分析库 analysis 和资源处理库 build,这里实际上是屏蔽了语法分析过程),跟 java 一样都是一个个 Element,具体可以看下代码的实现类


归纳一下主要有以下个核心部分:


用户触发 - 文件扫描 - 词法分析 - 注解提取 - 代码生成

微观探索

再使用放大镜仔仔细细研究一下其中的细节:

build.yaml 配置

在 Java 中我们使用谷歌提供的 AutoService 注解来生成 META-INF/services/javax.annotation.processing.Processor 文件关联注解处理器,但是 Flutter 中的 dart 注解只能在编译期做文章,因此需要一个配置告诉编译器,触发哪些 builder,对应的就是 build.yaml 文件,


先看一个 build.yaml 配置感受一下



build.yaml 配置的信息,最终都会被 buildconfig.dart 中的 BuildConfig 类读取到。关于参数说明,目前也没有太多资料,这里推荐官方说明 buildconfig,通过 buildconfig 包下的 BuildeConfig 解析


解析入口如下



从 build_config.dart 中可以看到,主要解析 4 个大的部分,下面将挑选常用的 2 个进行分析



targets


在 build_target.dart#BuildTarget 可以看到支持属性的描述,其中有个 builder 属性使用的比较多



在 TargetBuilderConfig 中有 3 个常用的属性


  • enable


当前 builder 是否生效


  • generate_for


这个属性比较重要,可以决定针对那些文件/文件夹做扫描,或者排除哪些文件 input_set.dart,使用如下



在 jsonseriable 的 build.yaml 中也可以看到它的 yaml 文件中对 generatefor 属性的使用


  • options


这个属性可以允许你以键值对形式携带一些配置数据到代码生成器中,对应的是 BuildOption 参数,下面在解读 builder 时候会再次讲述。


builder


来一个 builder



BuilderOptions 可以提取到上面的 option 属性配置


在 build.yaml 文件中描述如上,Map 即 BuilderDefinition 信息,下面将介绍一下常用的配置



更多配置可以参考 builder_definition.dart


其中有 2 个重要的属性单独解释一下


  • run_before


可以指定 builder 的运行顺序,如果几个 buidler 有互相依赖可以,比如在阿里的路由框架 annotationroute 中就使用到了这个属性,可以看看其 yaml 文件,主要在路由框架中使用到了 mustache4dart 需要收集路由信息来填充模板,它的解法是使用两个 builder,一个用来收集信息(routeWriteBuilder),收集完之后给另一个 builder(routeBuilder)结合 mustache4dart 模板来生成需要的路由表,具体可以参考其 routegenerator.dart


  • auto_apply



看文字可能理解起来可能有点晦涩,搞个图来解释一下,比如上图 libB 中使用了注解功能:


  • 当我们将 auto_apply 设置成 dependents 时:


如果 注解 package 是直接依赖在 libB 上的,那么只能在 libB 上正常使用注解,虽然 顶层 Package 包依赖了 libB,但是依然无法正常使用该注解


  • 当我们将 autoapply 设置成 allpackages 时:


如果 注解 package 是直接依赖在 libB 上的,那么在 libB 和 顶层 Package 上都能正常使用注解


  • 当我们将 autoapply 设置成 rootpackage 时:


如果 注解 package 是直接依赖在 libB 上的,那么只能在顶层 Package 上正常使用注解,虽然是 libB 上做的依赖,但是就是不能用,不过 注解 package 是直接依赖在 顶层 Package 上的时候,不管 autoapply 设置的是 dependents、allpackages 或者是 root_package 时,其实都是能正常使用的

关于 source_gen

简介


了解完了基本配置的 yaml 文件之后,不得不提 source_gen 这个强大的库,


sourcegen 基于官方的 analysis/build 提供了一系列友好的封装,sourcegen 基于 analyzer 和 build 库,其中


  • build 库主要是资源文件的处理

  • analyser 库是对 dart 文件生成语法结构 source_gen 主要提处理 dart 源码,可以通过注解生成代码。


核心类介绍



sourcegen 从 build 库提供的 Builder 派生出自己的 builder,并且封装了 3 个


Builder (builder.dart)|_Builder (builder.dart)|-LibraryBuilder (builder.dart)|-SharedPartBuilder (builder.dart)|-PartBuilder (builder.dart)
复制代码


  • SharedPartBuilder


生成.g.dart 文件,类似 jsonseriable 一样,使用地方需要用是 part of 引用,这样有个最大的好处就是引用问题不需要过于关注,要注意的是,需要使用 sourcegen|combining_builder,它会将所有.g 文件进行合并。


  • LibraryBuilder 生成独立的文件

  • PartBuilder 自定义 part 文件


生成器 Generator


并且 source_gen 封装了一套 Generator,以上的 buidler 接收 Generator 的集合,收集 Generator 的产出生成一份文件,Generator 只是一个抽象类,具体实现类是 GeneratorForAnnotation,默认只能拦截到 top-level 级别的(后面会解释)元素,会被注解生成器接受一个指定注解类型,即 GeneratorForAnnotation 是单注解处理器例如



由于 analyser 提供了语法节点的抽象元素 Element 和其 metadata 字段,对应 ElementAnnotation,注解生成器可以检查元素的 metadata 类型是否匹配声明的注解类型,从而找出被注解的元素及元素所在上下文的信息,然后将这些信息包装给使用者。


核心方法 generateForAnnotatedElement 例如我们有这样一段注解代码



从上面可以看出主要覆写了 generateForAnnotatedElement 方法,有三个关键参数


  • Element element


被 annotation 所修饰的元素,通过它可以获取到元素的 name、metadata、可见性等等。



更多 api 可以查看 element


关于 toplevel 注解


前文提到只能拦截到 toplevel 级别的元素,因此 class 内部的方法其实都没有扫描到,这是由于 dart 文件是不像 java,一个文件只能对应一个类,dart 文件可以是 function,也是是 class 或者其他,因此只能默认拦截到 top-level 级别的,后面需要开发者自己手动处理,比如 ClassElement 提供了 methods、fields 来给开发者进一步处理注解的机会,下面展示了解析类中的方法,属性也是类似的



Element 除了 ClassElementImpl 外还有多个派生如 FunctionElementImpl、ParamElementImpl 等,具体可以自行查阅。


  • ConstantReader annotation


表示注解对象,通过它可以提取到注解相关信息以及参数值


有两个关键方法


  • read

  • peek


不同之处在于,如果 read 方法读取了不存在的参数名,会抛出异常,peek 则不会,而是返回 null。


  • BuildStep buildStep


这一次构建的信息,通过它可以获取到一些输入输出信息,例如输入文件名等。


核心代码分析


source_gen 也是从 build 库的 Builder 封装而来



sourcegen 根据 Builder 实现自己的的 Builder,根据不同的特点派生出 SharedPartBuilder、LibraryBuilder、PartBuilder



这里面有个核心的 Generator



在 Builder 运行时,会调用 Generator 的 generate 方法,并传入两个重要的参数:


  • library 可以获取源代码信息以及注解信息

  • buildStep 它表示构建过程中的一个步骤,通过它,我们可以获取一些文件的输入输出信息


其中 library 包含的源码信息是一个个的 Element 元素,Element 只是抽象类,具体还是一个个 ClassElementImpl、FuncationElementImpl 等。source_gen 实现了该类 GeneratorForAnnotation



其中 第 2 点中 library.annotatedWith(typeChecker)跟进去看下


代码生成

  • 纯字符串拼接


使用三引号语法,这种只能解决一些低级生成


  • mustach


预制模板,通过一定的规则,提取信息之后填充信息到模板中,一个典型的例子如下



学习成本较低,适合一些固定格式的代码生成,比如路由表,阿里的 annotation_route 框架就是采用这个,可以看下它的模板 tpl



然后使用了 2 个生成器,一个用来采集信息,另一个用来将采集后的信息注入到 mustach 模板中



  • code_builder


非常强大,玩过 java 注解生成代码的朋友一定熟悉 javapoet,二者非常类似,code_builder 可以细分为表达式、语句、函数、类等等,就是学习成本比较高,需要按照它的语法去生成对应的代码,比如生成一个类



生成一个表达式



更多技巧需要看下源码去学习使用。

与 java 注解生成代码的对比

小结

本文初步探索了在 Dart 通过注解生成代码的技术,比起 java 的 apt,没有运行时反射用起来还是有点点麻烦,需要手动执行 build,而且各种繁琐的 builder 配置,让人感觉晦涩难懂,生成代码的技巧也跟 java 有着异曲同工之妙,需要借助一些外力比如 mustach,code_builder 等。这种技术给我们在解决一些例如路由,模板代码、动态代理等,多了一种处理手段,其他更多的使用场景需要我们去开发中慢慢探索。


参考



本文转载自公众号闲鱼技术(ID:XYtech_Alibaba)。


原文链接


https://mp.weixin.qq.com/s/ZA62prbsM6KwnHkBT4i7yQ


2020-08-11 10:002586

评论

发布
暂无评论
发现更多内容

App自动化测试的高级定位与PO设计模式

测试人

软件测试

HarmonyOS 5.0应用开发——Ability与Page数据传递

高心星

HarmonyOS ArkTS 鸿蒙Next

TiCDC 同步中断案例1 :修改时区后执行了受时区影响的 DDL 进而导致同步失败

TiDB 社区干货传送门

故障排查/诊断 6.x 实践

面向 TiDB AI 学习 TiDB

TiDB 社区干货传送门

TiDB Vector

TiDB数据库 最强SQL审核工具,求挑战

TiDB 社区干货传送门

性能测评 TiDB 源码解读 6.x 实践 TiDB Cloud TiDB Vector

食品加工、预制菜行业MES系统解决方案

万界星空科技

mes 万界星空科技mes 食品MES 食品加工 预制菜加工

2024年软件行业的发展趋势:从人工智能到低代码平台的变革

天津汇柏科技有限公司

云计算 低代码 AI 人工智能

面试:如何回答HR的问题

老张

面试 求职面试 职场认知

深入探索 WebView与微信小程序测试的奥秘

测试人

软件测试

深度揭秘“快稳省”背后的数仓硬核技术

字节跳动数据平台

大数据 数据仓库 云原生

腾讯云 AI 代码助手:AI Agent 构建企业新一代研发范式

CodeBuddy

Lightning导入单个TB级CSV文件加速方案

TiDB 社区干货传送门

迁移 管理与运维 大数据场景实践 8.x 实践

使用 Grafana 展示多个TiDB集群的告警

TiDB 社区干货传送门

监控 管理与运维

TiDB生态新伙伴:Navicat正式支持TiDB

TiDB 社区干货传送门

管理与运维 应用适配

区块链在溯源系统中的技术原理

北京木奇移动技术有限公司

区块链技术 区块链溯源系统开发 软件外包公司

校园兼职 | 大学生运营推广专员招募中!

测吧(北京)科技有限公司

测试

预制菜智能化生产管理MES系统解决方案

万界星空科技

mes 万界星空科技mes 预制菜加工 预制菜工厂 预制菜生产管理

什么是二级域名?如何申请二级域名?

国科云

大模型赋能智能编码安全|「智效融合,安全护航」西安站技术沙龙成功举办

百度安全

电商产品自动化测试实战——解锁高效测试新技能

测吧(北京)科技有限公司

测试

区块链智能合约开发的技术难点

北京木奇移动技术有限公司

区块链技术 智能合约开发 软件外包公司

杭州银行:分布式场景下快速构建数据模拟环境的探索与实践

TiDB 社区干货传送门

区块链智能合约的开发流程

北京木奇移动技术有限公司

区块链开发 智能合约开发 软件外包公司 新加坡

异构算力开源社区HAMi举办首届沙龙,将发布新版本,效能全面提升

新消费日报

玉溪具有资质等保测评机构在哪里?电话多少?

行云管家

网络安全 等保 等保测评 玉溪

中昊芯英创始人及CEO杨龚轶凡受邀出席2024企业家博鳌论坛

科技热闻

等保测评周期一般是多少?最长多久?

行云管家

网络安全 等保 等级保护 等保测评

区块链技术中的智能合约评审

北京木奇移动技术有限公司

区块链技术 智能合约开发 软件外包公司

HarmonyOS Web场景性能优化指导

HarmonyOS开发者

详解Dart中如何通过注解生成代码_大前端_龙湫_InfoQ精选文章