简介: 详解dart与java注解生成代码异同点

作者:闲鱼技术-龙湫

1、背景

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

  • Flutter中在禁用了dart:mirror,无奈应用反射状况下如何失去类相干信息?
  • Dart的文件不限度是class,能够是function、class,因此在注解扫描的范畴不同的状况下如何拿到层层信息而不仅仅是toplevel信息?
  • 提取到注解信息时又是如何生成简单的模板代码?

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

2、一个简略的例子

先从一个简略的例子感触下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,那么这些是如何相互配合运行生成注解的呢?

3、宏观概览

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

当咱们应用build_runner的 build之后 触发build,会去读取build.yaml文件的配置信息,这个信息最终会被
build_config.dart中的BuildConfig类读取到,而后通过读取到builder,下面例子的testBuilder,触发了其中的注解生成器(TestGenerator),来对形象语法树进行信息提取(因为source_gen封装了语法分析库analysis和资源解决库build,这里实际上是屏蔽了语法分析过程),跟java一样都是一个个Element,具体能够看下代码的实现类

演绎一下次要有以下个外围局部:

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

4、宏观摸索

再应用放大镜仔仔细细钻研一下其中的细节:

4.1 build.yaml配置

在Java中咱们应用谷歌提供的AutoService注解来生成META-INF/services/javax.annotation.processing.Processor 文件关联注解处理器,然而Flutter中的dart注解只能在编译期做文章,因而须要一个配置通知编译器,触发哪些builder,对应的就是build.yaml文件,
先看一个build.yaml配置感受一下

build.yaml 配置的信息,最终都会被 build_config.dart 中的 BuildConfig 类读取到。
对于参数阐明,目前也没有太多材料,这里举荐官网阐明build_config,通过build_config包下的Builde_Config解析

解析入口如下

从build_config.dart中能够看到,次要解析4个大的局部,上面将筛选罕用的2个进行剖析

4.1.1 targets

在 build_target.dart#BuildTarget 能够看到反对属性的形容,其中有个builder属性应用的比拟多

在TargetBuilderConfig中有3个罕用的属性

  • enable

以后builder是否失效

  • generate_for

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


在json_seriable的build.yaml中也能够看到它的yaml文件中对generate_for属性的应用

  • options

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

4.1.2 builder

来一个builder

BuilderOptions能够提取到下面的option属性配置

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

更多配置能够参考builder_definition.dart

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

  • run_before

能够指定builder的运行程序,如果几个buidler有相互依赖能够,比方在阿里的路由框架annotation_route中就应用到了这个属性,能够看看其yaml文件,次要在路由框架中应用到了mustache4dart须要收集路由信息来填充模板,它的解法是应用两个builder,一个用来收集信息(routeWriteBuilder),收集完之后给另一个builder(routeBuilder)联合mustache4dart模板来生成须要的路由表,具体能够参考其route_generator.dart

  • auto_apply


看文字可能了解起来可能有点艰涩,搞个图来解释一下,比方上图 libB中应用了注解性能:

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

如果 注解package 是间接依赖在 libB 上的,那么只能在 libB 上失常应用注解,尽管 顶层Package 包依赖了 libB,然而仍然无奈失常应用该注解

  • 当咱们将auto_apply设置成all_packages时:

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

  • 当咱们将auto_apply设置成root_package时:

如果 注解package 是间接依赖在 libB 上的,那么只能在顶层 Package 上失常应用注解,尽管是 libB 上做的依赖,然而就是不能用,不过 注解package 是间接依赖在 顶层Package 上的时候,不论 auto_apply 设置的是 dependents、all_packages 或者是 root_package 时,其实都是能失常应用的

4.2 对于source_gen

4.2.1 简介

理解完了根本配置的yaml文件之后,不得不提source_gen这个弱小的库,

source_gen基于官网的 analysis/build提供了一系列敌对的封装,source_gen 基于 analyzer 和 build 库,其中

  • build库次要是资源文件的解决
  • analyser库是对dart文件生成语法结构
    source_gen次要提解决dart源码,能够通过注解生成代码。

4.2.2外围类介绍

source_gen从build库提供的Builder派生出本人的_builder,并且封装了3个

Builder (builder.dart)

|_Builder (builder.dart)

   |-LibraryBuilder (builder.dart)   |-SharedPartBuilder (builder.dart)   |-PartBuilder (builder.dart)   

• SharedPartBuilder

生成.g.dart文件,相似json_seriable一样,应用中央须要用是part of援用,这样有个最大的益处就是援用问题不须要过于关注,要留神的是,须要应用 source_gen|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

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

4.2.3外围代码剖析

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


source_gen依据Builder实现本人的的_Builder,依据不同的特点派生出 SharedPartBuilder、LibraryBuilder、PartBuilder

这外面有个外围的 Generator

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

  • library 能够获取源代码信息以及注解信息
  • buildStep 它示意构建过程中的一个步骤,通过它,咱们能够获取一些文件的输入输出信息

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

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

5、对于代码生成

  • 纯字符串拼接

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

  • mustach

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

学习老本较低(理解mustach更多规定),适宜一些固定格局的代码生成,比方路由表,阿里的annotation_route框架就是采纳这个,能够看下它的模板tpl,

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

  • code_builder

十分弱小,玩过java注解生成代码的敌人肯定相熟javapoet,二者十分相似,code_builder能够细分为表达式、语句、函数、类等等,就是学习老本比拟高,须要依照它的语法去生成对应的代码,比方生成一个类

生成一个表达式

更多技巧须要看下源码去学习应用。

6、与Java注解生成代码比照

7、小结

本文初步摸索了在Dart通过注解生成代码的技术,比起java的apt,没有运行时反射用起来还是有点点麻烦,须要手动执行build,而且各种繁琐的builder配置,让人感觉艰涩难懂,生成代码的技巧也跟java有着殊途同归之妙,须要借助一些外力比方mustach,code_builder等。这种技术给咱们在解决一些例如路由,模板代码、动静代理等,多了一种解决伎俩,其余更多的应用场景须要咱们去开发中缓缓摸索。

参考

  • mustache
  • code_builder
  • https://github.com/Reign9201/image_path_helper
  • source_gen
  • Flutter 注解解决及代码生成
  • [[Part 2] Code generation in Dart: Annotations, source_gen and build_runner](https://medium.com/flutter-community/part-2-code-generation-in-dart-annotations-source-gen-and-build-runner-bbceee28697b))