- 开发工具
- 代码生成
【免费下载链接】auto
A collection of source code generators for Java.
导读
@AutoBuilder是 AutoValue 项目提供的注解处理器,它能把一个接口或抽象类实现为"通用 Builder":Setter 方法按参数名累积值,build()(或任意命名的无参抽象方法)以这些值调用指定类的构造函数或静态方法。调用方无需关心参数顺序,参数还可以带默认值,调用前也能插入校验逻辑。阅读本文后,你将掌握@AutoBuilder的完整用法——包括ofClass/callMethod两个注解参数、Kotlin 互操作、泛型约束、参数默认值、Getter 方法与toBuilder()反向构造,以及参数名不可用时的应对方案,并理解注解处理器在底层的工作机制。
AutoBuilder 是什么
AutoBuilder 解决了这样一个问题:当你要创建某个类的实例时,直接调用构造函数或工厂方法要求调用方记住参数的确切顺序。AutoBuilder 让调用方通过具名 Setter 逐一设置参数值,最后统一交给build()方法调用构造函数或静态方法。
如果你熟悉 AutoValue Builder 的用法,那么 AutoBuilder 上手会很快:@AutoValue.Builder的 Setter 对应@AutoValue类中的 Getter 方法,而@AutoBuilder的 Setter 对应的是构造函数或静态方法的参数。除此之外,两者非常相似。
从注解定义来看,AutoBuilder.java 是一个@Target(ElementType.TYPE)、@Retention(RetentionPolicy.CLASS)的注解,注释明确说明:被注解的接口或抽象类将被实现为一个 Builder。实现这一过程的是注解处理器 AutoBuilderProcessor.java,它通过@SupportedAnnotationTypes(AUTO_BUILDER_NAME)注册为 javac 编译期插件,并以ISOLATING增量模式工作,生成名为AutoBuilder_<TypeName>的具体子类。
基础示例:调用构造函数
以下是一个最简单的例子,为Person类生成 Builder:
@AutoBuilder(ofClass = Person.class) abstract class PersonBuilder { static PersonBuilder personBuilder() { return new AutoBuilder_PersonBuilder(); } abstract PersonBuilder setName(String name); abstract PersonBuilder setId(int id); abstract Person build(); }使用方式:
Person p = PersonBuilder.personBuilder().setName("Priz").setId(6).build();这与直接new Person("Priz", 6)效果相同,但不需要知道构造函数参数的顺序。这里setName与setId是Setter 方法:调用builder.setName("Priz")会把值"Priz"记录给参数name,setId同理。build()方法则用之前设置好的参数调用Person构造函数。
仓库的编译测试 AutoBuilderCompilationTest.java 中给出了生成类的完整预期输出,可以看到生成的AutoBuilder_Baz_Builder使用字段保存每个属性,用一个set$0位掩码记录哪些属性已被设置,并在build()中检查是否遗漏必需属性:
public Baz build() { if (set$0 != 0x1 || this.aString == null) { StringBuilder missing = new StringBuilder(); if ((set$0 & 0x1) == 0) { missing.append(" anInt"); } if (this.aString == null) { missing.append(" aString"); } throw new IllegalStateException("Missing required properties:" + missing); } return new Baz(this.anInt, this.aString); }这段由处理器模板(见 autobuilder.vm 与 AutoBuilderTemplateVars.java)渲染出的代码,印证了文档中"缺少必需参数会抛IllegalStateException"的行为。
调用 Kotlin 构造函数
Kotlin 本身支持命名参数与默认参数,因此不太需要 Builder。但从 Java 代码构造 Kotlin data class 时,AutoBuilder 就很有用了。
给定这样一个 Kotlin data class:
class KotlinData(val level: Int, val name: String?, val id: Long = -1L)可以这样为其编写 Builder:
@AutoBuilder(ofClass = KotlinData.class) public abstract class KotlinDataBuilder { public static KotlinDataBuilder kotlinDataBuilder() { return new AutoBuilder_KotlinDataBuilder(); } public abstract KotlinDataBuilder setLevel(int x); public abstract KotlinDataBuilder setName(@Nullable String x); public abstract KotlinDataBuilder setId(long x); public abstract KotlinData build(); }这里的规则是:
- Kotlin 类型
String?对应 AutoBuilder 类中的@Nullable String,其中@Nullable是任何同名注解,例如org.jetbrains.annotations.Nullable; id参数有默认值-1L,如果调用方不调用setId,构建出的KotlinData的id字段就是-1L。
如果使用 kapt,还可以把 Builder 直接定义在 data class 内部:
class KotlinData(val level: Int, val name: String?, val id: Long = -1L) { @AutoBuilder // 无需 ofClass:默认就是包含它的类 interface Builder { fun setLevel(x: Int): Builder fun setName(x: String?): Builder fun setId(x: Long): Builder fun build(): KotlinData } fun toBuilder(): Builder = AutoBuilder_KotlinData_Builder(this) companion object { @JvmStatic fun builder(): Builder = AutoBuilder_KotlinData_Builder() } }这个例子用接口而非抽象类定义 Builder,两者都可以。Java 代码构造实例:
KotlinData k = KotlinData.builder().setLevel(23).build();示例还实现了toBuilder()方法,用来获得一个以给定实例值初始化的 Builder,详见下文"从已构建实例反向得到 Builder"。
理解 Kotlin 类所需的依赖
为了让 AutoBuilder 能理解 Kotlin 类(读取参数名、识别带默认值的参数),通常需要在依赖com.google.auto.value:auto-value的同一位置额外添加org.jetbrains.kotlin:kotlin-metadata-jvm依赖;较早的org.jetbrains.kotlinx:kotlinx-metadata-jvm也可以。
这一点在源码中也有印证:KotlinMetadata.java 在 Kotlin metadata API 不可用时会发出警告[AutoBuilderNoMetadataApi] The Kotlin metadata API (kotlinx.metadata or kotlin.metadata) is not available. You may need to add a dependency on org.jetbrains.kotlin:kotlin-metadata-jvm。此外,当 Kotlin 构造函数的默认参数需要被传递时,处理器会生成一个桥接类(见 AutoBuilderProcessor.java 中的generateForwardingClass),通过 Kotlin 合成的DefaultConstructorMarker位掩码来指定哪些可选参数使用默认值——这是 Java 源码无法直接调用合成构造函数的解决办法。
仓库集成测试 AutoBuilderKotlinTest.java 覆盖了 Kotlin 默认参数、@Nullable、通配符类型以及从实例反构 Builder 等场景,可作为参考。
生成的子类
与@AutoValue.Builder一样,编译@AutoBuilder类会生成一个具体子类。上面例子中生成的是class AutoBuilder_PersonBuilder extends PersonBuilder。常见的做法是提供一个静态builder()方法,如示例所示,它调用new AutoBuilder_...(),这通常是对生成类的唯一引用。
如果@AutoBuilder类型是嵌套的,生成类名会体现嵌套关系:
class Outer { static class Inner { @AutoBuilder abstract static class Builder {...} } static Inner.Builder builder() { return new AutoBuilder_Outer_Inner_Builder(); } }@AutoBuilder注解参数
@AutoBuilder有两个注解参数:ofClass和callMethod(对应 AutoBuilder.java 中的两个成员,默认值分别为Void.class和空字符串)。
- 如果指定了
ofClass,build()将调用该类(ofClass指定的类)的构造函数或静态方法;否则调用包含@AutoBuilder类的那个类的构造函数或静态方法; - 如果指定了
callMethod,build()将调用指定名字的静态方法;否则调用构造函数。
下面用接口形式展示四种组合(也可以使用抽象类;如果抽象类是嵌套的,则必须是 static 的)。
同时指定callMethod与ofClass
@AutoBuilder(callMethod = "of", ofClass = LocalTime.class) interface LocalTimeBuilder { ... LocalTime build(); // 调用:LocalTime.of(...) }只指定ofClass
@AutoBuilder(ofClass = Thread.class) interface ThreadBuilder { ... Thread build(); // 调用:new Thread(...) }只指定callMethod
class Foo { static String concat(String first, String middle, String last) {...} @AutoBuilder(callMethod = "concat") interface ConcatBuilder { ... String build(); // 调用:Foo.concat(first, middle, last) } }注意本例中静态方法返回String。隐式的ofClass是Foo,但静态方法可以返回任意类型。
两者都不指定
class Person { Person(String name, int id) {...} @AutoBuilder interface Builder { ... Person build(); // 调用:new Person(name, id) } }从处理器源码看,getOfClass在未指定ofClass时会取@AutoBuilder类型的包围元素(enclosing element),如果它既不是类也不是 record,则报错[AutoBuilderEnclosing] @AutoBuilder must specify ofClass=Something.class or it must be nested inside the class to be built(见 AutoBuilderProcessor.java)。同样,被注解类型本身只能是类或接口,且不能是 private 的嵌套类——测试 AutoBuilderCompilationTest.java 中分别用[AutoBuilderWrongType]和[AutoBuilderPrivate]验证了这些约束。
build 方法
build 方法(即"构建方法")必须有特定的返回类型:如果调用构造函数,返回类型必须是所构造类的类型;如果调用静态方法,返回类型必须是该静态方法的返回类型。
它通常叫build(),但并不强制。唯一要求是:必须恰好有一个无参抽象方法,其返回类型符合上述描述,且不对应任何参数名。
下面的例子用call()命名,因为它更准确地反映了方法的作用:
public class LogUtil { public static void log(Level severity, String message, Object... params) {...} @AutoBuilder(callMethod = "log") public interface Caller { Caller setSeverity(Level level); Caller setMessage(String message); Caller setParams(Object... params); void call(); // 调用:LogUtil.log(severity, message, params) } }注意这里的返回类型是void,同样满足规则(静态方法返回void)。
从已构建实例反向得到 Builder
并不是总能从构造函数或方法调用的结果反推出一个可能生成它的 Builder。但在一种重要场景下是可行的:当构造函数或方法中的每个参数都在被构建类型中对应一个 "getter 方法" 时。构建 Java record 或 Kotlin data class(只要其 getter 对 Builder 可见)时这一条件总是成立。此时生成的 Builder 类会多一个第二个构造函数:接收一个被构建类型的对象,产出一个以该对象各属性值初始化的 Builder。随后可以用它生成一个只在个别属性上与原对象不同的新对象。(这与 AutoValue 的toBuilder()特性非常相似。)
如果构造函数或方法有参数String bar,则被构建类型必须有一个可见的方法String bar()或String getBar()(Java record 是前者,Kotlin data class 是后者)。如果每个参数都有对应方法,就会生成第二个构造函数。
处理器中的propertyToGetterName方法(见 AutoBuilderProcessor.java)实现了这套匹配逻辑:按参数名bar依次查找bar()、getBar()(boolean 参数还会查找isBar()),且要求 getter 返回类型与参数类型兼容;只有当全部参数都匹配成功时,才会启用"从实例反构"(模板变量toBuilderConstructor才为 true)。
如果你能修改被构建类型,最方便的做法是添加toBuilder()实例方法,让它调用new AutoBuilder_Foo(this)——上面 Kotlin 示例就是这么做的。否则,可以提供第二个静态 builder 方法:
@AutoBuilder(ofClass = Person.class) abstract class PersonBuilder { static PersonBuilder personBuilder() { return new AutoBuilder_PersonBuilder(); } static PersonBuilder personBuilder(Person person) { return new AutoBuilder_PersonBuilder(person); } ... }集成测试 AutoBuilderKotlinTest.java 中的kotlinWithDefaults_defaulted用例验证了这种用法:从实例x构造出等价的副本copy,再setAnInt(17)得到只改一个属性的新实例。
重载的构造函数或方法
有可能存在多个与callMethod和ofClass匹配的构造函数或静态方法。AutoBuilder 会忽略生成类不可见的那些——即 private 的,或位于不同包且包级私有(package-private)的。在其余候选中,它会选择参数名与@AutoBuilder的 setter 方法匹配的那个。如果匹配的构造函数/方法不是恰好一个,就是编译错误。
从源码看,matchingExecutable先按参数名与 setter 形状过滤候选,若过滤后仍多于一个,则优先选参数数量最多的;若最多个数仍有多个匹配,则报[AutoBuilderAmbiguous] Property names correspond to more than one ...。找不到任何匹配则报[AutoBuilderNoMatch],没有可见候选则报[AutoBuilderNoVisible]。
泛型
如果 Builder 调用泛型类型的构造函数,它必须与那个类型有相同的类型参数:
class NumberPair<T extends Number> { NumberPair(T first, T second) {...} @AutoBuilder interface Builder<T extends Number> { Builder<T> setFirst(T x); Builder<T> setSecond(T x); NumberPair<T> build(); } }如果 Builder 调用带类型参数的静态方法,它必须与那个方法有相同的类型参数:
class Utils { static <K extends Number, V> Map<K, V> singletonNumberMap(K key, V value) {...} @AutoBuilder(callMethod = "singletonNumberMap") interface Builder<K extends Number, V> { Builder<K, V> setKey(K x); Builder<K, V> setValue(V x); Map<K, V> build(); } }虽然不常见,但 Java 构造函数可以有自己独立的类型参数(与包含它的类无关)。调用这种构造函数的 Builder 必须依次声明:类的类型参数 + 构造函数的类型参数:
class CheckedSet<E> implements Set<E> { <T extends E> CheckedSet(Class<T> type) {...} @AutoBuilder interface Builder<E, T extends E> { Builder<E, T> setType(Class<T> type); CheckedSet<E> build(); } }必填、可选与可空参数
参数默认值的规则:
- 标注了
@Nullable的参数默认值为null; - 类型为
Optional、OptionalInt、OptionalLong、OptionalDouble的参数默认值为空; - Kotlin 构造函数中带默认值的参数默认取该默认值;
- 其余所有参数都是必填的,调用 build 方法时若遗漏任何一个,会抛出
IllegalStateException。
要建立默认值,可以在builder()方法返回 Builder 之前设置它们:
class Foo { Foo(String bar, @Nullable String baz, String buh) {...} static Builder builder() { return new AutoBuilder_Foo_Builder() .setBar(DEFAULT_BAR); } @AutoBuilder interface Builder { Builder setBar(String x); Builder setBaz(String x); Builder setBuh(String x); Foo build(); } { builder().build(); // IllegalStateException, buh is not set builder().setBuh("buh").build(); // OK, bar=DEFAULT_BAR and baz=null builder().setBaz(null).setBuh("buh").build(); // OK builder().setBar(null); // NullPointerException, bar is not @Nullable } }试图给没有标注@Nullable的参数设置null会产生NullPointerException。
@Nullable在这里是任何同名注解,例如javax.annotation.Nullable或org.checkerframework.checker.nullness.qual.Nullable。
生成代码中的行为与文档一致:从 AutoBuilderCompilationTest.java 的预期输出可以看到,setAString对null直接抛NullPointerException,而build()用位掩码检查遗漏的必填属性并抛IllegalStateException。另外,处理器还会对"基础类型标注@Nullable"的情况报[AutoBuilderNullPrimitive] Primitive types cannot be @Nullable(见 AutoBuilderProcessor.java 的propertySet方法)。
Getter 方法
@AutoBuilder类或接口还可以有Getter方法。Getter 返回某个参数当前已设置的值,其返回类型可以是参数类型本身,也可以是包裹该类型的Optional。前者在尚未设置任何值时调用会抛异常,后者则返回空Optional。
下面的例子中,nickname参数默认与name参数取相同值,但也可以设置成不同的值:
public class Named { Named(String name, String nickname) {...} @AutoBuilder public abstract static class Builder { public abstract Builder setName(String x); public abstract Builder setNickname(String x); abstract String getName(); abstract Optional<String> getNickname(); abstract Named autoBuild(); public Named build() { if (!getNickname().isPresent()) { setNickname(getName()); } return autoBuild(); } } }这个例子展示了 AutoBuilder 实现的包级私有autoBuild()方法:公开的build()方法在必要时调整 nickname 后调用它。这里用抽象类而非接口,是为了区分"供用户调用的 public 方法"和"Builder 自身逻辑使用的包级私有方法"。
构建注解实例
AutoBuilder 可以构建注解接口的实例。当注解没有元素(方法),或只有一个元素时,用 AutoAnnotation 更简单;但当注解有多个元素时,Builder 就很有用了。两种方式的例子都见 howto.md 中的 annotation 章节。
从处理器源码看(AutoBuilderProcessor.java 的buildAnnotation),当ofClass指向一个注解类型时,处理器会先自动生成一个带@AutoAnnotation方法的辅助类AutoBuilderAnnotation_<TypeName>,再把@AutoBuilder处理为调用该newAnnotation方法,从而绕开"注解既没有构造函数也没有静态方法"的限制。
命名约定
参数foo的 setter 方法可以叫setFoo或foo。getter 方法可以叫getFoo或foo,对于boolean参数还可以叫isFoo。Getter 与 Setter 的取名是相互独立的,例如 getter 可以是foo()而 setter 是setFoo(T)。
按惯例,setter 方法的参数名要么呼应所设置的参数(Builder setName(String name);),要么直接用x(Builder setName(String x);)。
如果类Foo有一个嵌套的@AutoBuilder用于构建Foo实例,那么按惯例这个类型叫Builder,实例通过静态方法Foo.builder()获得:
Foo foo1 = Foo.builder().setBar(bar).setBaz(baz).build(); Foo.Builder fooBuilder = Foo.builder();如果针对Foo的@AutoBuilder是独立顶层类,那么它通常叫FooBuilder,并有一个返回FooBuilder实例的静态方法fooBuilder(),这样调用方可以静态导入FooBuilder.fooBuilder直接写fooBuilder():
@AutoBuilder(ofClass = Foo.class) public abstract class FooBuilder { public static FooBuilder fooBuilder() { return new AutoBuilder_FooBuilder(); } ... public abstract Foo build(); }如果@AutoBuilder是设计来调用某个非工厂方法的静态方法,那么名字中"call"比"build"更合适:类型叫FooCaller,静态方法叫fooCaller(),"build 方法"叫call():
@AutoBuilder(callMethod = "log", ofClass = MyLogger.class) public abstract class LogCaller { public static LogCaller logCaller() { return new AutoBuilder_LogCaller(); } ... public abstract void call(); } // 使用方式: logCaller().setLevel(Level.INFO).setMessage("oops").call();其他 Builder 特性
还有一些 Builder 特性此处未展开,因为它们与@AutoValue.Builder相同,包括:
- 集合的特殊处理
- 嵌套 Builder 的处理
如果你只是想让@AutoValue类使用 Builder,请阅读 AutoValue with Builders;更细的"怎么做"问题参见 How do I... (Builder edition)。
参数名不可用的情况
AutoBuilder 依赖参数名信息。但 Java 中参数名并不总是可用的,至少在以下情况下它们是可用的:
- 与
@AutoBuilder类或接口同时编译的代码; - record(Java 16 起);
- Kotlin data class 的构造函数;
- 使用
-parameters选项编译的代码。
存在一个 javac 编译器 bug:在 JDK 11 之前的版本中,除第一种情况外,参数名对 AutoBuilder 都不可用。因此建议使用较新的 JDK 构建,必要时用--release选项生成可在更早版本上运行的代码。
如果参数名不可用,你总是可以在与@AutoBuilder类型同一个类中引入一个静态方法,让它去调用你想要的方法。由于它是与@AutoBuilder同时编译的,其参数名是可用的。
下面是一个用这种方式解决问题的例子。下面的代码通常无法编译,因为 JDK 方法的参数名不可用:
import java.time.LocalTime; public class TimeUtils { // 无法工作:LocalTime.of 的参数名不可用。 @AutoBuilder(callMethod = "of", ofClass = LocalTime.class) public interface TimeBuilder { TimeBuilder setHour(int x); TimeBuilder setMinute(int x); TimeBuilder setSecond(int x); LocalTime build(); } }它会报类似这样的错误:
error: [AutoBuilderNoMatch] Property names do not correspond to the parameter names of any static method named "of": public interface TimeBuilder { ^ of(int arg0, int arg1) of(int arg0, int arg1, int arg2) of(int arg0, int arg1, int arg2, int arg3)arg0、arg1等名字是编译器因为不知道真实名字而编造出来的。
引入一个静态方法即可解决:
import java.time.LocalTime; public class TimeUtils { static LocalTime localTimeOf(int hour, int minute, int second) { return LocalTime.of(hour, minute, second); } @AutoBuilder(callMethod = "localTimeOf") public interface TimeBuilder { TimeBuilder setHour(int x); TimeBuilder setMinute(int x); TimeBuilder setSecond(int x); LocalTime build(); } }注意:AutoBuilder 对参数名的匹配是大小写不敏感的(见 AutoBuilderProcessor.java 中executableMatches使用CASE_INSENSITIVE_ORDER的说明),同时处理器还会把 Kotlin 中可能出现的 Java 保留字参数名(如val、fun)修正为合法标识符,再生成到 Java 代码中(propertySet中的fixReservedIdentifiers)。
总结
@AutoBuilder为"调用构造函数或静态方法"这一场景提供了与@AutoValue.Builder同样优雅的具名参数体验:无需知道参数顺序、支持默认值与必填校验、支持 Kotlin 互操作与泛型、可反向从实例构造 Builder、甚至可以构建注解实例。结合仓库中的 AutoBuilderCompilationTest.java 与 AutoBuilderKotlinTest.java 集成测试,你可以验证这里描述的每一项行为,并在自己的项目中放心使用。
- 开发工具
- 代码生成
【免费下载链接】auto
A collection of source code generators for Java.
相关推荐
Objective-C静态方法注释:VVDocumenter-Xcode的静态函数文档生成
Objective C静态方法注释:VVDocumenter Xcode的静态函数文档生成 在Objective C开发中,手动编写静态方法注释不仅耗时,还容易
文档开发工具co.wrap用法详解:将生成器函数转换为普通Promise函数
co.wrap用法详解:将生成器函数转换为普通Promise函数 co.wrap是JavaScript异步编程库co的核心功能之一,它能够将生成器函数转换为返回
后端开发工具jsoneditor API 完全指南:构造函数、配置选项、方法与静态属性详解
jsoneditor API 完全指南:构造函数、配置选项、方法与静态属性详解 本篇技术指南以 docs/api.md https://link.gitcode
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考