天天看點

詳解Dart中如何通過注解生成代碼

作者:閑魚技術-龍湫

1、背景

最近在項目中使用到了Dart中的注解代碼生成技術,這跟之前Java中APT+JavaPoet生成代碼那套技術還是有一些不同的地方,比如

  • Flutter中在禁用了dart:mirror,無法使用反射情況下如何得到類相關資訊?
  • Dart的檔案不限制是class,可以是function、class,因而在注解掃描的範圍不同的情況下如何拿到層層資訊而不僅僅是toplevel資訊?
  • 提取到注解資訊時又是如何生成複雜的模闆代碼?

在Flutter中究竟是如何上面的問題呢?下面将一步步揭開這神秘的面紗。

2、一個簡單的例子

先從一個簡單的例子感受下dart中如何通過注解生成代碼

  • 聲明一個注解,并使用注解
詳解Dart中如何通過注解生成代碼

在Dart中構造器用const修飾就好,可以看出Dart的注解聲明起來比較簡單,不像java中還得有運作類型如RunTime、Source等

  • 解析注解的生成器

在Dart中我們一般使用source_gen中的GeneratorForAnnotation,該類繼承自Generator這個跟Java APT中的processor職責類似,需要在GeneratorForAnnotation的泛型中填入我們需要處理的注解

詳解Dart中如何通過注解生成代碼
  • 觸發生成器的Builer

有了上面的生成注解的生成器,我們還需要Builder來觸發

詳解Dart中如何通過注解生成代碼
  • 建立配置檔案 build.yaml
詳解Dart中如何通過注解生成代碼
  • 運作builder

由于Flutter 禁用了dart:mirror無法使用反射,是以隻能在通過指令在編譯期觸發,執行如下指令,将會看到生成的代碼

詳解Dart中如何通過注解生成代碼
詳解Dart中如何通過注解生成代碼

是不是感受到了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配置感受一下

詳解Dart中如何通過注解生成代碼

build.yaml 配置的資訊,最終都會被 build_config.dart 中的 BuildConfig 類讀取到。

關于參數說明,目前也沒有太多資料,這裡推薦官方說明

build_config

,通過build_config包下的Builde_Config解析

解析入口如下

詳解Dart中如何通過注解生成代碼

從build_config.dart中可以看到,主要解析4個大的部分,下面将挑選常用的2個進行分析

詳解Dart中如何通過注解生成代碼

4.1.1 targets

在 build_target.dart#BuildTarget 可以看到支援屬性的描述,其中有個builder屬性使用的比較多

詳解Dart中如何通過注解生成代碼

在TargetBuilderConfig中有3個常用的屬性

  • enable

目前builder是否生效

  • generate_for

這個屬性比較重要,可以決定針對那些檔案/檔案夾做掃描,或者排除哪些檔案

input_set.dart

,使用如下

詳解Dart中如何通過注解生成代碼

在json_seriable的

build.yaml

中也可以看到它的yaml檔案中對generate_for屬性的使用

  • options

這個屬性可以允許你以鍵值對形式攜帶一些配置資料到代碼生成器中,對應的是BuildOption參數,下面在解讀builder時候會再次講述

4.1.2 builder

來一個builder

詳解Dart中如何通過注解生成代碼
BuilderOptions可以提取到上面的option屬性配置

在build.yaml檔案中描述如上,

Map 即 BuilderDefinition 資訊,下面将介紹一下常用的配置

詳解Dart中如何通過注解生成代碼

更多配置可以參考

builder_definition.dart

其中有2個重要的屬性單獨解釋一下

  • run_before

可以指定builder的運作順序,如果幾個buidler有互相依賴可以,比如在阿裡的路由架構

annotation_route

中就使用到了這個屬性,可以看看其

yaml

檔案,主要在路由架構中使用到了mustache4dart需要收集路由資訊來填充模闆,它的解法是使用兩個builder,一個用來收集資訊(routeWriteBuilder),收集完之後給另一個builder(routeBuilder)結合mustache4dart模闆來生成需要的路由表,具體可以參考其

route_generator.dart
  • auto_apply
詳解Dart中如何通過注解生成代碼

看文字可能了解起來可能有點晦澀,搞個圖來解釋一下,比如上圖 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核心類介紹

詳解Dart中如何通過注解生成代碼

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是單注解處理器例如

詳解Dart中如何通過注解生成代碼

