【问题标题】:Gson with Scala causes StackOverflow for Enumerations带有 Scala 的 Gson 导致 StackOverflow 的枚举
【发布时间】:2020-07-28 00:32:03
【问题描述】:

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

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

并且我有想要在 JSON 中序列化的类

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

该类包括 Enum 对象。由于某些循环依赖,使用 GSON 进行序列化似乎会导致 StackOverflow。

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.StackOverflowError: 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$7.write(TypeAdapters.java:233)
        at com.google.gson.internal.bind.TypeAdapters$7.write(TypeAdapters.java:218)
        at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:69)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.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$1.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$1.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$1.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$1.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$1.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$1.write(ReflectiveTypeAdapterFactory.java:127)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:245)

【问题讨论】:

    标签: scala gson stack-overflow


    【解决方案1】:

    我不是 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 实际上是对具体枚举外部类的引用(这实际上是无限递归和堆栈溢出错误的原因)。

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

    • 所有 类型实现自定义类型适配器,这些类型可以包含此类枚举(对于数据包(Scala 中的案例类?)非常容易,因为字段已经包含类型信息,尽管 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 类很可能不完整。在 how to deserialize a json string that contains @@ with scala' 上查看更多关于 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()));
    

    【讨论】:

    • 哇,非常感谢。我没有意识到 Gson 对 Scala 枚举器有多么糟糕。为了解除对自己的阻塞,我删除了枚举器,而只使用了整数值。我只生成 JSON 而不是反序列化。
    • @SaherAhwal 好吧,Gson 并不是为与 Scala 一起使用而设计的,因为它仍然很好地支持开箱即用的 Java“本机”枚举。如果不需要对生成的枚举进行反序列化,也可能不需要保留类型信息,请看答案更新。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-19
    • 1970-01-01
    相关资源
    最近更新 更多