详解Dart中如何通过注解生成代码
作者:閑魚技術(shù)-龍湫
1、背景
最近在項(xiàng)目中使用到了Dart中的注解代碼生成技術(shù),這跟之前Java中APT+JavaPoet生成代碼那套技術(shù)還是有一些不同的地方,比如
- Flutter中在禁用了dart:mirror,無法使用反射情況下如何得到類相關(guān)信息?
- Dart的文件不限制是class,可以是function、class,因而在注解掃描的范圍不同的情況下如何拿到層層信息而不僅僅是toplevel信息?
- 提取到注解信息時(shí)又是如何生成復(fù)雜的模板代碼?
在Flutter中究竟是如何上面的問題呢?下面將一步步揭開這神秘的面紗。
2、一個(gè)簡(jiǎn)單的例子
先從一個(gè)簡(jiǎn)單的例子感受下dart中如何通過注解生成代碼
- 聲明一個(gè)注解,并使用注解
在Dart中構(gòu)造器用const修飾就好,可以看出Dart的注解聲明起來比較簡(jiǎn)單,不像java中還得有運(yùn)行類型如RunTime、Source等
- 解析注解的生成器
在Dart中我們一般使用source_gen中的GeneratorForAnnotation,該類繼承自Generator這個(gè)跟Java APT中的processor職責(zé)類似,需要在GeneratorForAnnotation的泛型中填入我們需要處理的注解
- 觸發(fā)生成器的Builer
有了上面的生成注解的生成器,我們還需要Builder來觸發(fā)
- 創(chuàng)建配置文件 build.yaml
- 運(yùn)行builder
由于Flutter 禁用了dart:mirror無法使用反射,因此只能在通過命令在編譯期觸發(fā),執(zhí)行如下命令,將會(huì)看到生成的代碼
是不是感受到了Dart注解生成代碼的奇特之處了,有像Java中AnnotationProcessor Tool的Generator,但是又多了Builder和build.yaml,那么這些是如何相互配合運(yùn)行生成注解的呢?
3、宏觀概覽
使用望遠(yuǎn)鏡宏觀概覽整個(gè)過程
當(dāng)我們使用build_runner的 build之后 觸發(fā)build,會(huì)去讀取build.yaml文件的配置信息,這個(gè)信息最終會(huì)被
build_config.dart中的BuildConfig類讀取到,然后通過讀取到builder,上面例子的testBuilder,觸發(fā)了其中的注解生成器(TestGenerator),來對(duì)抽象語法樹進(jìn)行信息提取(由于source_gen封裝了語法分析庫(kù)analysis和資源處理庫(kù)build,這里實(shí)際上是屏蔽了語法分析過程),跟java一樣都是一個(gè)個(gè)Element,具體可以看下代碼的實(shí)現(xiàn)類
歸納一下主要有以下個(gè)核心部分:
用戶觸發(fā) - 文件掃描 - 詞法分析 - 注解提取 - 代碼生成
4、微觀探索
再使用放大鏡仔仔細(xì)細(xì)研究一下其中的細(xì)節(jié):
4.1 build.yaml配置
在Java中我們使用谷歌提供的AutoService注解來生成META-INF/services/javax.annotation.processing.Processor 文件關(guān)聯(lián)注解處理器,但是Flutter中的dart注解只能在編譯期做文章,因此需要一個(gè)配置告訴編譯器,觸發(fā)哪些builder,對(duì)應(yīng)的就是build.yaml文件,
先看一個(gè)build.yaml配置感受一下
build.yaml 配置的信息,最終都會(huì)被 build_config.dart 中的 BuildConfig 類讀取到。
關(guān)于參數(shù)說明,目前也沒有太多資料,這里推薦官方說明build_config,通過build_config包下的Builde_Config解析
解析入口如下
從build_config.dart中可以看到,主要解析4個(gè)大的部分,下面將挑選常用的2個(gè)進(jìn)行分析
4.1.1 targets
在 build_target.dart#BuildTarget 可以看到支持屬性的描述,其中有個(gè)builder屬性使用的比較多
在TargetBuilderConfig中有3個(gè)常用的屬性
- enable
當(dāng)前builder是否生效
- generate_for
這個(gè)屬性比較重要,可以決定針對(duì)那些文件/文件夾做掃描,或者排除哪些文件 input_set.dart,使用如下
在json_seriable的build.yaml中也可以看到它的yaml文件中對(duì)generate_for屬性的使用
- options
這個(gè)屬性可以允許你以鍵值對(duì)形式攜帶一些配置數(shù)據(jù)到代碼生成器中,對(duì)應(yīng)的是BuildOption參數(shù),下面在解讀builder時(shí)候會(huì)再次講述
4.1.2 builder
來一個(gè)builder
BuilderOptions可以提取到上面的option屬性配置
在build.yaml文件中描述如上,
Map 即 BuilderDefinition 信息,下面將介紹一下常用的配置
更多配置可以參考builder_definition.dart
其中有2個(gè)重要的屬性單獨(dú)解釋一下
- run_before
可以指定builder的運(yùn)行順序,如果幾個(gè)buidler有互相依賴可以,比如在阿里的路由框架annotation_route中就使用到了這個(gè)屬性,可以看看其yaml文件,主要在路由框架中使用到了mustache4dart需要收集路由信息來填充模板,它的解法是使用兩個(gè)builder,一個(gè)用來收集信息(routeWriteBuilder),收集完之后給另一個(gè)builder(routeBuilder)結(jié)合mustache4dart模板來生成需要的路由表,具體可以參考其route_generator.dart
- auto_apply
看文字可能理解起來可能有點(diǎn)晦澀,搞個(gè)圖來解釋一下,比如上圖 libB中使用了注解功能:
- 當(dāng)我們將auto_apply設(shè)置成dependents時(shí):
如果 注解package 是直接依賴在 libB 上的,那么只能在 libB 上正常使用注解,雖然 頂層Package 包依賴了 libB,但是依然無法正常使用該注解
- 當(dāng)我們將auto_apply設(shè)置成all_packages時(shí):
如果 注解package 是直接依賴在 libB 上的,那么在 libB 和 頂層Package上都能正常使用注解
- 當(dāng)我們將auto_apply設(shè)置成root_package時(shí):
如果 注解package 是直接依賴在 libB 上的,那么只能在頂層 Package 上正常使用注解,雖然是 libB 上做的依賴,但是就是不能用,不過 注解package 是直接依賴在 頂層Package 上的時(shí)候,不管 auto_apply 設(shè)置的是 dependents、all_packages 或者是 root_package 時(shí),其實(shí)都是能正常使用的
4.2 關(guān)于source_gen
4.2.1 簡(jiǎn)介
了解完了基本配置的yaml文件之后,不得不提source_gen這個(gè)強(qiáng)大的庫(kù),
source_gen基于官方的 analysis/build提供了一系列友好的封裝,source_gen 基于 analyzer 和 build 庫(kù),其中
- build庫(kù)主要是資源文件的處理
- analyser庫(kù)是對(duì)dart文件生成語法結(jié)構(gòu)
source_gen主要提處理dart源碼,可以通過注解生成代碼。
4.2.2核心類介紹
source_gen從build庫(kù)提供的Builder派生出自己的_builder,并且封裝了3個(gè)
Builder (builder.dart)
|_Builder (builder.dart)
|-LibraryBuilder (builder.dart)|-SharedPartBuilder (builder.dart)|-PartBuilder (builder.dart)? SharedPartBuilder
生成.g.dart文件,類似json_seriable一樣,使用地方需要用是part of引用,這樣有個(gè)最大的好處就是引用問題不需要過于關(guān)注,要注意的是,需要使用 source_gen|combining_builder,它會(huì)將所有.g文件進(jìn)行合并。
? LibraryBuilder
生成獨(dú)立的文件
? PartBuilder
自定義part文件
生成器Generator
并且source_gen封裝了一套Generator,以上的buidler接收Generator的集合,收集Generator的產(chǎn)出生成一份文件,Generator只是一個(gè)抽象類,具體實(shí)現(xiàn)類是GeneratorForAnnotation,默認(rèn)只能攔截到top-level級(jí)別的(后面會(huì)解釋)元素,會(huì)被注解生成器接受一個(gè)指定注解類型,即GeneratorForAnnotation是單注解處理器例如
由于analyser提供了語法節(jié)點(diǎn)的抽象元素Element和其metadata字段,對(duì)應(yīng)ElementAnnotation,注解生成器可以檢查元素的metadata類型是否匹配聲明的注解類型,從而找出被注解的元素及元素所在上下文的信息,然后將這些信息包裝給使用者。
核心方法generateForAnnotatedElement
例如我們有這樣一段注解代碼
從上面可以看出主要覆寫了generateForAnnotatedElement方法,有三個(gè)關(guān)鍵參數(shù)
- Element element
被 annotation 所修飾的元素,通過它可以獲取到元素的name、metadata、可見性等等。
更多api可以查看element
關(guān)于toplevel注解
前文提到只能攔截到toplevel級(jí)別的元素,因此class內(nèi)部的方法其實(shí)都沒有掃描到,這是由于dart 文件是不像java,一個(gè)文件只能對(duì)應(yīng)一個(gè)類,dart文件可以是function,也是是class或者其他,因此只能默認(rèn)攔截到top-level級(jí)別的,后面需要開發(fā)者自己手動(dòng)處理,比如ClassElement提供了 methods、fields來給開發(fā)者進(jìn)一步處理注解的機(jī)會(huì),下面展示了解析類中的方法,屬性也是類似的
Element除了ClassElementImpl外還有多個(gè)派生如 FunctionElementImpl、ParamElementImpl等,具體可以自行查閱。
- ConstantReader annotation
表示注解對(duì)象,通過它可以提取到注解相關(guān)信息以及參數(shù)值
有兩個(gè)關(guān)鍵方法
? read
? peek
不同之處在于,如果read方法讀取了不存在的參數(shù)名,會(huì)拋出異常,peek則不會(huì),而是返回null。
- BuildStep buildStep
這一次構(gòu)建的信息,通過它可以獲取到一些輸入輸出信息,例如輸入文件名等
4.2.3核心代碼分析
source_gen也是從build庫(kù)的Builder封裝而來
source_gen根據(jù)Builder實(shí)現(xiàn)自己的的_Builder,根據(jù)不同的特點(diǎn)派生出 SharedPartBuilder、LibraryBuilder、PartBuilder
這里面有個(gè)核心的 Generator
在 Builder 運(yùn)行時(shí),會(huì)調(diào)用 Generator 的 generate方法,并傳入兩個(gè)重要的參數(shù):
- library 可以獲取源代碼信息以及注解信息
- buildStep 它表示構(gòu)建過程中的一個(gè)步驟,通過它,我們可以獲取一些文件的輸入輸出信息
其中l(wèi)ibrary 包含的源碼信息是一個(gè)個(gè)的 Element 元素,Element只是抽象類,具體還是一個(gè)個(gè)ClassElementImpl、FuncationElementImpl等。
source_gen實(shí)現(xiàn)了該類 GeneratorForAnnotation
其中 第2點(diǎn)中l(wèi)ibrary.annotatedWith(typeChecker)跟進(jìn)去看下
5、關(guān)于代碼生成
- 純字符串拼接
使用三引號(hào)語法,這種只能解決一些低級(jí)生成
- mustach
預(yù)制模板,通過一定的規(guī)則,提取信息之后填充信息到模板中,一個(gè)典型的例子如下
學(xué)習(xí)成本較低(了解mustach更多規(guī)則),適合一些固定格式的代碼生成,比如路由表,阿里的annotation_route框架就是采用這個(gè),可以看下它的模板tpl,
然后使用了2個(gè)生成器,一個(gè)用來采集信息,另一個(gè)用來將采集后的信息注入到mustach模板中
- code_builder
非常強(qiáng)大,玩過java注解生成代碼的朋友一定熟悉javapoet,二者非常類似,code_builder可以細(xì)分為表達(dá)式、語句、函數(shù)、類等等,就是學(xué)習(xí)成本比較高,需要按照它的語法去生成對(duì)應(yīng)的代碼,比如生成一個(gè)類
生成一個(gè)表達(dá)式
更多技巧需要看下源碼去學(xué)習(xí)使用。
6、與Java注解生成代碼對(duì)比
7、小結(jié)
本文初步探索了在Dart通過注解生成代碼的技術(shù),比起java的apt,沒有運(yùn)行時(shí)反射用起來還是有點(diǎn)點(diǎn)麻煩,需要手動(dòng)執(zhí)行build,而且各種繁瑣的builder配置,讓人感覺晦澀難懂,生成代碼的技巧也跟java有著異曲同工之妙,需要借助一些外力比如mustach,code_builder等。這種技術(shù)給我們?cè)诮鉀Q一些例如路由,模板代碼、動(dòng)態(tài)代理等,多了一種處理手段,其他更多的使用場(chǎng)景需要我們?nèi)ラ_發(fā)中慢慢探索。
參考
- 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)
原文鏈接:https://developer.aliyun.com/article/769533?
版權(quán)聲明:本文內(nèi)容由阿里云實(shí)名注冊(cè)用戶自發(fā)貢獻(xiàn),版權(quán)歸原作者所有,阿里云開發(fā)者社區(qū)不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。具體規(guī)則請(qǐng)查看《阿里云開發(fā)者社區(qū)用戶服務(wù)協(xié)議》和《阿里云開發(fā)者社區(qū)知識(shí)產(chǎn)權(quán)保護(hù)指引》。如果您發(fā)現(xiàn)本社區(qū)中有涉嫌抄襲的內(nèi)容,填寫侵權(quán)投訴表單進(jìn)行舉報(bào),一經(jīng)查實(shí),本社區(qū)將立刻刪除涉嫌侵權(quán)內(nèi)容。總結(jié)
以上是生活随笔為你收集整理的详解Dart中如何通过注解生成代码的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 客户端稳定性优化实战,Crash率最高下
- 下一篇: 如何保证NFS文件锁的一致性?