如何将java.util.Properties对象序列化为JSON文件并反序列化回来?

4

我有一个类型为java.util.Properties的变量。我正在尝试将其写入JSON文件,并从该文件中读取。

Properties变量看起来像下面这样:

Properties inner3 = new Properties();
inner3.put("i1", 1);
inner3.put("i2", 100);

Properties inner2 = new Properties();
inner2.put("aStringProp", "aStringValue");
inner2.put("inner3", inner3);

Properties inner1 = new Properties();
inner1.put("aBoolProp", true);
inner1.put("inner2", inner2);

Properties topLevelProp = new Properties();
topLevelProp.put("count", 1000000);
topLevelProp.put("size", 1);
topLevelProp.put("inner1", inner1);

当我将topLevelProp序列化为JSON时,我期望结果如下。
{
  "inner1": {
    "inner2": {
      "aStringProp": "aStringValue",
      "inner3": {
        "i2": 100,
        "i1": 1
      }
    },
    "aBoolProp": true
  },
  "size": 1,
  "count": 1000000
}

上述JSON结果可以通过使用Gson以相当直接的方式生成,但是当它被提供相同的JSON字符串进行反序列化时,它会失败。
Gson gson = new GsonBuilder().create();
String json = gson.toJson(topLevelProp); //{"inner1":{"inner2":{"aStringProp":"aStringValue","inner3":{"i2":100,"i1":1}},"aBoolProp":true},"size":1,"count":1000000}

//following line throws error: Expected a string but was BEGIN_OBJECT at line 1 column 12 path $.
Properties propObj = gson.fromJson(json, Properties.class); 

尝试使用Jackson:
ObjectMapper mapper = new ObjectMapper();
mapper.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true);
mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
File file = new File("configs/config1.json");
mapper.writeValue(file, topLevelProp);

最后一行代码报错:

com.fasterxml.jackson.databind.JsonMappingException: java.util.Properties cannot be cast to java.lang.String (through reference chain: java.util.Properties["inner1"])

尝试按以下方式将其反序列化为字符串,但由于以下错误而失败:

Properties jckProp = JsonSerializer.mapper.readValue(json, Properties.class);

无法从START_OBJECT令牌中反序列化java.lang.String实例 在[来源:{"inner1":{"inner2":{"aStringProp":"aStringValue","inner3":{"i2":100,"i1":1}},"aBoolProp":true},"size":1,"count":1000000}; line: 1, column: 11] (通过引用链:java.util.Properties["inner1"]) 如何处理这个问题?
更新:根据 cricket_007的想法, 找到com.fasterxml.jackson.databind.node.ObjectNode, 可以按以下方式使用:
ObjectNode jckProp = JsonSerializer.mapper.readValue(json, ObjectNode.class);
System.out.println(jckProp.get("size").asInt());
System.out.println("jckProp: " + jckProp);
System.out.println("jckProp.inner: " + jckProp.get("inner1"));

我认为这对我来说可能是未来的发展方向,因为我大多数时候需要从JSON文件中读取内容。


1
(虽然与您的问题无直接关系,但是...)引用 Properties 的 javadoc由于 Properties 继承自 Hashtable,因此 put 和 putAll 方法可以应用于 Properties 对象。强烈不建议使用它们,因为它们允许调用者插入键或值不是字符串的条目。 - Sean Bright
为什么不能只使用JSON对象作为伪属性对象? - OneCricketeer
@SeanBright 我必须在一个严重依赖属性的项目中工作。我认为最好使用一个适当的配置对象(可能是一个简单的POJO),这实际上是我在这种情况下的备选方案。 - Sayan Pal
@cricket_007,不确定您所说的伪属性对象是什么意思。如果有参考资料那就太好了。 - Sayan Pal
1
只需直接使用JSONObject类即可。存储实际的JSON数据。如有需要,可以创建一个JSONObject getProperties()方法。 - OneCricketeer
显示剩余4条评论
3个回答

6
您遇到的问题是错误使用了java.util.Properties:它不是多级树形结构,而是一个简单的字符串键值对映射。 因此,虽然在技术上可能添加非字符串属性值(部分原因是因为该类是在Java泛型之前添加的,这使得它允许更好的类型安全性),但不应该这样做。对于嵌套结构,应使用java.util.Map或特定的树形数据结构。
至于Properties,javadoc中例如这样写道:
The Properties class represents a persistent set of properties.
The Properties can be saved to a stream or loaded from a stream.
Each key and its corresponding value in the property list is a string.
...
If the store or save method is called on a "compromised" Properties    
object that contains a non-String key or value, the call will fail. 

