问题
How can I serialize a tree (implemented in Java using the DefaultMutableTreeNode class) to JSON (for transferring via RESTful method to an iOS client)?
I tried:
String jsonString = (new Gson()).toJson(topNode);
// topNode is DefaultMutableTreeNode at the root
It crashed with StackOverflowError.
回答1:
Swing's DefaultMutableTreeNode class is a tree-like data structure
which contains instances of this same type both as children and as parent.
That's why Gson's default serializer ran into infinite recursion
and hence threw a StackOverflowError.
To solve this problem you need to customize your Gson with a smarter JsonSerializer
specially crafted for converting a DefaultMutableTreeNode to JSON.
As a bonus you might also want to provide a JsonDeserializer
for converting such JSON back to a DefaultMutableTreeNode.
For that create your Gson instance not just by new Gson(), but by
Gson gson = new GsonBuilder()
.registerTypeAdapter(DefaultMutableTreeNode.class, new DefaultMutableTreeNodeSerializer())
.registerTypeAdapter(DefaultMutableTreeNode.class, new DefaultMutableTreeNodeDeserializer())
.setPrettyPrinting()
.create();
The DefaultMutableTreeNodeSerializer below is responsible
for converting a DefaultMutableTreeNode to JSON.
It converts its properties allowsChildren, userObject and children to JSON.
Note that it does not convert the parent property to JSON,
because doing that would produce an inifinite recursion again.
public class DefaultMutableTreeNodeSerializer implements JsonSerializer<DefaultMutableTreeNode> {
@Override
public JsonElement serialize(DefaultMutableTreeNode src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("allowsChildren", src.getAllowsChildren());
jsonObject.add("userObject", context.serialize(src.getUserObject()));
if (src.getChildCount() > 0) {
jsonObject.add("children", context.serialize(Collections.list(src.children())));
}
return jsonObject;
}
}
For testing let us serialize the root node of a sample JTree to JSON,
and then deserialize it again.
JTree tree = new JTree(); // create a sample tree
Object topNode = tree.getModel().getRoot(); // a DefaultMutableTreeNode
String jsonString = gson.toJson(topNode);
System.out.println(jsonString);
DefaultMutableTreeNode topNode2 = gson.fromJson(jsonString, DefaultMutableTreeNode.class);
It generates the following JSON output:
{
"allowsChildren": true,
"userObject": "JTree",
"children": [
{
"allowsChildren": true,
"userObject": "colors",
"children": [
{
"allowsChildren": true,
"userObject": "blue"
},
{
"allowsChildren": true,
"userObject": "violet"
},
{
"allowsChildren": true,
"userObject": "red"
},
{
"allowsChildren": true,
"userObject": "yellow"
}
]
},
{
"allowsChildren": true,
"userObject": "sports",
"children": [
{
"allowsChildren": true,
"userObject": "basketball"
},
{
"allowsChildren": true,
"userObject": "soccer"
},
{
"allowsChildren": true,
"userObject": "football"
},
{
"allowsChildren": true,
"userObject": "hockey"
}
]
},
{
"allowsChildren": true,
"userObject": "food",
"children": [
{
"allowsChildren": true,
"userObject": "hot dogs"
},
{
"allowsChildren": true,
"userObject": "pizza"
},
{
"allowsChildren": true,
"userObject": "ravioli"
},
{
"allowsChildren": true,
"userObject": "bananas"
}
]
}
]
}
The DefaultMutableTreeNodeDeserializer below is responsible
for converting JSON back to a DefaultMutableTreeNode.
It uses the same idea as the deserializer from
How to serialize/deserialize a DefaultMutableTreeNode with Jackson?.
The DefaultMutableTreeNode is not very POJO-like and thus doesn't
work well together with Gson.
Therefore it uses a well-behaving POJO helper class (with properties
allowsChildren, userObject and children) and lets Gson
deserialize the JSON content into this class.
Then the POJO object (and its POJO children) is converted to a
DefaultMutableTreeNode object (with DefaultMutableTreeNode children).
public class DefaultMutableTreeNodeDeserializer implements JsonDeserializer<DefaultMutableTreeNode> {
@Override
public DefaultMutableTreeNode deserialize(JsonElement json, Type type, JsonDeserializationContext context) {
return context.<POJO>deserialize(json, POJO.class).toDefaultMutableTreeNode();
}
private static class POJO {
private boolean allowsChildren;
private Object userObject;
private List<POJO> children;
// no need for: POJO parent
public DefaultMutableTreeNode toDefaultMutableTreeNode() {
DefaultMutableTreeNode node = new DefaultMutableTreeNode();
node.setAllowsChildren(allowsChildren);
node.setUserObject(userObject);
if (children != null) {
for (POJO child : children) {
node.add(child.toDefaultMutableTreeNode()); // recursion!
// this did also set the parent of the child-node
}
}
return node;
}
// Following setters needed by Gson's deserialization:
public void setAllowsChildren(boolean allowsChildren) {
this.allowsChildren = allowsChildren;
}
public void setUserObject(Object userObject) {
this.userObject = userObject;
}
public void setChildren(List<POJO> children) {
this.children = children;
}
}
}
回答2:
This is an improved alternative to my older answer which used implementations of JsonSerializer and JsonDeserializer for DefaultMutableTreeNode.
The API doc of these 2 interfaces says:
New applications should prefer TypeAdapter, whose streaming API is more efficient than this interface's tree API.
Let's therefore use this preferred approach and implement a
TypeAdapter for DefaultMutableTreeNode.
For using it you create your Gson instance like this
(instead of just using new Gson()):
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(DefaultMutableTreeNodeTypeAdapter.FACTORY)
.setPrettyPrinting()
.create();
The DefaultMutableTreeNodeTypeAdapter below is responsible for
converting a DefaultMutableTreeNode to and from JSON.
It writes/reads its properties allowsChildren, userObject and children.
There is no need to write the parent property, because the parent-child
relations are already encoded in the nested structure of the JSON-output.
public class DefaultMutableTreeNodeTypeAdapter extends TypeAdapter<DefaultMutableTreeNode> {
public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() {
@Override
@SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if (type.getRawType() == DefaultMutableTreeNode.class) {
return (TypeAdapter<T>) new DefaultMutableTreeNodeTypeAdapter(gson);
}
return null;
}
};
private final Gson gson;
private DefaultMutableTreeNodeTypeAdapter(Gson gson) {
this.gson = gson;
}
@Override
public void write(JsonWriter out, DefaultMutableTreeNode node) throws IOException {
out.beginObject();
out.name("allowsChildren");
out.value(node.getAllowsChildren());
out.name("userObject");
gson.toJson(node.getUserObject(), Object.class, out);
if (node.getChildCount() > 0) {
out.name("children");
gson.toJson(Collections.list(node.children()), List.class, out); // recursion!
}
// No need to write node.getParent(), it would lead to infinite recursion.
out.endObject();
}
@Override
public DefaultMutableTreeNode read(JsonReader in) throws IOException {
in.beginObject();
DefaultMutableTreeNode node = new DefaultMutableTreeNode();
while (in.hasNext()) {
switch (in.nextName()) {
case "allowsChildren":
node.setAllowsChildren(in.nextBoolean());
break;
case "userObject":
node.setUserObject(gson.fromJson(in, Object.class));
break;
case "children":
in.beginArray();
while (in.hasNext()) {
node.add(read(in)); // recursion!
// this did also set the parent of the child-node
}
in.endArray();
break;
default:
in.skipValue();
break;
}
}
in.endObject();
return node;
}
}
来源:https://stackoverflow.com/questions/53997112/how-to-serialize-defaultmutabletreenode-java-to-json