詳解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
更多配置可以參考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等。這種技術給我們在解決一些例如路由,模板程式碼、動態代理等,多了一種處理手段,其他更多的使用場景需要我們去開發中慢慢探索。
參考
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69900359/viewspace-2709628/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 詳解Dart中如何通過註解生成程式碼Dart
- 如何生成文字: 透過 Transformers 用不同的解碼方法生成文字ORM
- Flutter 註解處理及程式碼生成Flutter
- GetX程式碼生成IDEA外掛,超詳細功能講解(透過現象看本質)Idea
- 如何透過 ABAP 程式碼給 SAP OData 後設資料增添註解試讀版
- Spring 註解學習 詳細程式碼示例Spring
- [Flutter]Dart Future詳解FlutterDart
- java程式碼自動生成帶swagger3註解JavaSwagger
- [Flutter翻譯]【第2部分】在Dart中生成程式碼:註解、source_gen和build_runnerFlutterDartUI
- 如何透過babel去操作ast, 並生成對應的程式碼。BabelAST
- dart類詳細講解Dart
- Java註解詳解Java
- Lombok 註解詳解Lombok
- @FeignClient註解詳解client
- Java 註解詳解Java
- 如何在macOS中透過應用程式視窗浮動註釋Mac
- Flutter (三) Dart 語言基礎詳解 (非同步,生成器,隔離,後設資料,註釋)FlutterDart非同步
- hyperf 註解文件生成
- Java註解(Annotation)詳解Java
- Java註解詳解「註解專案實戰」Java
- mybatis中註解對映SQL示例程式碼MyBatisSQL
- 使用自定義註解透過BeanPostProcessor實現策略模式Bean模式
- 一文詳解如何在 ChengYing 中透過產品線部署一鍵提升效率
- IDEA中如何設定檔案頭註釋和方法註釋(詳解)Idea
- SwaggerAPI註解詳解,以及註解常用引數配置SwaggerAPI
- Dart語言詳解(一)——詳細介紹Dart
- 透過程式碼例項簡單瞭解Python sys模組Python
- 詳解Android Gradle生成位元組碼流程AndroidGradle
- 【SpringBoot系列】SpringBoot註解詳解Spring Boot
- Spring IoC 公共註解詳解Spring
- 如何透過SqlResultSetMapping和NamedNativeQuery生成DTO?SQLAPP
- java中如何自定義註解Java
- Java ”框架 = 註解 + 反射 + 設計模式“ 之 註解詳解Java框架反射設計模式
- 【leetcode】leetcode22括號生成通過程式碼及題解LeetCode
- 電子郵件地址註冊過程詳解
- Java註解最全詳解(超級詳細)Java
- java如何讓程式碼變得優雅——自定義註解Java
- 詳解Python中的程式Python