现在:如果您有这样的“受损”Properties实例,使用Jackson或Gson最好的选择是构建一个java.util.Map(或者可能是旧的Hashtable),并进行序列化。这样做应该不会出现问题。


2

正如StaxMan所说,你误用了Properties类,并且由于缺乏类型信息,你很可能会遇到严重的问题。然而,对于弱类型映射,你也可能面临同样的情况。 如果这对你来说是必须的,那么你可以使用自定义的Gson JsonDeserializer(请注意JSON数组问题):

final class PropertiesJsonDeserializer
        implements JsonDeserializer<Properties> {

    private static final JsonDeserializer<Properties> propertiesJsonDeserializer = new PropertiesJsonDeserializer();

    private PropertiesJsonDeserializer() {
    }

    static JsonDeserializer<Properties> getPropertiesJsonDeserializer() {
        return propertiesJsonDeserializer;
    }

    @Override
    public Properties deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
            throws JsonParseException {
        final Properties properties = new Properties();
        final JsonObject jsonObject = jsonElement.getAsJsonObject();
        for ( final Entry<String, JsonElement> e : jsonObject.entrySet() ) {
            properties.put(e.getKey(), parseValue(context, e.getValue()));
        }
        return properties;
    }

    private static Object parseValue(final JsonDeserializationContext context, final JsonElement valueElement) {
        if ( valueElement instanceof JsonObject ) {
            return context.deserialize(valueElement, Properties.class);
        }
        if ( valueElement instanceof JsonPrimitive ) {
            final JsonPrimitive valuePrimitive = valueElement.getAsJsonPrimitive();
            if ( valuePrimitive.isBoolean() ) {
                return context.deserialize(valueElement, Boolean.class);
            }
            if ( valuePrimitive.isNumber() ) {
                return context.deserialize(valueElement, Number.class); // depends on the JSON literal due to the lack of real number type info
            }
            if ( valuePrimitive.isString() ) {
                return context.deserialize(valueElement, String.class);
            }
            throw new AssertionError();
        }
        if ( valueElement instanceof JsonArray ) {
            throw new UnsupportedOperationException("Arrays are unsupported due to lack of type information (a generic list or a concrete type array?)");
        }
        if ( valueElement instanceof JsonNull ) {
            throw new UnsupportedOperationException("Nulls cannot be deserialized");
        }
        throw new AssertionError("Must never happen");
    }

}

因此,它可以像这样使用:
private static final Gson gson = new GsonBuilder()
        .registerTypeAdapter(Properties.class, getPropertiesJsonDeserializer())
        .create();

public static void main(final String... args) {
    final Properties outgoingProperties = createProperties();
    out.println(outgoingProperties);
    final String json = gson.toJson(outgoingProperties);
    out.println(json);
    final Properties incomingProperties = gson.fromJson(json, Properties.class);
    out.println(incomingProperties);
}

private static Properties createProperties() {
    final Properties inner3 = new Properties();
    inner3.put("i1", 1);
    inner3.put("i2", 100);
    final Properties inner2 = new Properties();
    inner2.put("aStringProp", "aStringValue");
    inner2.put("inner3", inner3);
    final Properties inner1 = new Properties();
    inner1.put("aBoolProp", true);
    inner1.put("inner2", inner2);
    final Properties topLevelProp = new Properties();
    topLevelProp.put("count", 1000000);
    topLevelProp.put("size", 1);
    topLevelProp.put("inner1", inner1);
    return topLevelProp;
}

以下是输出结果:

{inner1={inner2={aStringProp=aStringValue, inner3={i2=100, i1=1}}, aBoolProp=true}, size=1, count=1000000}
{"inner1":{"inner2":{"aStringProp":"aStringValue","inner3": {"i2":100,"i1":1}},"aBoolProp":true},"size":1,"count":1000000}
{inner1={inner2={aStringProp=aStringValue, inner3={i2=100, i1=1}}, aBoolProp=true}, size=1, count=1000000}


类型信息注入

如果您愿意在结果 JSON 中注入类型信息,那么可以保存一些类型信息。假设您愿意将数值存储为非原始类型,而是以两个键 _$T_$V 的形式存储为 JSON 对象,以持有属性的实际类型(确实是一个类,不是任何 java.reflect.Type,遗憾的是)和相关联的值,以便恢复属性的真实类型。这也可以应用于数组,但由于缺乏对某些参数化实例进行类型参数化的能力(除非您可以通过 Class 实例访问它),因此仍然无法包含参数化类型:

