写点什么

详解 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:002747

评论

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

善盾SD币是什么?

飞亚科技

区块链农产品质量安全溯源,保证农产品品质

13530558032

EFT【阿凡提】等级规则、收益、排线方法与EFTalk十大关键点

币圈那点事

语音聊天室 anyHouse 使用手册

anyRTC开发者

ios android 音视频 WebRTC RTC

华云大咖说 | 华云数据与海量数据携手共建国产云生态

华云数据

区块链数据共享平台—追踪、溯源、可信

电微13828808271

区块链+

【LeetCode】寻找旋转排序数组中的最小值Java题解

Albert

算法 LeetCode 4月日更

重磅功能!博睿数据APM助企业从容应对云原生架构演进

博睿数据

应用性能监控产品 Bonree Server 博睿数据 bonree

马特量化交易机器人,炒币24小时不停歇

飞亚科技

Spring IOC 特性有哪些,不会读不懂源码!

小傅哥

Java spring 小傅哥 控制反转IOC

区块链产业园区服务平台开发,搭建区块链园区运营平台

13828808769

区块链+ #区块链#

Linux cat 命令

一个大红包

4月日更

盘点 15 个好用的 API 接口管理神器

Java小咖秀

工具 工具分享

手把手教你写一个spring IOC容器

华为云开发者联盟

spring 容器 ioc spring框架

技术分享第二讲报名!

神策技术社区

大数据 活动 报名 神策

火山引擎 Redis 云原生实践

火山引擎开发者社区

云原生 redis cluster

python 调用 cmd 而不显示黑框的方法

一代咩神

Python cmd

初识Nginx(一)

书旅

nginx

年薪百万是社会认同,更是自身价值体现

博文视点Broadview

区块链电子合同--助推合同数字化管理

13530558032

多功能工具箱Quicker+笔记软件flomo,竟然还能擦出这样的火花?

彭宏豪95

效率 工具软件 笔记 工具分享 4月日更

面试笔记(一)事务连环炮

U2647

分布式事务 事务隔离级别 事务 4月日更

LeetCode题解:剑指 Offer 49. 丑数,二叉堆,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

空中交警:借你一双“慧眼”,让你看透这飞机的“黑色十分钟”

华为云开发者联盟

modelarts yolo 华为云ModelArts 模型开发 华为开发者大会2021

3w 字长文爆肝 Java 基础面试题!太顶了!!!

苹果看辽宁体育

Java 面试 后端

区块链农产品溯源平台,为农产品质量安全护航

13828808769

区块链 区块链+

Golang 字符串分组

一代咩神

Go 语言

智慧平安社区建设,创建“三零平安社区”

13530558032

团队协作中,如何写出让同事赞不绝口的代码

有道技术团队

代码规范

SumSwap节点预售关注度飙升而Uniswap V3版本却备受争议

币圈资讯

“区块链+电子处方”,医疗跟更健康

电微13828808271

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