有没有一种方法可以使@Builder批注适用于不可变的类? [英] Is there a way to make the @Builder annotation work for immutable classes?

查看:53
本文介绍了有没有一种方法可以使@Builder批注适用于不可变的类?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试在Groovy中开发一个项目,并且一直在浏览我的代码,并试图找到可以用更普通的Groovy代替的区域,直到找到

I'm trying to develop a project in Groovy and I've been looking through my code and trying to find areas which I could replace with something more idiomatically Groovy until I find a solution for another issue I've been having.

我已经开始更深入地研究AST转换注释的使用-它们极大地减少了我在某些地方必须编写的代码量.但是,我在使用 groovy.transform.builder.Builder 批注与我的不可变值类之一时遇到了问题.此注释的来源托管在这里.

I've started taking a more in-depth look into the use of AST transformation annotations - they've helped significantly cut down on the amount of code I have to write in some places. However, I'm having an issue using the groovy.transform.builder.Builder annotation with one of my immutable value classes. The source for this annotation is hosted here.

问题是注解似乎使构建器直接设置了构建者的值,而不是存储值的副本并将它们传递给构建者的构造器.当您尝试将其与不可变类一起使用时,这会导致 ReadOnlyPropertyException .

The issue is that the annotation seems to make the builder set the values of the buildee directly rather than storing a copy of the values and passing them to the buildee's constructor. This results in a ReadOnlyPropertyException when you try to use it with immutable classes.

您可以通过此注释选择四种可能的Builder策略,其中我尝试了 DefaultStrategy ExternalStrategy InitializerStrategy .但是,所有这些都导致了问题.

There are four possible Builder strategies you can select with this annotation, and of them I've tried DefaultStrategy, ExternalStrategy and InitializerStrategy. However, all of these have caused problems.

ExternalStrategy 看起来像是四个中最有前途的,您可以找到基于它的SSCCE,其中详细列出了问题此处.

ExternalStrategy looks like the most promising of the four, and you can find an SSCCE based on it detailing the problem here.

示例中的源代码也包括在下面:

The source code from the example is also included below:

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()

此处..

修改
我已尝试使用以下CFrick的答案,而不是使用 InitializerStrategy 而不是 ExternalStrategy .

现在所有内容都可以编译,但是当我尝试执行测试时,在运行时出现以下错误:

Everything now compiles, but I get the following errors at run-time when I try to execute my tests:

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)

此后出现一系列异常,如下所示:

Followed thereafter by a series of exceptions like the following:

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 类,如下所示:

What I have right now is a BaseVersion class like the following:

/**
 * 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 对象的测试规范类:

A test specification class for Version objects:

/**
 * 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),
        ]

    // ...
}

以及其他尝试创建失败的 BaseVersion 实例的类,例如 TestSetup .

And other classes that try to create instances of BaseVersion that are failing, such as TestSetup.

推荐答案

您的代码在那里失败,因为在那里选择的策略基本上可以做到:

Your code there fails, because chosen strategy there basically does:

 def v = new Value().with{ setValue(1); return it }

,这不能在 @Immutable 对象上完成.

and this can not be done on @Immutable objects.

根据 docs ,仅有 InitializerStrategy ,可以显式应对 @Immutable .

According to the docs, there is only InitializerStrategy, that can explicitly cope with @Immutable.

您可以将InitializerStrategy与@Canonical和@Immutable结合使用.如果您的@Builder批注没有显式的包含或排除批注属性,但是您的@Canonical批注具有,则@Canonical的批注将重新用于@Builder.

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)'

这取决于您的工作,这是非常丑陋的语法,仅使用基于Map的c'tors可能会更好.即使没有 @TypeChecked 一个 new Value(vlaue:666)将产生一个错误,并且对于(具有多个属性的类)保留参数将使它们 null

Depending on what you are up to, this is rahter ugly syntax and you might be better off just using the Map-based c'tors. Even without e.g. @TypeChecked a new Value(vlaue: 666) will generate an error and leaving params (for a class with multiple properties) will leave them null.

这篇关于有没有一种方法可以使@Builder批注适用于不可变的类?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