带有 Scala 的 Gson 导致枚举的 StackOverflow

Gson with Scala causes StackOverflow for Enumerations

我在 Scala class 中定义了一个枚举,如下所示

// define compression types as enumerator
  object CompressionType extends Enumeration
  {
    type CompressionType = Value
    
    val None, Gzip, Snappy, Lz4, Zstd = Value    
  }

我有 class 想要在 JSON

中连载
case class ProducerConfig(batchNumMessages : Int, lingerMs : Int, messageSize : Int,
                            topic: String, compressionType: CompressionType.Value )

class 包含枚举对象。似乎使用 GSON 序列化会导致 Whosebug 由于某种循环依赖。

val gson = new Gson
      val jsonBody = gson.toJson(producerConfig)
      println(jsonBody)

下面是我得到的堆栈跟踪。我看到这个 question here and answer 除了解决方案似乎是 Java 解决方案并且不适用于 scala。有人可以澄清一下吗?

17:10:04.475 [ERROR] i.g.a.Gatling$ - Run crashed
java.lang.WhosebugError: null
        at com.google.gson.stream.JsonWriter.beforeName(JsonWriter.java:617)
        at com.google.gson.stream.JsonWriter.writeDeferredName(JsonWriter.java:400)
        at com.google.gson.stream.JsonWriter.value(JsonWriter.java:526)
        at com.google.gson.internal.bind.TypeAdapters.write(TypeAdapters.java:233)
        at com.google.gson.internal.bind.TypeAdapters.write(TypeAdapters.java:218)
        at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:69)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.write(ReflectiveTypeAdapterFactory.java:127)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:245)
        at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:69)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.write(ReflectiveTypeAdapterFactory.java:127)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:245)
        at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:69)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.write(ReflectiveTypeAdapterFactory.java:127)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:245)
        at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:69)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.write(ReflectiveTypeAdapterFactory.java:127)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:245)
        at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:69)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.write(ReflectiveTypeAdapterFactory.java:127)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:245)
        at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:69)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.write(ReflectiveTypeAdapterFactory.java:127)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:245)
        at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:69)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.write(ReflectiveTypeAdapterFactory.java:127)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:245)

我不是 Scala 专家,但我认为在这里使用 Gson 是一个错误的工具。

  • 首先,Gson 不知道 scala.Enumeration 因此将其作为可使用反射遍历的常规数据包处理。
  • 其次,没有一种简单的(如果有的话?)反序列化到原始值状态的方法(如果您只打算生产而不使用 JSON 文档,可以忽略)。

原因如下:

object Single
        extends Enumeration {

    val Only = Value

}
final class Internals {

    private Internals() {
    }

    static void inspect(final Object o, final Excluder excluder, final boolean serialize)
            throws IllegalAccessException {
        inspect(o, clazz -> !excluder.excludeClass(clazz, serialize), field -> !excluder.excludeField(field, serialize));
    }

    static void inspect(final Object o, final Predicate<? super Class<?>> inspectClass, final Predicate<? super Field> inspectField)
            throws IllegalAccessException {
        for ( Class<?> c = o.getClass(); c != null; c = c.getSuperclass() ) {
            if ( !inspectClass.test(c) ) {
                continue;
            }
            System.out.println(c);
            for ( final Field f : c.getDeclaredFields() ) {
                if ( !inspectField.test(f) ) {
                    continue;
                }
                f.setAccessible(true);
                System.out.printf("\t%s: %s\n", f, f.get(o));
            }
        }
    }

}
final Object value = Single.Only();
Internals.inspect(value, gson.excluder(), true);

产生:

class scala.Enumeration$Val
    private final int scala.Enumeration$Val.i: 0
    private final java.lang.String scala.Enumeration$Val.name: null
class scala.Enumeration$Value
    private final scala.Enumeration scala.Enumeration$Value.scala$Enumeration$$outerEnum: Single
class java.lang.Object

如您所见,有两个关键字段:

  • private final java.lang.String scala.Enumeration$Val.name 给出 null 除非命名(尽管可以使用 toString 获得枚举元素)。
  • private final scala.Enumeration scala.Enumeration$Value.scala$Enumeration$$outerEnum 实际上是对具体枚举外部 class 的引用(这实际上是无限递归的原因,因此是堆栈溢出错误)。

这两个阻止正确的反序列化。 至少可以通过三种方式获取外层枚举类型:

  • 要么为 所有 类型实现自定义类型适配器,这些类型可以包含此类枚举(对于数据包来说很容易(Scala 中的情况 classes?),因为字段已经包含类型信息,尽管 Gson 对此提供了很差的支持;不适用于像上面这样的单个原始文字或集合);
  • 或将外部枚举名称烘焙为 JSON,其中包含名称和外部类型的两个条目。

后者可以这样做(在 Java 中,希望在 Scala 中很容易简化它):

final class ScalaStuff {

    private static final Field outerEnumField;
    private static final Map<String, Method> withNameMethodCache = new ConcurrentHashMap<>();