final class PropertiesJsonDeserializer
        implements JsonDeserializer<Properties> {

    private static final JsonDeserializer<Properties> propertiesJsonDeserializer = new PropertiesJsonDeserializer();

    private PropertiesJsonDeserializer() {
    }

    static JsonDeserializer<Properties> getPropertiesJsonDeserializer() {
        return propertiesJsonDeserializer;
    }

    @Override
    public Properties deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
            throws JsonParseException {
        final Properties properties = new Properties();
        final JsonObject jsonObject = jsonElement.getAsJsonObject();
        for ( final Entry<String, JsonElement> e : jsonObject.entrySet() ) {
            properties.put(e.getKey(), parseValue(context, e.getValue()));
        }
        return properties;
    }

    private static Object parseValue(final JsonDeserializationContext context, final JsonElement valueElement) {
        if ( valueElement instanceof JsonObject ) {
            return context.deserialize(valueElement, Properties.class);
        }
        if ( valueElement instanceof JsonPrimitive ) {
            final JsonPrimitive valuePrimitive = valueElement.getAsJsonPrimitive();
            if ( valuePrimitive.isBoolean() ) {
                return context.deserialize(valueElement, Boolean.class);
            }
            if ( valuePrimitive.isNumber() ) {
                return context.deserialize(valueElement, Number.class); // depends on the JSON literal due to the lack of real number type info
            }
            if ( valuePrimitive.isString() ) {
                return context.deserialize(valueElement, String.class);
            }
            throw new AssertionError();
        }
        if ( valueElement instanceof JsonArray ) {
            throw new UnsupportedOperationException("Arrays are unsupported due to lack of type information (a generic list or a concrete type array?)");
        }
        if ( valueElement instanceof JsonNull ) {
            throw new UnsupportedOperationException("Nulls cannot be deserialized");
        }
        throw new AssertionError("Must never happen");
    }

}

final class TypeAwarePropertiesSerializer
        implements JsonSerializer<Properties> {

    private static final JsonSerializer<Properties> typeAwarePropertiesSerializer = new TypeAwarePropertiesSerializer();

    private TypeAwarePropertiesSerializer() {
    }

    static JsonSerializer<Properties> getTypeAwarePropertiesSerializer() {
        return typeAwarePropertiesSerializer;
    }

    @Override
    public JsonElement serialize(final Properties properties, final Type type, final JsonSerializationContext context) {
        final JsonObject propertiesJson = new JsonObject();
        for ( final Entry<Object, Object> entry : properties.entrySet() ) {
            final String property = (String) entry.getKey();
            final Object value = entry.getValue();
            if ( value instanceof Boolean ) {
                propertiesJson.addProperty(property, (Boolean) value);
            } else if ( value instanceof Character ) {
                propertiesJson.addProperty(property, (Character) value);
            } else if ( value instanceof Number ) {
                final JsonObject wrapperJson = newWrapperJson(value);
                wrapperJson.addProperty("_$V", (Number) value);
                propertiesJson.add(property, wrapperJson);
            } else if ( value instanceof String ) {
                propertiesJson.addProperty(property, (String) value);
            } else if ( value instanceof Properties || value instanceof Collection || value instanceof Map ) {
                propertiesJson.add(property, context.serialize(value));
            } else if ( value != null ) {
                final Class<?> aClass = value.getClass();
                if ( aClass.isArray() ) {
                    final JsonObject wrapperJson = newWrapperJson(value);
                    wrapperJson.add("_$V", context.serialize(value));
                    propertiesJson.add(property, wrapperJson);
                } else {
                    throw new UnsupportedOperationException("Cannot process: " + value);
                }
            } else /* now the value is always null, Properties cannot hold nulls */ {
                throw new AssertionError("Must never happen");
            }
        }
        return propertiesJson;
    }

    private static JsonObject newWrapperJson(final Object value) {
        final JsonObject wrapperJson = new JsonObject();
        wrapperJson.addProperty("_$T", value.getClass().getName());
        return wrapperJson;
    }

}

