【问题标题】:Adding no arg constructor to Scala enumerations不向 Scala 枚举添加 arg 构造函数
【发布时间】:2017-03-14 13:08:21
【问题描述】:

我有以下 Scala 枚举:

object RunMode extends Enumeration {
  val CLIENT_MODE = Value("CLIENT")
  val SERVER_MODE = Value("SERVER")
}

我有一些 JSON 作为我的应用程序的输入,例如:

{
    "version" : "0.1",
    "runMode" : "CLIENT"
}

这里的 JSON 字段“runMode”实际上是我的RunMode 枚举,它的值将始终是“CLIENT”或“SERVER”。我正在尝试使用 GSON 将此 JSON 反序列化为 AppConfig 实例:

class AppConfig(version : String, runMode : RunMode) {
  def version() : String = { this.version }
  def runMode() : RunMode.Value = { this.runMode }
}  

我有以下 GSON 代码:

val gson = new Gson()
val text = Source.fromFile(jsonConfigFile).mkString
gson.fromJson(text, classOf[AppConfig])

运行时:

java.lang.RuntimeException: Unable to invoke no-args constructor for class scala.Enumeration$Value. Register an InstanceCreator with Gson for this type may fix this problem.
> Buildiat com.google.gson.internal.ConstructorConstructor$14.construct(ConstructorConstructor.java:226)
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:210)
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129)
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220)
    at com.google.gson.Gson.fromJson(Gson.java:887)
  <rest of stacktrace omitted for brevity>

很明显,GSON 期望 RunMode 有一个无参数构造函数,而它没有,因此它无法在运行时反序列化我的 JSON 文件。

我已经尝试了一百万种不同的组合,但似乎无法找到神奇的构造函数定义。所以我问:如何向RunMode 添加无参数构造函数,以便 GSON 可以将其反序列化为 AppConfig 实例?

【问题讨论】:

  • 您需要 Gson 有什么特别的原因吗?如果没有,有很多 Scala 库可以开箱即用并支持枚举。
  • 感谢@YuvalItzchakov (+1) 如果你能提供一个具体的代码示例,我当然会考虑!
  • 我也认为(如果我错了,请随时纠正我!)这个问题的核心不是 GSON,它是事实上,我的 RunMode 枚举需要一个无 arg ctor。我曾经使用过的每一个序列化框架(Jackson、XStream、GSON 等)都对模型类有相同的要求。因此,尽管我想我会接受任何有效的非 GSON 解决方案,但我真的很想让 no arg ctor 与 Scala 枚举一起使用,因为该问题可能会在代码的其他区域再次出现

标签: json scala enums gson


【解决方案1】:

这并没有直接回答使用 Gson 失败的原因,而是提供了一种替代方法。这是一个使用argonaut的例子:

RunMode枚举定义:

object RunMode extends Enumeration {
  type RunMode = Value
  val CLIENT_MODE = Value("CLIENT")
  val SERVER_MODE = Value("SERVER")

  implicit def runModeCodec: CodecJson[RunMode.RunMode] = CodecJson({
    case CLIENT_MODE => "CLIENT".asJson
    case SERVER_MODE => "SERVER".asJson
  }, c => c.focus.string match {
    case Some("CLIENT") => DecodeResult.ok(CLIENT_MODE)
    case Some("SERVER") => DecodeResult.ok(SERVER_MODE)
    case _ => DecodeResult.fail("Could not decode RunMode", c.history)
  })
}

Foo的定义(匹配你要创建的对象):

case class Foo(version: String, runMode: RunMode)
object Foo {
  implicit def codec: CodecJson[Foo] = 
    casecodec2(Foo.apply, Foo.unapply)("version", "runMode")
}

现在是解码/编码示例:

object ArgonautEnumCodec {
  def main(args: Array[String]): Unit = {
    val res: String = Foo("0.1", RunMode.CLIENT_MODE).asJson.toString
    println(res)

    val foo: Foo = res.decodeOption[Foo].get
    println(foo)
  }
}

产量:

{"version":"0.1","runMode":"CLIENT"}
Foo(0.1,CLIENT)

