有没有办法让@Builder 注释适用于不可变 类?
Is there a way to make the @Builder annotation work for immutable classes?
我正在尝试在 Groovy 中开发一个项目,我一直在查看我的代码并试图找到可以用更惯用的东西替换的区域 Groovy 直到我找到.
的解决方案
我已经开始更深入地研究 AST 转换注释的使用 - 它们帮助显着减少了我在某些地方必须编写的代码量。但是,我在使用带有我的不可变值 class 之一的 groovy.transform.builder.Builder
注释时遇到问题。此注释的来源托管在 here.
问题是注释似乎让构建器直接设置构建器的值,而不是存储值的副本并将它们传递给构建器的构造器。当您尝试将它与不可变的 classes 一起使用时,这会导致 ReadOnlyPropertyException
。
您可以使用此注释 select 四种可能的构建器策略,我已经尝试了其中的 DefaultStrategy
、ExternalStrategy
和 InitializerStrategy
。然而,这一切都带来了问题。
ExternalStrategy
看起来是四个中最有前途的,你可以找到一个基于它的 SSCCE 详细说明问题 here.
示例中的源代码也包含在下面:
import groovy.transform.Immutable
import groovy.transform.builder.Builder as GBuilder
import groovy.transform.builder.ExternalStrategy
/*
* Uncommenting the below causes a failure:
* 'groovy.lang.ReadOnlyPropertyException: Cannot set readonly property: value for class: Value'
*/
//@Immutable
class Value {
@GBuilder(forClass = Value, prefix = 'set', builderStrategy = ExternalStrategy)
static class Builder { }
int value
String toString() { "Value($value)" }
}
def builder = new Value.Builder()
println builder.setValue(1).build()
关于此事似乎也有相关的 JIRA 讨论 here。
编辑
我试过在下面使用 CFrick 的答案,使用 InitializerStrategy
而不是 ExternalStrategy
.
现在一切都可以编译,但是当我尝试执行测试时 运行 时出现以下错误:
java.lang.IllegalAccessError: tried to access class com.github.tagc.semver.version.BaseVersion from class com.github.tagc.semver.version.BaseVersion$com.github.tagc.semver.version.BaseVersionInitializer
at java.lang.Class.getDeclaringClass(Class.java:1227)
at java.beans.MethodRef.set(MethodRef.java:46)
at java.beans.MethodDescriptor.setMethod(MethodDescriptor.java:117)
at java.beans.MethodDescriptor.<init>(MethodDescriptor.java:72)
at java.beans.MethodDescriptor.<init>(MethodDescriptor.java:56)
at java.beans.Introspector.getTargetMethodInfo(Introspector.java:1163)
at java.beans.Introspector.getBeanInfo(Introspector.java:426)
at java.beans.Introspector.getBeanInfo(Introspector.java:173)
at com.github.tagc.semver.version.VersionFactory.createBaseVersion(VersionFactory.groovy:34)
at com.github.tagc.semver.test.util.TestSetup.<clinit>(TestSetup.groovy:77)
at java.lang.Class.forName(Class.java:344)
at com.github.tagc.semver.version.SnapshotDecoratorSpec.#decoratedVersion should be considered equal to patch-bumped #releaseVersion snapshot(SnapshotDecoratorSpec.groovy:24)
随后出现如下一系列异常:
java.lang.NoClassDefFoundError: Could not initialize class com.github.tagc.semver.test.util.TestSetup
at java.lang.Class.forName(Class.java:344)
at com.github.tagc.semver.version.SnapshotDecoratorSpec.#decoratedVersion should be considered equal to minor-bumped #releaseVersion snapshot(SnapshotDecoratorSpec.groovy:36)
我现在拥有的是 BaseVersion
class,如下所示:
/**
* A concrete, base implementation of {@link com.github.tagc.semver.version.Version Version}.
*
* @author davidfallah
* @since v0.1.0
*/
@Immutable
@Builder(prefix = 'set', builderStrategy = InitializerStrategy)
@PackageScope
final class BaseVersion implements Version {
// ...
/**
* The major category of this version.
*/
int major = 0
/**
* The minor category of this version.
*/
int minor = 0
/**
* The patch category of this version.
*/
int patch = 0
/**
* Whether this version is a release or snapshot version.
*/
boolean release = false
// ...
}
生产这些实例的工厂:
/**
* A factory for producing base and decorated {@code Version} objects.
*
* @author davidfallah
* @since v0.5.0
*/
class VersionFactory {
// ...
/**
* Returns an instance of {@link com.github.tagc.semver.version.BaseVersion BaseVersion} constructed
* with the given parameters.
*
* @param major the major category value of the version instance
* @param minor the minor category value of the version instance
* @param patch the patch category value of the version instance
* @param release the release setting of the version instance
* @return an instance of {@code BaseVersion}
*/
static BaseVersion createBaseVersion(int major, int minor, int patch, boolean release) {
return new BaseVersion(major, minor, patch, release)
}
/**
* Returns an instance of {@link com.github.tagc.semver.version.BaseVersion BaseVersion} constructed
* with the given parameters.
*
* @param m a map of parameter names and their corresponding values corresponding to the
* construction parameters of {@code BaseVersion}.
*
* @return an instance of {@code BaseVersion}
*/
static BaseVersion createBaseVersion(Map m) {
return new BaseVersion(m)
}
/**
* Returns an instance of {@link com.github.tagc.semver.version.BaseVersion BaseVersion} constructed
* with the given parameters.
*
* @param l a list of parameter values corresponding to the construction parameters of {@code BaseVersion}.
*
* @return an instance of {@code BaseVersion}
*/
static BaseVersion createBaseVersion(List l) {
return new BaseVersion(l)
}
/**
* Returns a builder for {@link com.github.tagc.semver.version.BaseVersion BaseVersion} to specify
* the construction parameters for the {@code BaseVersion} incrementally.
*
* @return an instance of {@code BaseVersion.Builder}
*/
static Object createBaseVersionBuilder() {
return BaseVersion.builder()
}
// ...
}
Version
个对象的测试规范class:
/**
* Test specification for {@link com.github.tagc.semver.version.Version Version}.
*
* @author davidfallah
* @since 0.1.0
*/
@Unroll
class VersionSpec extends Specification {
static exampleVersions = [
VersionFactory.createBaseVersion(major:1, minor:2, patch:3),
VersionFactory.createBaseVersion(major:0, minor:0, patch:0),
VersionFactory.createBaseVersion(major:5, minor:4, patch:3),
VersionFactory.createBaseVersion(major:1, minor:16, patch:2),
VersionFactory.createBaseVersion(major:4, minor:5, patch:8),
]
// ...
}
和其他 class 尝试创建失败的 BaseVersion
实例,例如 TestSetup
。
你的代码失败了,因为那里选择的策略基本上是这样的:
def v = new Value().with{ setValue(1); return it }
这不能在 @Immutable
个对象上完成。
根据 docs,只有 InitializerStrategy
可以显式处理 @Immutable
。
You can use the InitializerStrategy in conjunction with @Canonical and @Immutable. If your @Builder annotation doesn’t have explicit includes or excludes annotation attributes but your @Canonical annotation does, the ones from @Canonical will be re-used for @Builder.
例如
import groovy.transform.*
import groovy.transform.builder.*
@Immutable
@ToString
@Builder(prefix='set', builderStrategy=InitializerStrategy)
class Value {
int value
}
def builder = Value.createInitializer().setValue(1)
assert new Value(builder).toString()=='Value(1)'
根据您的工作,这是相当丑陋的语法,您最好只使用基于地图的 c'tor。即使没有例如@TypeChecked
a new Value(vlaue: 666)
将产生错误并留下参数(对于具有多个属性的 class)将留下它们 null
.
我正在尝试在 Groovy 中开发一个项目,我一直在查看我的代码并试图找到可以用更惯用的东西替换的区域 Groovy 直到我找到
我已经开始更深入地研究 AST 转换注释的使用 - 它们帮助显着减少了我在某些地方必须编写的代码量。但是,我在使用带有我的不可变值 class 之一的 groovy.transform.builder.Builder
注释时遇到问题。此注释的来源托管在 here.
问题是注释似乎让构建器直接设置构建器的值,而不是存储值的副本并将它们传递给构建器的构造器。当您尝试将它与不可变的 classes 一起使用时,这会导致 ReadOnlyPropertyException
。
您可以使用此注释 select 四种可能的构建器策略,我已经尝试了其中的 DefaultStrategy
、ExternalStrategy
和 InitializerStrategy
。然而,这一切都带来了问题。
ExternalStrategy
看起来是四个中最有前途的,你可以找到一个基于它的 SSCCE 详细说明问题 here.
示例中的源代码也包含在下面:
import groovy.transform.Immutable
import groovy.transform.builder.Builder as GBuilder
import groovy.transform.builder.ExternalStrategy
/*
* Uncommenting the below causes a failure:
* 'groovy.lang.ReadOnlyPropertyException: Cannot set readonly property: value for class: Value'
*/
//@Immutable
class Value {
@GBuilder(forClass = Value, prefix = 'set', builderStrategy = ExternalStrategy)
static class Builder { }
int value
String toString() { "Value($value)" }
}
def builder = new Value.Builder()
println builder.setValue(1).build()
关于此事似乎也有相关的 JIRA 讨论 here。
编辑
我试过在下面使用 CFrick 的答案,使用 InitializerStrategy
而不是 ExternalStrategy
.
现在一切都可以编译,但是当我尝试执行测试时 运行 时出现以下错误:
java.lang.IllegalAccessError: tried to access class com.github.tagc.semver.version.BaseVersion from class com.github.tagc.semver.version.BaseVersion$com.github.tagc.semver.version.BaseVersionInitializer
at java.lang.Class.getDeclaringClass(Class.java:1227)
at java.beans.MethodRef.set(MethodRef.java:46)
at java.beans.MethodDescriptor.setMethod(MethodDescriptor.java:117)
at java.beans.MethodDescriptor.<init>(MethodDescriptor.java:72)
at java.beans.MethodDescriptor.<init>(MethodDescriptor.java:56)
at java.beans.Introspector.getTargetMethodInfo(Introspector.java:1163)
at java.beans.Introspector.getBeanInfo(Introspector.java:426)
at java.beans.Introspector.getBeanInfo(Introspector.java:173)
at com.github.tagc.semver.version.VersionFactory.createBaseVersion(VersionFactory.groovy:34)
at com.github.tagc.semver.test.util.TestSetup.<clinit>(TestSetup.groovy:77)
at java.lang.Class.forName(Class.java:344)
at com.github.tagc.semver.version.SnapshotDecoratorSpec.#decoratedVersion should be considered equal to patch-bumped #releaseVersion snapshot(SnapshotDecoratorSpec.groovy:24)
随后出现如下一系列异常:
java.lang.NoClassDefFoundError: Could not initialize class com.github.tagc.semver.test.util.TestSetup
at java.lang.Class.forName(Class.java:344)
at com.github.tagc.semver.version.SnapshotDecoratorSpec.#decoratedVersion should be considered equal to minor-bumped #releaseVersion snapshot(SnapshotDecoratorSpec.groovy:36)
我现在拥有的是 BaseVersion
class,如下所示:
/**
* A concrete, base implementation of {@link com.github.tagc.semver.version.Version Version}.
*
* @author davidfallah
* @since v0.1.0
*/
@Immutable
@Builder(prefix = 'set', builderStrategy = InitializerStrategy)
@PackageScope
final class BaseVersion implements Version {
// ...
/**
* The major category of this version.
*/
int major = 0
/**
* The minor category of this version.
*/
int minor = 0
/**
* The patch category of this version.
*/
int patch = 0
/**
* Whether this version is a release or snapshot version.
*/
boolean release = false
// ...
}
生产这些实例的工厂:
/**
* A factory for producing base and decorated {@code Version} objects.
*
* @author davidfallah
* @since v0.5.0
*/
class VersionFactory {
// ...
/**
* Returns an instance of {@link com.github.tagc.semver.version.BaseVersion BaseVersion} constructed
* with the given parameters.
*
* @param major the major category value of the version instance
* @param minor the minor category value of the version instance
* @param patch the patch category value of the version instance
* @param release the release setting of the version instance
* @return an instance of {@code BaseVersion}
*/
static BaseVersion createBaseVersion(int major, int minor, int patch, boolean release) {
return new BaseVersion(major, minor, patch, release)
}
/**
* Returns an instance of {@link com.github.tagc.semver.version.BaseVersion BaseVersion} constructed
* with the given parameters.
*
* @param m a map of parameter names and their corresponding values corresponding to the
* construction parameters of {@code BaseVersion}.
*
* @return an instance of {@code BaseVersion}
*/
static BaseVersion createBaseVersion(Map m) {
return new BaseVersion(m)
}
/**
* Returns an instance of {@link com.github.tagc.semver.version.BaseVersion BaseVersion} constructed
* with the given parameters.
*
* @param l a list of parameter values corresponding to the construction parameters of {@code BaseVersion}.
*
* @return an instance of {@code BaseVersion}
*/
static BaseVersion createBaseVersion(List l) {
return new BaseVersion(l)
}
/**
* Returns a builder for {@link com.github.tagc.semver.version.BaseVersion BaseVersion} to specify
* the construction parameters for the {@code BaseVersion} incrementally.
*
* @return an instance of {@code BaseVersion.Builder}
*/
static Object createBaseVersionBuilder() {
return BaseVersion.builder()
}
// ...
}
Version
个对象的测试规范class:
/**
* Test specification for {@link com.github.tagc.semver.version.Version Version}.
*
* @author davidfallah
* @since 0.1.0
*/
@Unroll
class VersionSpec extends Specification {
static exampleVersions = [
VersionFactory.createBaseVersion(major:1, minor:2, patch:3),
VersionFactory.createBaseVersion(major:0, minor:0, patch:0),
VersionFactory.createBaseVersion(major:5, minor:4, patch:3),
VersionFactory.createBaseVersion(major:1, minor:16, patch:2),
VersionFactory.createBaseVersion(major:4, minor:5, patch:8),
]
// ...
}
和其他 class 尝试创建失败的 BaseVersion
实例,例如 TestSetup
。
你的代码失败了,因为那里选择的策略基本上是这样的:
def v = new Value().with{ setValue(1); return it }
这不能在 @Immutable
个对象上完成。
根据 docs,只有 InitializerStrategy
可以显式处理 @Immutable
。
You can use the InitializerStrategy in conjunction with @Canonical and @Immutable. If your @Builder annotation doesn’t have explicit includes or excludes annotation attributes but your @Canonical annotation does, the ones from @Canonical will be re-used for @Builder.
例如
import groovy.transform.*
import groovy.transform.builder.*
@Immutable
@ToString
@Builder(prefix='set', builderStrategy=InitializerStrategy)
class Value {
int value
}
def builder = Value.createInitializer().setValue(1)
assert new Value(builder).toString()=='Value(1)'
根据您的工作,这是相当丑陋的语法,您最好只使用基于地图的 c'tor。即使没有例如@TypeChecked
a new Value(vlaue: 666)
将产生错误并留下参数(对于具有多个属性的 class)将留下它们 null
.