由于analyser提供了文法節點的抽象元素Element和其metadata字段,對應ElementAnnotation,注解生成器可以檢查元素的metadata類型是否比對聲明的注解類型,進而找出被注解的元素及元素所在上下文的資訊,然後将這些資訊包裝給使用者。

核心方法generateForAnnotatedElement

例如我們有這樣一段注解代碼

詳解Dart中如何通過注解生成代碼

從上面可以看出主要覆寫了generateForAnnotatedElement方法,有三個關鍵參數

  • Element element

被 annotation 所修飾的元素,通過它可以擷取到元素的name、metadata、可見性等等。

詳解Dart中如何通過注解生成代碼

更多api可以檢視

element
關于toplevel注解

前文提到隻能攔截到toplevel級别的元素,是以class内部的方法其實都沒有掃描到,這是由于dart 檔案是不像java,一個檔案隻能對應一個類,dart檔案可以是function,也是是class或者其他,是以隻能預設攔截到top-level級别的,後面需要開發者自己手動處理,比如ClassElement提供了 methods、fields來給開發者進一步處理注解的機會,下面展示了解析類中的方法,屬性也是類似的

詳解Dart中如何通過注解生成代碼

Element除了ClassElementImpl外還有多個派生如 FunctionElementImpl、ParamElementImpl等,具體可以自行查閱。

  • ConstantReader annotation

表示注解對象,通過它可以提取到注解相關資訊以及參數值

有兩個關鍵方法

• read

• peek

不同之處在于,如果read方法讀取了不存在的參數名,會抛出異常,peek則不會,而是傳回null。

  • BuildStep buildStep

這一次建構的資訊,通過它可以擷取到一些輸入輸出資訊,例如輸入檔案名等

4.2.3核心代碼分析

source_gen也是從build庫的Builder封裝而來

詳解Dart中如何通過注解生成代碼

source_gen根據Builder實作自己的的

_Builder

,根據不同的特點派生出 SharedPartBuilder、LibraryBuilder、PartBuilder

詳解Dart中如何通過注解生成代碼

這裡面有個核心的 Generator

詳解Dart中如何通過注解生成代碼

在 Builder 運作時,會調用 Generator 的 generate方法,并傳入兩個重要的參數:

  • library 可以擷取源代碼資訊以及注解資訊
  • buildStep 它表示建構過程中的一個步驟,通過它,我們可以擷取一些檔案的輸入輸出資訊

其中library 包含的源碼資訊是一個個的 Element 元素,Element隻是抽象類,具體還是一個個ClassElementImpl、FuncationElementImpl等。

source_gen實作了該類 GeneratorForAnnotation

詳解Dart中如何通過注解生成代碼

其中 第2點中library.annotatedWith(typeChecker)跟進去看下

詳解Dart中如何通過注解生成代碼

5、關于代碼生成

  • 純字元串拼接

使用三引号文法,這種隻能解決一些低級生成

  • mustach

預制模闆,通過一定的規則,提取資訊之後填充資訊到模闆中,一個典型的例子如下

詳解Dart中如何通過注解生成代碼

學習成本較低(

了解mustach更多規則

),适合一些固定格式的代碼生成,比如路由表,阿裡的annotation_route架構就是采用這個,可以看下它的模闆

tpl

詳解Dart中如何通過注解生成代碼

然後使用了2個生成器,一個用來采集資訊,另一個用來将采集後的資訊注入到mustach模闆中

詳解Dart中如何通過注解生成代碼

非常強大,玩過java注解生成代碼的朋友一定熟悉javapoet,二者非常類似,code_builder可以細分為表達式、語句、函數、類等等,就是學習成本比較高,需要按照它的文法去生成對應的代碼,比如生成一個類

詳解Dart中如何通過注解生成代碼

生成一個表達式

詳解Dart中如何通過注解生成代碼

更多技巧需要看下源碼去學習使用。

6、與Java注解生成代碼對比

詳解Dart中如何通過注解生成代碼

7、小結

本文初步探索了在Dart通過注解生成代碼的技術,比起java的apt,沒有運作時反射用起來還是有點點麻煩,需要手動執行build,而且各種繁瑣的builder配置,讓人感覺晦澀難懂,生成代碼的技巧也跟java有着異曲同工之妙,需要借助一些外力比如mustach,code_builder等。這種技術給我們在解決一些例如路由,模闆代碼、動态代理等,多了一種處理手段,其他更多的使用場景需要我們去開發中慢慢探索。

參考