    static {
        try {
            outerEnumField = Enumeration.Value.class.getDeclaredField("scala$Enumeration$$outerEnum");
            outerEnumField.setAccessible(true);
        } catch ( final NoSuchFieldException ex ) {
            throw new RuntimeException(ex);
        }
    }

    private ScalaStuff() {
    }

    @Nonnull
    static String toEnumerationName(@Nonnull final Enumeration.Value value) {
        try {
            final Class<? extends Enumeration> aClass = ((Enumeration) outerEnumField.get(value)).getClass();
            final String typeName = aClass.getTypeName();
            final int length = typeName.length();
            assert !typeName.isEmpty() && typeName.charAt(length - 1) == '$';
            return typeName.substring(0, length - 1);
        } catch ( final IllegalAccessException ex ) {
            throw new RuntimeException(ex);
        }
    }

    @Nonnull
    static Enumeration.Value fromEnumerationValue(@Nonnull final String type, @Nonnull final String enumerationName)
            throws ClassNotFoundException, NoSuchMethodException {
        // using get for exception propagation cleanliness; computeIfAbsent would complicate exception handling
        @Nullable
        final Method withNameMethodCandidate = withNameMethodCache.get(type);
        final Method withNameMethod;
        if ( withNameMethodCandidate != null ) {
            withNameMethod = withNameMethodCandidate;
        } else {
            final Class<?> enumerationClass = Class.forName(type);
            withNameMethod = enumerationClass.getMethod("withName", String.class);
            withNameMethodCache.put(type, withNameMethod);
        }
        try {
            return (Enumeration.Value) withNameMethod.invoke(null, enumerationName);
        } catch ( final IllegalAccessException | InvocationTargetException ex ) {
            throw new RuntimeException(ex);
        }
    }

}
final class ScalaEnumerationTypeAdapterFactory
        implements TypeAdapterFactory {

    private static final TypeAdapterFactory instance = new ScalaEnumerationTypeAdapterFactory();

    private ScalaEnumerationTypeAdapterFactory() {
    }

    static TypeAdapterFactory getInstance() {
        return instance;
    }

    @Override
    @Nullable
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        if ( !Enumeration.Value.class.isAssignableFrom(typeToken.getRawType()) ) {
            return null;
        }
        @SuppressWarnings("unchecked")
        final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) Adapter.instance;
        return typeAdapter;
    }

    private static final class Adapter
            extends TypeAdapter<Enumeration.Value> {

        private static final TypeAdapter<Enumeration.Value> instance = new Adapter()
                .nullSafe();

        private Adapter() {
        }

        @Override
        public void write(final JsonWriter out, final Enumeration.Value value)
                throws IOException {
            out.beginObject();
            out.name("type");
            out.value(ScalaStuff.toEnumerationName(value));
            out.name("name");
            out.value(value.toString());
            out.endObject();
        }

        @Override
        public Enumeration.Value read(final JsonReader in)
                throws IOException {
            in.beginObject();
            @Nullable
            String type = null;
            @Nullable
            String name = null;
            while ( in.hasNext() ) {
                switch ( in.nextName() ) {
                case "type":
                    type = in.nextString();
                    break;
                case "name":
                    name = in.nextString();
                    break;
                default:
                    in.skipValue();
                    break;
                }
            }
            in.endObject();
            if ( type == null || name == null ) {
                throw new JsonParseException("Insufficient enum data: " + type + ", " + name);
            }
            try {
                return ScalaStuff.fromEnumerationValue(type, name);
            } catch ( final ClassNotFoundException | NoSuchMethodException ex ) {
                throw new JsonParseException(ex);
            }
        }

    }

}

将通过以下 JUnit 5 测试:

private static final Gson gson = new GsonBuilder()
        .disableHtmlEscaping()
        .registerTypeAdapterFactory(ScalaEnumerationTypeAdapterFactory.getInstance())
        .create();

@Test
public void test() {
    final Enumeration.Value before = Single.Only();
    final String json = gson.toJson(before);
    System.out.println(json);
    final Enumeration.Value after = gson.fromJson(json, Enumeration.Value.class);
    Assertions.assertSame(before, after);
}

其中 json 变量将包含以下 JSON 负载:

{"type":"Single","name":"Only"}

上面的ScalaStuff class很可能不完整。有关 Scala 和 Gson 的影响,请参阅


更新 1

由于假设 JSON 消费者可以自己处理枚举反序列化,因此您不需要使用生成的 JSON 文档,因此您可以生成一个比生成无名更具描述性的枚举值名称整数。只需将上面的 Adapter 替换为:

private static final class Adapter
        extends TypeAdapter<Enumeration.Value> {

    private static final TypeAdapter<Enumeration.Value> instance = new Adapter()
            .nullSafe();

    private Adapter() {
    }

    @Override
    public void write(final JsonWriter out, final Enumeration.Value value)
            throws IOException {
        out.value(value.toString());
    }

    @Override
    public Enumeration.Value read(final JsonReader in) {
        throw new UnsupportedOperationException();
    }

}

然后下面的测试将是绿色的:

Assertions.assertEquals("\"Only\"", gson.toJson(Single.Only()));