【讨论】:

    【解决方案2】:

    由于我不是 Scala 人,但有一些 Gson 背景,因此对 Scala 的工作原理有所了解对我来说很有趣。出现异常的原因是 Gson 无法实例化抽象类 scala.Enumeration.ValueAutoConfig 类的内容与 vanilla Java 中的以下类非常相似:

    final class AppConfig {
    
        final String version;
    
        // This is where ig gets failed 
        final scala.Enumeration.Value runMode;
    
        AppConfig(final String version, final scala.Enumeration.Value runMode) {
            this.version = version;
            this.runMode = runMode;
        }
    
    }
    

    据我了解 Scala 枚举是如何实现的,与 Java 枚举不同,它们本身没有类型,并且每个 Scala 枚举值似乎都是 scala.Enumeration$Val 的一个实例,没有提供足够的“主机”枚举类型信息从它的类型(但是实例似乎有它们的外部类引用)。这就是为什么自定义实现自定义类型适配器并不那么简单,并且需要对真正的枚举类型进行一些检查(但不确定如何实现)。

    Gson 提供了一个特殊的注解@JsonAdapter,可以注解某个字段,包括要应用的类型适配器。所以上面类中的AppConfig.runMode可以注释为:

    @JsonAdapter(RunModeEnumTypeAdapter.class)
    final scala.Enumeration.Value runMode;
    

    请注意,它的名称中有一些关于目标类型的提示。这是因为可能没有其他方法可以指定目标枚举类型。现在,如何实现一个通用的scala.Enumeration 类型适配器。

    // E - a special generic type bound to associate a Scala enumeration with
    // So any Scala enumeration can be processed with this type adapter
    abstract class AbstractScalaEnumTypeAdapter<E extends scala.Enumeration>
            extends TypeAdapter<scala.Enumeration.Value> {
    
        private final E enumeration;
    
        protected AbstractScalaEnumTypeAdapter(final E enumeration) {
            this.enumeration = enumeration;
        }
    
        @Override
        @SuppressWarnings("resource")
        public final void write(final JsonWriter out, final scala.Enumeration.Value value)
                throws IOException {
            // If the given value is null, null must be written to the writer (however it depends on a particular Gson instance configuration)
            if ( value == null ) {
                out.nullValue();
            } else {
                // Does Scala provide something like java.lang.Enumeration#name?
                out.value(value.toString());
            }
        }
    
        @Override
        public final scala.Enumeration.Value read(final JsonReader in)
                throws IOException {
            final JsonToken token = in.peek();
            switch ( token ) {
            case NULL:
                // Consume the `null` JSON token
                in.nextNull();
                return null;
            case STRING:
                // Consume a JSON string value and lookup an appropriate Scala enumeration value by its name
                final String rawValue = in.nextString();
                return enumeration.withName(rawValue);
            // These case labels are matter of style and cover the rest of possible Gson JSON tokens, and are not really necessary
            case BEGIN_ARRAY:
            case END_ARRAY:
            case BEGIN_OBJECT:
            case END_OBJECT:
            case NAME:
            case NUMBER:
            case BOOLEAN:
            case END_DOCUMENT:
                throw new MalformedJsonException("Unexpected token: " + token);
            // Something else? Must never happen
            default:
                throw new AssertionError(token);
            }
        }
    
    }
    

    现在,RunMode 可以绑定到上面的类型适配器了:

    final class RunModeEnumTypeAdapter
            extends AbstractScalaEnumTypeAdapter<RunMode$> {
    
        // Gson can instantiate this itself
        private RunModeEnumTypeAdapter() {
            // This is how it looks like from the Java perspective
            // And this is the "hint" I was talking about above
            super(RunMode$.MODULE$);
        }
    
    }
    

    使用示例:

    final Gson gson = new Gson();
    final AppConfig appConfig = gson.fromJson("{\"version\":\"0.1\",\"runMode\":\"CLIENT\"}", AppConfig.class);
    System.out.println(appConfig.version);
    System.out.println(appConfig.runMode);
    System.out.println(gson.toJson(appConfig));
    

    输出:

    0.1
    客户
    {"version":"0.1","runMode":"CLIENT"}

    可能没有 Scala 做的那么好和紧凑,但我希望上面的代码可以毫无问题地翻译成 Scala。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-03-24
      • 2011-08-20
      • 1970-01-01
      • 2011-10-30
      • 1970-01-01
      • 2020-10-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多