ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AutoBuilder 使用指南:为任意构造函数与静态方法生成通用 Builder

AutoBuilder 使用指南:为任意构造函数与静态方法生成通用 Builder 开发工具代码生成【免费下载链接】autoA collection of source code generators for Java.项目地址https://gitcode.com/gh_mirrors/auto2/auto点击查看免费下载导读AutoBuilder是 AutoValue 项目提供的注解处理器它能把一个接口或抽象类实现为通用 BuilderSetter 方法按参数名累积值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类生成 BuilderAutoBuilder(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记录给参数namesetId同理。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 classclass KotlinData(val level: Int, val name: String?, val id: Long -1L)可以这样为其编写 BuilderAutoBuilder(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.Nullableid参数有默认值-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和空字符串。如果指定了ofClassbuild()将调用该类ofClass指定的类的构造函数或静态方法否则调用包含AutoBuilder类的那个类的构造函数或静态方法如果指定了callMethodbuild()将调用指定名字的静态方法否则调用构造函数。下面用接口形式展示四种组合也可以使用抽象类如果抽象类是嵌套的则必须是 static 的。同时指定callMethod与ofClassAutoBuilder(callMethod of, ofClass LocalTime.class) interface LocalTimeBuilder { ... LocalTime build(); // 调用LocalTime.of(...) }只指定ofClassAutoBuilder(ofClass Thread.class) interface ThreadBuilder { ... Thread build(); // 调用new Thread(...) }只指定callMethodclass 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 ofClassSomething.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 NumberPairT extends Number { NumberPair(T first, T second) {...} AutoBuilder interface BuilderT extends Number { BuilderT setFirst(T x); BuilderT setSecond(T x); NumberPairT build(); } }如果 Builder 调用带类型参数的静态方法它必须与那个方法有相同的类型参数class Utils { static K extends Number, V MapK, V singletonNumberMap(K key, V value) {...} AutoBuilder(callMethod singletonNumberMap) interface BuilderK extends Number, V { BuilderK, V setKey(K x); BuilderK, V setValue(V x); MapK, V build(); } }虽然不常见但 Java 构造函数可以有自己独立的类型参数与包含它的类无关。调用这种构造函数的 Builder 必须依次声明类的类型参数 构造函数的类型参数class CheckedSetE implements SetE { T extends E CheckedSet(ClassT type) {...} AutoBuilder interface BuilderE, T extends E { BuilderE, T setType(ClassT type); CheckedSetE 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, barDEFAULT_BAR and baznull 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 OptionalString 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);要么直接用xBuilder 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类或接口同时编译的代码recordJava 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 集成测试你可以验证这里描述的每一项行为并在自己的项目中放心使用。赞分享开发工具代码生成【免费下载链接】autoA collection of source code generators for Java.项目地址https://gitcode.com/gh_mirrors/auto2/auto点击查看免费下载相关推荐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组件上一篇FanControl终极指南如何完美控制Windows电脑风扇噪音与散热下一篇终极Windows界面定制指南如何用开源工具恢复经典体验创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表