final class TypeAwarePropertiesDeserializer
        implements JsonDeserializer<Properties> {

    private static final JsonDeserializer<Properties> typeAwarePropertiesDeserializer = new TypeAwarePropertiesDeserializer();

    private TypeAwarePropertiesDeserializer() {
    }

    static JsonDeserializer<Properties> getTypeAwarePropertiesDeserializer() {
        return typeAwarePropertiesDeserializer;
    }

    @Override
    public Properties deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
            throws JsonParseException {
        try {
            final Properties properties = new Properties();
            final JsonObject jsonObject = jsonElement.getAsJsonObject();
            for ( final Entry<String, JsonElement> e : jsonObject.entrySet() ) {
                properties.put(e.getKey(), parseValue(context, e.getValue()));
            }
            return properties;
        } catch ( final ClassNotFoundException ex ) {
            throw new JsonParseException(ex);
        }
    }

    private static Object parseValue(final JsonDeserializationContext context, final JsonElement valueElement)
            throws ClassNotFoundException {
        if ( valueElement instanceof JsonObject ) {
            final JsonObject valueObject = valueElement.getAsJsonObject();
            if ( isWrapperJson(valueObject) ) {
                return context.deserialize(getWrapperValueObject(valueObject), getWrapperClass(valueObject));
            }
            return context.deserialize(valueElement, Properties.class);
        }
        if ( valueElement instanceof JsonPrimitive ) {
            final JsonPrimitive valuePrimitive = valueElement.getAsJsonPrimitive();
            if ( valuePrimitive.isBoolean() ) {
                return context.deserialize(valueElement, Boolean.class);
            }
            if ( valuePrimitive.isNumber() ) {
                throw new AssertionError("Must never happen because of 'unboxing' above");
            }
            if ( valuePrimitive.isString() ) {
                return context.deserialize(valueElement, String.class);
            }
            throw new AssertionError("Must never happen");
        }
        if ( valueElement instanceof JsonArray ) {
            return context.deserialize(valueElement, Collection.class);
        }
        if ( valueElement instanceof JsonNull ) {
            throw new UnsupportedOperationException("Nulls cannot be deserialized");
        }
        throw new AssertionError("Must never happen");
    }

    private static boolean isWrapperJson(final JsonObject valueObject) {
        return valueObject.has("_$T") && valueObject.has("_$V");
    }

    private static Class<?> getWrapperClass(final JsonObject valueObject)
            throws ClassNotFoundException {
        return Class.forName(valueObject.get("_$T").getAsJsonPrimitive().getAsString());
    }

    private static JsonElement getWrapperValueObject(final JsonObject valueObject) {
        return valueObject.get("_$V");
    }

}

现在,topLevelProp 可以填充以下内容:
topLevelProp.put("ARRAY", new String[]{ "foo", "bar" });
topLevelProp.put("RAW_LIST", asList("foo", "bar"));

如果您使用了这些特殊的JSON反序列化器:
private static final Gson typeAwareGson = new GsonBuilder()
        .registerTypeAdapter(Properties.class, getTypeAwarePropertiesSerializer())
        .registerTypeAdapter(Properties.class, getTypeAwarePropertiesDeserializer())
        .create();

一个样例输出:

{RAW_LIST=[foo, bar], inner1={inner2={aStringProp=aStringValue, inner3={i2=100, i1=1}}, aBoolProp=true}, size=1, count=1000000, ARRAY=[Ljava.lang.String;@b81eda8}
{"RAW_LIST":["foo","bar"],"inner1":{"inner2":{"aStringProp":"aStringValue","inner3":{"i2":{"_$T":"java.lang.Integer","_$V":100},"i1":{"_$T":"java.lang.Integer","_$V":1}}},"aBoolProp":true},"size":{"_$T":"java.lang.Integer","_$V":1},"count":{"_$T":"java.lang.Integer","_$V":1000000},"ARRAY":{"_$T":"[Ljava.lang.String;","_$V":["foo","bar"]}}
{RAW_LIST=[foo, bar], inner1={inner2={aStringProp=aStringValue, inner3={i2=100, i1=1}}, aBoolProp=true}, size=1, count=1000000, ARRAY=[Ljava.lang.String;@e2144e4}

总结两种方法,您可能希望消除弱类型的需求,并尽可能引入显式的POJO映射。


0

因为我只需要反序列化功能,即为传入的Json(在我的情况下是一个REST端点)生成Java属性,所以我很快就实现了这个解决方案:

public class Configuration extends Properties {
    public void load(JsonElement json) {
        addJson("", json);
        return;
    }

    public void addJson(String root, JsonElement json) {

        // recursion for objects
        if (json instanceof JsonObject) {
            if (!root.equals("")) root += ".";
            final JsonObject jsonObject = json.getAsJsonObject();
            for ( final Entry<String, JsonElement> e : jsonObject.entrySet() ) {
            addJson(root + e.getKey(), e.getValue());
            }           
            return;
        }

        // recursion for arrays
        if (json instanceof JsonArray) {
            final JsonArray jsonArray = json.getAsJsonArray();
            if (!root.equals("")) root += ".";
            int count = 0;
            for(final JsonElement e : jsonArray) {
                addJson(root+count, e);
                count++;
            }
            return;
        }

        // leaves: add property
        this.setProperty(root, json.getAsString());
    }
}

正如您所看到的,这是扩展Properties类。当然,另一种选择是事先初始化一个Properties对象并将其传递到递归中。

希望这对某人有用 :-)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接