Gson使用存根序列化循环引用 [英] Gson Serialize Circular References Using Stubs

查看:120
本文介绍了Gson使用存根序列化循环引用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我试图实现一些简单的Json序列化功能,但我很难应付Gson的大规模复杂性。



所以基本上我有许多实体类相互引用大量的循环引用。要将此结构序列化为JSON,我想跟踪已经序列化的对象。实体类都实现了一个名为 Identified 的接口,它有一个方法 String getId(),它提供了一个全局唯一的id。因此,在对一个根元素进行序列化时,我想将所有遇到的ID保存在 Set 中,并根据该集合决定是完全序列化一个对象还是将该对象序列化为一个存根




something:{
__stub:true,
id :...
}



在我看来,这不应该太难,但我一直无法把东西放在一起。使用自定义 JsonSerializer 我无法使用默认方式序列化一个对象(不会被序列化为存根)。使用 TypeAdapterFactory ,我无法访问实际的对象。



实现这一点,将是非常好的!



最好的问候

解决方案

我不确定这是否可能很容易。据我所知,Gson提倡不变性,似乎缺乏自定义序列化上下文支持(至少我不知道是否可以使用自定义 JsonSerializationContext 尽可能)。因此,可能的解决方法之一可能如下:

IIdentifiable.java



一个简单的界面请求一个对象的自定义ID。

  interface IIdentifiable< ID> {

ID getId();

$ b $ / code>



Entity.java



一个简单的实体,可以以两种方式引用另一个实体引用:


  • 直接依赖于next实体;

  • 其他引用的引用的集合。
覆盖> final class实体
实现IIdentifiable< String> {

@SerializedName(ID_PROPERTY_NAME)
private final String id;

private final Collection< Entity> entities = new ArrayList<>();
下一个私人实体;

私人实体(最终字符串id){
this.id = id;


static Entity entity(final String id){
return new Entity(id);
}

@Override
public String getId(){
return id;
}

实体setAll(final Entity ... entities){
this.entities.clear();
this.entities.addAll(asList(entities));
返回此;
}

实体setNext(下一个实体){
this.next = next;
返回此;




$ b

IdentitySerializingTypeAdapterFactory.java



我没有找到更简单的方法,而不是将它变成一个类型的适配器工厂,不幸的是,这个实现是完全有状态的,而不能是 final class IdentitySerializingTypeAdapterFactory
实现TypeAdapterFactory {$ b $} $ {

b
private final Collection< Object> traversedEntityIds = new HashSet<>();

private IdentitySerializingTypeAdapterFactory(){
}

static TypeAdapterFactory identitySerializingTypeAdapterFactory(){
return new IdentitySerializingTypeAdapterFactory();
}

@Override
public< T> TypeAdapter< T> create(final Gson gson,final TypeToken< T> typeToken){
final boolean isIdentifiable = IIdentifiable.class.isAssignableFrom(typeToken.getRawType());
final TypeAdapter< T> delegateAdapter = gson.getDelegateAdapter(this,typeToken);
if(isIdentifiable){
return new TypeAdapter< T>(){
@Override
public void write(final JsonWriter out,final T value)
throws IOException {
final IIdentifiable<?>可识别的=(II可识别的)值;
final Object id = identifiable.getId();
if(!traversedEntityIds.contains(id)){
delegateAdapter.write(out,value);
traversedEntityIds.add(id);
} else {
out.beginObject();
out.name(REF_ID_PROPERTY_NAME);
writeSimpleValue(out,id);
out.endObject();



@Override
public T read(final JsonReader in){
throw new UnsupportedOperationException();
}
};
}
返回delegateAdapter;
}

}

类型适配器首先尝试检查如果给定的实体已经被遍历。如果是的话,那么它正在写一个类似于你的特殊对象(当然,行为可以通过战略模式重写,但让它更简单)。如果不是,则获得默认类型适配器,然后将给定实体委派给该适配器,并且如果后者类型的适配器成功,则将其注册为遍历的适配器。



其余部分



以下是其余部分。


$ b

SystemNames.java



final class SystemNames {

Private SystemNames(){
}

private static final String SYSTEM_PREFIX =__ $;

static final String ID_PROPERTY_NAME = SYSTEM_PREFIX +id;
static final String REF_ID_PROPERTY_NAME = SYSTEM_PREFIX +refId;

}



GsonJsonWriters.java



  final class GsonJsonWriters {

private GsonJsonWriters(){
}

static void writeSimpleValue(final JsonWriter writer,final Object value)
抛出IOException {
if(value == null){
writer.nullValue();
} else if(value instanceof Double){
writer.value((double)value);
} else if(value instanceof Long){
writer.value((long)value);
} else if(value instanceof String){
writer.value((String)value);
} else if(value instanceof Boolean){
writer.value((Boolean)value);
} else if(value instanceof Number){
writer.value((Number)value);
} else {
throw new IllegalArgumentException(无法处理+ value类型的值);



$ b $ / code>



测试



在下面的测试中,有三个实体由 FOO BAR BAZ 字符串标识符。它们都具有这样的循环依赖关系:


  • FOO - > BAR BAR - > BAZ BAZ - > FOO 使用下一步属性;

  • FOO - > [BAR,BAZ] BAR - > [FOO,BAZ] BAZ - > [FOO,BAR] 使用实体属性。



适配器工厂是有状态的,甚至 GsonBuilder 必须从头开始创建,因此在使用之间不会有被破坏状态。简单地说,一旦Gson实例被使用一次,它就必须被处置,所以在下面的测试中有 GsonBuilder 供应商。

  public final class Q41213747Test {

private static final Entity foo = entity(FOO);
private static final Entity bar = entity(BAR);
private static final实体baz = entity(BAZ);

static {
foo.setAll(bar,baz).setNext(bar);
bar.setAll(foo,baz).setNext(baz);
baz.setAll(foo,bar).setNext(foo);
}

@Test
public void testSerializeSameJson(){
final String json1 = newSerializingGson()。toJson(foo);
final String json2 = newSerializingGson()。toJson(foo);
assertThat(必须是相同的,因为GSON实例是有状态的,json1,是(json2));


@Test
public void testSerializeNotSameJson(){
final Gson gson = newSerializingGson();
final String json1 = gson.toJson(foo);
final String json2 = gson.toJson(foo);
assertThat(因为GSON实例是有状态的,所以在调用之间不能相同,json1是(not(json2)));


@Test
public void testOutput(){
out.println(newSerializingGson()。toJson(foo));


private static Gson newSerializingGson(){
return newSerializingGson(GsonBuilder :: new);

$ b private static Gson newSerializingGson(final Supplier< GsonBuilder> defaultGsonBuilderSupplier){
return defaultGsonBuilderSupplier.get()
.registerTypeAdapterFactory(identitySerializingTypeAdapterFactory())
.create();
}

}



 __ $ id:FOO,
entities:[
{
__ $ id:BAR,
实体:[
{
__ $ refId:FOO
},
{
__ $ id:BAZ,
实体:[
{
__ $ refId:FOO
},
{
__ $ refId:BAR

$,
next:{
__ $ refId:FOO
}
}
],
next:{
__ $ refId:BAZ
}
},
{
__ $ refId:BAZ
}
],
next:{
__ $ refId:BAR
}
}

反序列化这些东西看起来非常复杂。至少使用GSON设施。




您是否考虑重新考虑您的JSON模型以避免JSON输出中的循环依赖?也许把你的对象分解成一个映射,比如 Map< ID,Object> ,并使引用变为transient或 @Expose --annotated对你来说可能更容易使用?它也会简化反序列化。

I'm trying to implement some simple Json serialization functionality but I'm having a hard time coping with the massive complexity of Gson.

So basically I have a bunch of Entity classes which reference each other with a lot of circular reference. To serialize this structure to JSON I want to keep track of the objects already serialized. The Entity classes all implement an interface called Identified which has one method String getId() giving a globally unique id. So during serializiation of one root element, I want to store all encountered ids in a Set and decide based on that set, whether to fully serialize an object or to serialize that object as a stub

"something": { "__stub": "true", "id": "..." }

This shouldn't be too hard a task in my opinion, but I haven't been able to put something together. Using a custom JsonSerializer I'm not able to have an object (that is not to be serialized as a stub) serialized in the default way. Using a TypeAdapterFactory, I'm not able to access the actual object.

So, any help on how to achieve this, would be very nice!

Best regards

解决方案

I'm not sure if it's possible easily. As far as I know, Gson promotes immutability and seems to lack custom serialization context support (at least I don't know if it's possible to use custom JsonSerializationContext wherever possible). Thus, one of possible work-around might be the following:

IIdentifiable.java

A simple interface to request a custom ID for an object.

interface IIdentifiable<ID> {

    ID getId();

}

Entity.java

A simple entity that can hold another entity references in two manners:

  • a direct dependency to a "next" entity;
  • a collection of references to other references.

final class Entity
        implements IIdentifiable<String> {

    @SerializedName(ID_PROPERTY_NAME)
    private final String id;

    private final Collection<Entity> entities = new ArrayList<>();
    private Entity next;

    private Entity(final String id) {
        this.id = id;
    }

    static Entity entity(final String id) {
        return new Entity(id);
    }

    @Override
    public String getId() {
        return id;
    }

    Entity setAll(final Entity... entities) {
        this.entities.clear();
        this.entities.addAll(asList(entities));
        return this;
    }

    Entity setNext(final Entity next) {
        this.next = next;
        return this;
    }

}

IdentitySerializingTypeAdapterFactory.java

I didn't find any easier way rather than making it a type adapter factory, and, unfortunately, this implementation is totally stateful and cannot be reused.

final class IdentitySerializingTypeAdapterFactory
        implements TypeAdapterFactory {

    private final Collection<Object> traversedEntityIds = new HashSet<>();

    private IdentitySerializingTypeAdapterFactory() {
    }

    static TypeAdapterFactory identitySerializingTypeAdapterFactory() {
        return new IdentitySerializingTypeAdapterFactory();
    }

    @Override
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        final boolean isIdentifiable = IIdentifiable.class.isAssignableFrom(typeToken.getRawType());
        final TypeAdapter<T> delegateAdapter = gson.getDelegateAdapter(this, typeToken);
        if ( isIdentifiable ) {
            return new TypeAdapter<T>() {
                @Override
                public void write(final JsonWriter out, final T value)
                        throws IOException {
                    final IIdentifiable<?> identifiable = (IIdentifiable<?>) value;
                    final Object id = identifiable.getId();
                    if ( !traversedEntityIds.contains(id) ) {
                        delegateAdapter.write(out, value);
                        traversedEntityIds.add(id);
                    } else {
                        out.beginObject();
                        out.name(REF_ID_PROPERTY_NAME);
                        writeSimpleValue(out, id);
                        out.endObject();
                    }
                }

                @Override
                public T read(final JsonReader in) {
                    throw new UnsupportedOperationException();
                }
            };
        }
        return delegateAdapter;
    }

}

The type adapter firstly tries to check if a given entity has been already traversed. If yes, then it's writing a special object similar to your one (the behavior could be rewritten via the strategy pattern, of course, but let it be more simple). If no, then the default type adapter is obtained, and then the given entity is delegated to that adapter, and registered as a traversed one if the latter type adapter succeeds.

The rest

And here is the rest.

SystemNames.java

final class SystemNames {

    private SystemNames() {
    }

    private static final String SYSTEM_PREFIX = "__$";

    static final String ID_PROPERTY_NAME = SYSTEM_PREFIX + "id";
    static final String REF_ID_PROPERTY_NAME = SYSTEM_PREFIX + "refId";

}

GsonJsonWriters.java

final class GsonJsonWriters {

    private GsonJsonWriters() {
    }

    static void writeSimpleValue(final JsonWriter writer, final Object value)
            throws IOException {
        if ( value == null ) {
            writer.nullValue();
        } else if ( value instanceof Double ) {
            writer.value((double) value);
        } else if ( value instanceof Long ) {
            writer.value((long) value);
        } else if ( value instanceof String ) {
            writer.value((String) value);
        } else if ( value instanceof Boolean ) {
            writer.value((Boolean) value);
        } else if ( value instanceof Number ) {
            writer.value((Number) value);
        } else {
            throw new IllegalArgumentException("Cannot handle values of type " + value);
        }
    }

}

Testing

In the test below, there are three entities identified by FOO, BAR, and BAZ string identifiers. All of them have circular dependencies like this:

  • FOO -> BAR, BAR -> BAZ, BAZ -> FOO using the next property;
  • FOO -> [BAR, BAZ], BAR -> [FOO, BAZ], BAZ -> [FOO, BAR] using the entities property.

Since the type adapter factory is stateful, even GsonBuilder must be created from scratch thus not having "spoiled" state between use. Simply speaking, once a Gson instance is used once, it must be disposed, so there are GsonBuilder suppliers in the test below.

public final class Q41213747Test {

    private static final Entity foo = entity("FOO");
    private static final Entity bar = entity("BAR");
    private static final Entity baz = entity("BAZ");

    static {
        foo.setAll(bar, baz).setNext(bar);
        bar.setAll(foo, baz).setNext(baz);
        baz.setAll(foo, bar).setNext(foo);
    }

    @Test
    public void testSerializeSameJson() {
        final String json1 = newSerializingGson().toJson(foo);
        final String json2 = newSerializingGson().toJson(foo);
        assertThat("Must be the same between the calls because the GSON instances are stateful", json1, is(json2));
    }

    @Test
    public void testSerializeNotSameJson() {
        final Gson gson = newSerializingGson();
        final String json1 = gson.toJson(foo);
        final String json2 = gson.toJson(foo);
        assertThat("Must not be the same between the calls because the GSON instance is stateful", json1, is(not(json2)));
    }

    @Test
    public void testOutput() {
        out.println(newSerializingGson().toJson(foo));
    }

    private static Gson newSerializingGson() {
        return newSerializingGson(GsonBuilder::new);
    }

    private static Gson newSerializingGson(final Supplier<GsonBuilder> defaultGsonBuilderSupplier) {
        return defaultGsonBuilderSupplier.get()
                .registerTypeAdapterFactory(identitySerializingTypeAdapterFactory())
                .create();
    }

}

{
    "__$id": "FOO",
    "entities": [
        {
            "__$id": "BAR",
            "entities": [
                {
                    "__$refId": "FOO"
                },
                {
                    "__$id": "BAZ",
                    "entities": [
                        {
                            "__$refId": "FOO"
                        },
                        {
                            "__$refId": "BAR"
                        }
                    ],
                    "next": {
                        "__$refId": "FOO"
                    }
                }
            ],
            "next": {
                "__$refId": "BAZ"
            }
        },
        {
            "__$refId": "BAZ"
        }
    ],
    "next": {
        "__$refId": "BAR"
    }
}

Deserialization of such stuff looks really complicated. At least using GSON facilities.


Do you consider rethinking your JSON model in order to avoid circular dependencies in JSON output? Maybe decomposing your objects to a single map like Map<ID, Object> and making references transient or @Expose-annotated could be easier for you to use? It would simplify deserialization as well.

这篇关于Gson使用存根序列化循环引用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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