Dart 注解与代码生成
Dart语言在Flutter中禁用了反射,因此我们无法使用反射去实现一些框架功能,甚至无法去识别注解,为此Dart提供了另一套方案,就是编译前通过解析器,静态的生成代码。
入门示例
如果我们想使用注解来生成Dart代码,那么我们至少要创建两个Dart包。一个用来提供注解类,一个用来生成代码。
先看一个简单例子,假设我们现在想开发一个基于Dart注解的ORM库,那么分别创建两个包,命名为:orm
、orm_generator
在相同目录下,使用Dart命令分别先创建两个库模版工程,然后再创建一个example
工程作为Demo来测试我们编写的orm
库:
dart create -t package-simple orm
dart create -t package-simple orm_generator
dart create -t console-simple example
目录结构如下:
|--dir
|--orm
|--orm_generator
|--example
声明注解
在orm
包中,创建lib/src/annotations.dart
文件:
/// 声明一个具有常量构造方法的类
class Table {
final String? name;
const Table({this.name});
}
我们知道,在Dart语言中,任意一个具有const
构造方法的类,都可以作为注解使用。
注意,自动生成的模版工程,会在lib
下创建一个与工程同名的导包文件orm.dart
,我们可以在里面导出src
下的各种文件:
library orm;
export 'src/annotations.dart';
这样一来,其他人就可以仅使用一句简洁的导入语句来使用注解类:import 'package:orm/orm.dart';
工程结构:
使用注解
这里也就是要依赖注解。进入example
工程,就像使用其他第三方库一样,在pubspec.yaml
文件中配置依赖:
dependencies:
orm:
path: ../orm/
注意,这里配置的是本地依赖,如果我们的库最终上传到Dart的
pub
仓库,就要改成版本号依赖了。
创建bin/example.dart
文件:
import 'package:orm/orm.dart';
@Table(name: 'students')
class Student{
String? _name;
int? _age;
}
可以看到,Dart中注解的使用十分简单,使用@
符号引用我们写的注解,并给注解的构造传参。运行代码好像什么也不会发生,这是因为我们还没有处理这些注解,后面我们就会扫描这些注解,并生成相应的代码。
现在应该思考的是,我们想用这些注解干嘛?
显然的,我们在Student
类上使用了注解,其实是想将Student
类和数据库中的students
表映射起来,Student
中的字段就对应表中的字段。我们希望避免使用SQL语言操作数据库,通过对类的操作就能自动实现对数据库的操作,这就是ORM的功能。
在Dart上要想实现这一点,只能通过注解来自动生成大量模版代码,以达到代码复用,简化开发的目的。我们这里只简单的模拟这个需求,转到具体实现上,我们希望根据上述注解,生成如下代码:
class StudentEntity {
String? _name;
int? _age;
String? get name => _name;
set name(String? name) {
this._name = name;
}
int? get age => _age;
set age(int? age) {
this._age = age;
}
void save() {
print('execute sql:[insert into students values ($_name,$_age)]');
}
}
这里save
方法仅简单模拟执行SQL语句。
工程结构:
生成代码
上面我们简述了两头的环节,现在具体来说一说中间的环节,也是关键的一环,如何扫描识别注解,并生成Dart模版代码。
1. 配置依赖
进入orm_generator
包,在pubspec.yaml
文件中添加依赖:
dependencies:
build:
source_gen:
orm:
path: ../orm/
dev_dependencies:
build_runner:
2. 创建元素访问器
创建lib/src/visitor.dart
:
import 'package:analyzer/dart/element/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
/// 派生自己的元素访问器用于扫描
class MyVisitor extends SimpleElementVisitor<void> {
String? className;
final fields = <String, dynamic>{};
@override
void visitConstructorElement(ConstructorElement element) {
className = element.type.returnType.toString();
}
@override
void visitFieldElement(FieldElement element) {
final elementType = element.type.toString();
fields[element.name] = elementType;
}
}
这里的元素访问器主要用来扫描被注解的类,在此例中,也就是Student
类。SimpleElementVisitor
提供了许多有用的扫描方法,我们自定义的类仅实现了其中两个,visitConstructorElement
和visitFieldElement
分别用来扫描Student
的构造方法和成员字段。
扫描构造方法时,通过它的返回类型,可以拿到该类的类名,这里将其保存到className
变量中。扫描类成员变量时,将成员变量名和变量类型保存到fields
字典中。
3. 创建代码生成器
创建lib/src/table_generator.dart
文件
import 'package:analyzer/dart/element/element.dart';
import 'package:build/src/builder/build_step.dart';
import 'package:orm/orm.dart';
import 'package:source_gen/source_gen.dart';
import '../generators.dart';
class TableGenerator extends GeneratorForAnnotation<Table>{
@override
generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
final visitor = MyVisitor();
// 传入我们自己定义的元素访问器
element.visitChildren(visitor);
final codeBuffer = StringBuffer();
for (final field in visitor.fields.keys) {
codeBuffer.writeln("${visitor.fields[field]} $field ;");
}
codeBuffer.writeln();
// 遍历类的成员字段
for (final field in visitor.fields.keys) {
if(field.startsWith('_')){
// 去除私有变量的下划线
final variable = field.replaceFirst('_', '');
//生成: String get name => _name;
codeBuffer.writeln(
"${visitor.fields[field]} get $variable => $field;");
// 生成: set name(String name) {
// this._name = name;
// }
codeBuffer
.writeln('set $variable(${visitor.fields[field]} $variable) {');
codeBuffer.writeln('this.$field = $variable;');
codeBuffer.writeln('}');
}
}
var ann = annotation.peek('name');
var tableName = ann != null ? ann.stringValue : visitor.className;
var sb = StringBuffer();
for (final field in visitor.fields.keys) {
sb.write('\$$field,');
}
var parameter = sb.toString().substring(0,sb.length-1);
var code = '''
class ${visitor.className}Entity{
$codeBuffer
void save(){
print('execute sql:[insert into $tableName values ($parameter)]');
}
}
''';
return code;
}
}
这里我们自定义类继承自GeneratorForAnnotation
,它的泛型指定为我们想要扫描的注解类型。这个类的名字非常清晰,表明通过注解来生成代码。
我们必须实现该类的generateForAnnotatedElement
方法,该方法的返回值是一个String
,也就是我们想要生成的Dart源码。这个方法的实现非常简单,主要就是在做字符串的拼接,拼接完成之后直接返回。这里值得注意的有两个地方,首先我们实例化之前写好的元素访问器,并通过element.visitChildren(visitor);
设置我们的访问器,访问器中的逻辑主要实现对被注解类的扫描。然后将我们需要的数据解析出来,接下来就可以拼接生成Dart源码了。另一地方是我们使用了该方法的第二个参数ConstantReader
,通过调用它的peek
方法来查询注解中传入的参数name
,因为这里name
是可选的,所以必须判空。
4. 创建构建器
上面的逻辑虽然都写好了,但是显然还缺乏一个入口,谁去调用上面的一系列逻辑呢?此外,也还缺乏文件生成的逻辑,别忘了generateForAnnotatedElement
方法只是返回了字符串,并没有涉及在磁盘上写文件的操作。这时候,我们还需要创建一个构建器,由构建器来完成一系列的调用。
创建lib/src/builder.dart
文件:
import 'package:build/build.dart';
import 'package:orm_generator/src/table_generator.dart';
import 'package:source_gen/source_gen.dart';
Builder generateTable(BuilderOptions options) =>
SharedPartBuilder([TableGenerator()], 'table_generator');
这里我们定义了一个全局函数,它的参数是BuilderOptions
类型,返回值必须是一个构建器Builder
类型。注意,此处我们不用自定义一个构建器,这是因为我们依赖了source_gen
库,这个库为我们实现了三个Builder
的子类,这里使用了其中的SharedPartBuilder
,省去了我们许多工夫。创建SharedPartBuilder
对象时需要两个参数,第一个是生成器列表,第二个是传一个唯一标识符作为ID。
最后,我们还需要在工程的根目录下创建一个配置文件build.yaml
:
targets:
$default:
builders:
orm_generator|orm:
enabled: true
builders:
orm_generator:
target: ":orm_generator"
import: "package:orm_generator/generators.dart"
builder_factories: ["generateTable"]
build_extensions: { ".dart": [".g.dart"] }
auto_apply: dependents
build_to: cache
applies_builders: ["source_gen|combining_builder"]
5. 生成
在example
工程的配置文件中添加依赖:
dev_dependencies:
build_runner:
orm_generator:
path: ../orm_generator/
在使用注解的地方,添加part of
引用:
import 'package:orm/orm.dart';
part 'example.g.dart';
@Table(name: 'students')
class Student{
String? _name;
int? _age;
}
好了,到这来只需要在工程下执行命令即可生成代码:
# dart工程执行
pub run build_runner build
# flutter工程执行
flutter pub run build_runner build
如果重复生成,可能需要添加参数删除上一次生成的文件:
pub run build_runner build --delete-conflicting-outputs
# 或者
flutter pub run build_runner build --delete-conflicting-outputs
在同目录下生成了example.g.dart
文件,打开文件查看,生成的代码与我们预期的一致。
工程结构:
高级代码生成
以上,我们使用了最简单的方式,字符串拼接来生成Dart源码。这种方式不仅简单直观,也可谓简陋,在复杂需求的情况下,极大的缺乏灵活性。实际上官方为我们提供了一个code_builder库专门用来生成源码,它是通过API的方式来生成源码。
在orm_generator
包中添加对该库的依赖:
dev_dependencies:
build_runner:
code_builder:
我们现在就使用code_builder
库来改造上述示例:
import 'package:analyzer/dart/element/element.dart';
import 'package:build/src/builder/build_step.dart';
import 'package:orm/orm.dart';
import 'package:source_gen/source_gen.dart';
import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';
import '../generators.dart';
class TableGenerator extends GeneratorForAnnotation<Table>{
@override
generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
final visitor = MyVisitor();
// 访问元素的所有子元素,没有特定的顺序
element.visitChildren(visitor);
// 读取注解参数
var ann = annotation.peek('name');
var tableName = ann != null ? ann.stringValue : visitor.className;
var sb = StringBuffer();
for (final field in visitor.fields.keys) {
sb.write('\$$field,');
}
var parameter = sb.toString().substring(0,sb.length-1);
// 创建一个类
var clz = Class((b){
b.name = '${visitor.className}Entity';
// 生成类的成员变量
Iterable<Field> fields = visitor.fields.keys.map((e) => Field((f) {
f.name = e;
f.type = refer(visitor.fields[e]);
}));
b.fields.addAll(fields);
// 生成 setter 和 getter方法
fields.forEach((e) {
final variable = e.name.startsWith('_')? e.name.replaceFirst('_', ''): e.name;
b.methods.add(Method((m){
m.name = variable;
m.lambda = false;
m.type = MethodType.setter;
m.requiredParameters.add(Parameter((p) {
p.name = variable;
p.type = e.type;
}));
m.body = Code("${e.name} = $variable;");
}));
b.methods.add(Method((m){
m.name = variable;
m.lambda = true;
m.returns = e.type;
m.type = MethodType.getter;
m.body = Code(e.name);
}));
});
// 生成 save 方法
b.methods.add(Method((m){
m.name = 'save';
m.lambda = false;
m.returns = refer('void');
m.body = Code("print('execute sql:[insert into $tableName values ($parameter)]');");
}));
});
final emitter = DartEmitter();
// 对生成的代码格式化后再返回
return DartFormatter().format(clz.accept(emitter).toString());
}
}
运行后示例,可见与我们通过字符串拼接生成的源码相同。更多用法,请查看code_builder的 API文档
原理简述
首先看一下我们使用到的几个库:
- source_gen:它是对另外两个库analyzer 和 build的封装。这两个库更加底层,使用起来更麻烦,
source_gen
在这两者之上提供了更简单友好的API给我们使用。因此source_gen
并不是必须的,我们也可以直接使用底层的接口来实现。 - build_runner:为我们提供了命令行调用,即提供了
pub run build_runner
命令。它主要读取我们编写的build.yaml
配置文件,从而运行指定的构建器。 - code_builder:以API调用的方式生成Dart源码,避免直接的字符串拼接。
具体来说,source_gen
主要为我们提供了两个抽象生成器:
Generator
:完全控制生成器,继承该类,可访问代码所有元素,获得完全控制GeneratorForAnnotation<T>
:简单生成器,主要解析被注解的类或其他成员
以及其实现的三个构建器:
SharedPartBuilder
:该构建会生成一个扩展名为:.g.dart
文件,使用的地方需要用part of
引用PartBuilder
:该构建可以生成自定义part
文件,即任意的文件名,例如:students.dart
LibraryBuilder
: 该构建可以生成一个单独的文件,即一个可以被导入的独立库。
整体流程如下图:
配置文件
我们在orm_generator
包中添加了一份配置文件:build.yaml
,该配置文件主要是指导build_runner
库如何启动构建。关于这些配置项,需要进行一些详细说明。
根据官方文档描述,该配置文件有5大部分,我们先来看一下最重要的两个部分,也是我们上面例子配置的两个部分,分别是
targets
:配置你要构建的目标。builders
:配置具体构建行为。
将包划分为构建目标
当对包中的文件子集构建时,包可以被分解成多个 目标。 目标是在build.yaml
的targets
部分配置。 每个目标的键构成该目标的名称。 目标可以用 '$definingPackageName:$targetname'
引用。 当目标名称与包名称匹配时,也可以只称为包名称。 每个包中的一个目标必须使用包名称,以便运行时默认使用它。
在build.yaml
文件中,这个目标可以用键$default
或包的名称来定义。
每个目标还可以包含以下键:
- sources:字符串列表或Map,可选。包中构成该目标的一组文件。文件是用glob语法指定的。如果使用一个字符串列表,则它们被视为
include
glob。如果使用Map 只能有include
和exclude
键。任何与include
键中的 glob 匹配且在exclude
中没有被排除的文件都被认为是目标的源。当include
被省略时,每个文件都被认为是匹配的。 - dependencies:字符串列表,可选。这个目标所依赖的其他目标。格式为
'$packageName:$targetName'
的字符串用于依赖包中的目标,或者$packageName
用于依赖包的默认目标。默认情况下,这是这个包所依赖的所有包名(来自pubspec.yaml
)。 - builders:Map,可选。
配置builders
每个目标可以指定一个builders
键,用于配置该目标的构建器。此值是一个Map。键的格式是'$packageName:$builderName'
。该配置可以有以下的键:
- enabled: 布尔型,可选。是否将构建器应用于此目标。如果你想要基于构建器的默认行为,省略此键
auto_apply
的配置。手动应用的构建器 (auto_apply: none
) 仅在目标使用enabled: True
指定构建器时才使用 - generate_for: 字符串列表或 Map,可选。 可以决定针对哪些文件或文件夹做扫描,或者排除哪些文件。极大的提供扫描的效率。
- options: Map类型,可选。可以允许你以键值对形式携带一些配置数据到代码生成器中,对应的是
BuildOption
参数,如上例的generateTable
函数参数 - dev_options:同
options
,当构建在开发模式下使用此键覆盖。 - release_options: 同
options
,当构建在发布模式下使用此键覆盖。
这其中最重要的是generate_for
配置,正确使用它能大大加速代码生成:
targets:
$default:
builders:
freezed:freezed:
generate_for:
include:
- lib/**.codegen.dart
以上指定只扫描带.codegen.dart
后缀名的文件,也可以配置只扫描某个文件夹。当然,我们还可以指定排除规则,不扫描哪些文件:
targets:
$default:
builders:
freezed:freezed:
generate_for:
exclude: ['**.g.dart','**.c.dart']
另需注意,我们可以配置全局global_options
去覆盖目标级别构建器的options
。这些选项在所有构建器默认值和目标级配置之后,在--define
命令行参之前应用。
配置构建器
构建器 在 build.yaml
的 builders
部分进行配置。这是一个构建器名称到配置的Map。每个构建器的配置可以包含以下键:
- import
- builder_factories
- build_extensions
- auto_apply
- required_inputs
- runs_before
- applies_builders
- build_to
- is_optional:可选的布尔值。指定 Builder 是否可以延迟运行。它的一个输出被后面的Builder请求时才执行。该选项很少见。默认值为
False
。 - defaults: 可选的。当用户未在其
builders
部分指定相应键时的默认值。可包含以下键:- generate_for
- options
- dev_options
- release_options
重要的配置说明:
配置示例:
builders:
my_builder:
import: "package:my_package/builder.dart"
builder_factories: ["myBuilder"]
build_extensions: {".dart": [".my_package.dart"]}
auto_apply: dependents
defaults:
release_options:
some_key: "Some value the users will want in release mode"
用法进阶
在上面的示例中,我们一直使用的是顶级注解,即注解添加到类上。有时候我们希望给类中的方法添加注解,这种情况下简单生成器GeneratorForAnnotation
是无法处理的,它只能识别顶级注解,也就是类上、全局函数等元素上的注解。解决这个问题,有两种思路:
- 先给类上添加一个顶级注解,然后再到类中方法上加注解。这样就能使用
GeneratorForAnnotation
先识别到被注解的类,然后遍历类中的所有成员,定位到被添加了注解的方法成员。 - 从
Generator
派生自定义的生成器
以上第一种思路显然不够灵活,如果只想注解方法,还得先给类加上一个注解,多此一举。现在我们来看一下如何自定义生成器,实现完全控制:
abstract class GeneratorForAnnotatedMethod<AnnotationType> extends Generator {
DartObject? getAnnotation(Element element) {
final annotations =
TypeChecker.fromRuntime(AnnotationType).annotationsOf(element);
if (annotations.isEmpty) {
return null;
}
if (annotations.length > 1) {
throw Exception(
"You tried to add multiple @$AnnotationType() annotations to the "
"same element (${element.name}), but that's not possible.");
}
return annotations.single;
}
@override
String generate(LibraryReader library, BuildStep buildStep) {
final values = <String>{};
for (final element in library.allElements) {
if (element is ClassElement && !element.isEnum) {
for (final method in element.methods) {
final annotation = getAnnotation(method);
if (annotation != null) {
values.add(generateForAnnotatedMethod(method, ConstantReader(annotation),
));
}
}
}
}
return values.join('\n');
}
String generateForAnnotatedMethod(MethodElement method, ConstantReader annotation);
}
以上先实现了一个能解析方法注解的生成器,主要是重写generate
方法,然后遍历所有的类元素,再遍历类中的成员方法,检查每个方法是否带有我们期望的注解。看一下使用示例:
class EventGenerator extends GeneratorForAnnotatedMethod<Event>{
@override
String generateForAnnotatedMethod(MethodElement method, ConstantReader annotation) {
var className = annotation.read('name').stringValue;
return 'class $className{}';
}
}
使用方法注解:
class Test{
@Event('UpEvent')
void func1(){}
@Event('DownEvent')
void func2(){}
}
附:Build 配置文件
官方提供了一个Build配置文件的文档,如下:
build.yaml
文件由 BuildConfig 对象描述。
这是一份关于build.yaml
格式的技术性文件,如果你想看例子或目标导向的文档,你可能想看看 build_config/README.md。
BuildConfig
key | value | default |
---|---|---|
targets | Map<String, BuildTarget> | 与包同名的单个目标 |
builders | Map<String, BuilderDefinition> | empty |
post_process_builders | Map<String, PostProcessBuilderDefinition> | empty |
global_options | Map<String, GlobalBuilderOptions> | empty |
additional_public_assets | List | empty |
BuildTarget
key | value | default |
---|---|---|
auto_apply_builders | bool | true |
builders | Map<BuilderKey, TargetBuilderConfig> | empty |
dependencies | List<TargetKey> | pubspec 中所有依赖项的所有默认目标 |
sources | InputSet | 所有默认 sources |
BuilderDefinition
key | value | default |
---|---|---|
builder_factories | List | none |
import | String | none |
build_extensions | Map<String, List | none |
auto_apply | AutoApply | AutoApply.none |
required_inputs | List | none |
runs_before | List<BuilderKey> | none |
applies_builders | List<BuilderKey> | none |
is_optional | bool | false |
build_to | BuildTo | BuildTo.cache |
defaults | TargetBuilderConfigDefaults | none |
PostProcessBuilderDefinition
key | value | default |
---|---|---|
builder_factory | String | none |
import | String | none |
input_extensions | List | none |
defaults | TargetBuilderConfigDefaults | none |
InputSet
注意,除了以下结构,你还可以提供一个List<String>
,这将被推断为下面的 include
键。
key | value | default |
---|---|---|
include | List | ** |
exclude | List | none |
TargetBuilderConfig
key | value | default |
---|---|---|
enabled | bool | true |
generate_for | InputSet | ** |
options | BuilderOptions | none |
dev_options | BuilderOptions | none |
release_options | BuilderOptions | none |
TargetBuilderConfigDefaults
key | value | default |
---|---|---|
generate_for | InputSet | ** |
options | BuilderOptions | none |
dev_options | BuilderOptions | none |
release_options | BuilderOptions | none |
GlobalBuilderOptions
key | value | default |
---|---|---|
options | BuilderOptions | none |
dev_options | BuilderOptions | none |
release_options | BuilderOptions | none |
BuilderOptions
一个任意的Map<String, dynamic>
的配置选项,由各个构建器公开。请参阅你正在配置的构建器的文档以获得指导。
AutoApply
value | meaning |
---|---|
none | 默认情况下不适用于任何包,必须明确 |
: : enabled.: | |
dependents | 适用于所有直接依赖于此的包 |
:: package.: | |
all_packages | 适用于图表中的所有包。 |
root_package | 仅适用于root(应用程序)包。 |
BuildTo
value | meaning |
---|---|
cache | 将所有文件写入缓存目录 |
source | 将所有文件直接写入源目录 |
TargetKey
一个target
标识符。一个目标键有两个部分,一个是package
,一个是name
。
为了构建一个键,用:
连接包和名称,例如,在foo
包中的 bar
目标将被这样引用foo:bar
。
有一个特殊的别名,那就是$default
目标。这指的是当前包中的默认目标(与包的名称相同)。
BuilderKey
一个 builder
的标识符。一个构建器有两个部分,一个是package
,一个是name
。
为了构建一个键,用 |
连接包和名字,例如,在foo
包中的bar
构建器将被这样引用 foo|bar
。
关注公众号:编程之路从0到1
或关注博主的视频课程