写点什么

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

评论

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

Flink是如何支持批流一体的

编程江湖

flink

无服务器应用DevOps最新实践(内附完整演讲+视频)

亚马逊云科技 (Amazon Web Services)

计算

这8个JS 新功能,你应该去尝试一下

华为云开发者联盟

JavaScript 前端 开发 索引 开发语言

前端开发之Vue事件修饰符和按键修饰符

@零度

Vue 前端开发

改进企业CRM系统实施的方法

低代码小观

企业管理 CRM 企业管理系统 CRM系统 企业管理工具

物联网场景中灵活实施对设备的控制管理

亚马逊云科技 (Amazon Web Services)

analytics

第二节:SpingBoot单元测试

入门小站

java 编程

IT运维人员日常工作包含哪些?核心任务是什么?工作量多吗?

行云管家

运维 IT运维 服务器运维

iOS——解密RunLoop原理

iOSer

ios iOS面试 ios开发 RunLoop

实战 MongoDB Aggregate

PingCode研发中心

mongo pipeline Expression

深入解析Apache Pulsar系列: Broker消息确认的管理

博文视点Broadview

java开发之Redis数据结构

@零度

redis JAVA开发

手把手教程|通过部署 Apache Superset 实现 Amazon S3 的数据可视化

亚马逊云科技 (Amazon Web Services)

analytics

4 种高速安全混合云解决方案,助力您的云迁移之旅!

亚马逊云科技 (Amazon Web Services)

网络

复盘和反思一个被全公司邮件通报的漏测

LynnYang

测试 Postman Mock

LeetCode 每日一题 No.1220 统计元音字母序列的数目

DawnMagnet

rust LeetCode 力扣

阿里云视频云「 vPaaS 」演绎了怎样的音视频应用开发「未来图景」?

阿里云CloudImagine

阿里云 音视频 低代码 低代码开发平台 视频云

在字节,A/B 实验是这么做的!

字节跳动数据平台

大数据 字节跳动 AB testing实战 ab测试

大数据平台中的企业级数仓建设

五分钟学大数据

数据仓库 1月月更

建木持续集成平台v2.2.0发布

Jianmu

开源 持续集成 CI/CD

恒源云(GPUSHARE)_实例关机后如何操作迁移?

恒源云

gpu 运维 实例

腾讯自选股如何实现单位小时内完成千万级数据运算

ninetyhe

腾讯 海量数据 分布式,

Linux下玩转nginx系列(一)——初识nginx及其使用入门

anyRTC开发者

nginx Linux 音视频 WebRTC 服务器

【网络安全】2022年第一次靶场渗透实战学习

H

网络安全 渗透测试

数据安全是指什么?有什么意义?

行云管家

防火墙 信息安全 数据安全 堡垒机

打造手淘极简包的轻量化框架

阿里巴巴终端技术

ios android 框架设计 移动开发 包大小

60岁代码匠的几篇小作文,解决了大多数程序的迷茫(下)

图灵社区

java 编程

2022年RPA行业发展十大趋势,六千字长文助你看懂RPA

王吉伟频道

RPA 机器人流程自动化 RPAaaS 超自动化 自动化优先

带你玩转Flink流批一体分布式实时处理引擎

华为云开发者联盟

flink 分布式 实时计算 批处理 流处理框架

17 Prometheus之服务发现介绍

穿过生命散发芬芳

Prometheus 1月月更

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