带有 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()));
我在 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()));