如何从 Mongo 数据库中读取 com.fasterxml.jackson.databind.node.TextNode 并转换为映射 <String, Object>?

How to read a com.fasterxml.jackson.databind.node.TextNode from a Mongo DB and convert to a Map <String, Object>?

我们在 Spring-boot 应用程序中使用 SpringDataMongoDB 来管理我们的数据。

我们之前的模型是这样的:

public class Response implements Serializable {
    //...
    private JsonNode errorBody; //<-- Dynamic
    //...
}

JsonNode FQDN 是 com.fasterxml.jackson.databind.JsonNode

在数据库中保存了这样的文档:

"response": {
  ...
        "errorBody": {
          "_children": {
            "code": {
              "_value": "Error-code-value",
              "_class": "com.fasterxml.jackson.databind.node.TextNode"
            },
            "message": {
              "_value": "Error message value",
              "_class": "com.fasterxml.jackson.databind.node.TextNode"
            },
            "description": {
              "_value": "Error description value",
              "_class": "com.fasterxml.jackson.databind.node.TextNode"
            }
          },
          "_nodeFactory": {
            "_cfgBigDecimalExact": false
          },
          "_class": "com.fasterxml.jackson.databind.node.ObjectNode"
     },
  ...
 }

我们已经在生产数据库上保存了数百个这样的文档,而无需以编程方式读取它们,因为它们只是一种日志。

由于我们注意到此输出将来可能难以阅读,因此我们决定将模型更改为:

public class Response implements Serializable {
    //...
    private Map<String,Object> errorBody;
    //...
}

数据现在保存如下:

"response": {
  ...
        "errorBody": {
          "code": "Error code value",
          "message": "Error message value",
          "description": "Error description value",
          ...
        },
  ...
 }

您可能已经注意到,这要简单得多。

读取数据时,例如:repository.findAll()

新格式读取没有任何问题。

但我们在使用旧格式时面临这些问题:

org.springframework.data.mapping.MappingException: No property v found on entity class com.fasterxml.jackson.databind.node.TextNode to bind constructor parameter to!

org.springframework.data.mapping.model.MappingInstantiationException: Failed to instantiate com.fasterxml.jackson.databind.node.ObjectNode using constructor NO_CONSTRUCTOR with arguments

当然 TextNode class 有一个以 v 作为参数的构造函数,但是 属性 名称是 _value 并且 ObjectNode 有没有默认构造函数:我们根本无法更改它。

我们创建了已添加到配置中的自定义转换器。

public class ObjectNodeWriteConverter implements Converter<ObjectNode, DBObject> {    
    @Override
    public DBObject convert(ObjectNode source) {
        return BasicDBObject.parse(source.toString());
    }
}
public class ObjectNodeReadConverter implements Converter<DBObject, ObjectNode> {
    @Override
    public ObjectNode convert(DBObject source) {
        try {
            return new ObjectMapper().readValue(source.toString(), ObjectNode.class);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
}

我们为 TextNode

做了同样的事情

但我们仍然遇到错误。

转换器被读取,因为我们有一个 ZonedDateTimeConverter 正在做他的工作。

我们不能只是抹去或忽略旧数据,因为我们也需要阅读它们以研究它们。

我们如何设置一个不会在读取旧格式时失败的自定义 reader?

由于旧格式是预定义的并且您知道它的结构,您可以实现自定义反序列化器以同时处理旧格式和新格式。如果 errorBody JSON Object 包含这些键中的任何一个:_children_nodeFactory_class 你知道它是一种旧格式,你需要迭代 _children JSON Object 并获取 _value 键以找到真实值。您可以忽略其余的键和值。简单的实现如下所示:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;
import lombok.ToString;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class JsonMongo2FormatsApp {
    public static void main(String[] args) throws IOException {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        JsonMapper mapper = JsonMapper.builder().build();
        Response response = mapper.readValue(jsonFile, Response.class);
        System.out.println(response.getErrorBody());
    }
}

@Data
@ToString
class Response {

    @JsonDeserialize(using = ErrorMapJsonDeserializer.class)
    private Map<String, String> errorBody;
}

class ErrorMapJsonDeserializer extends JsonDeserializer<Map<String, String>> {

    @Override
    public Map<String, String> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        TreeNode root = p.readValueAsTree();
        if (!root.isObject()) {
            // ignore everything except JSON Object
            return Collections.emptyMap();
        }
        ObjectNode objectNode = (ObjectNode) root;
        if (isOldFormat(objectNode)) {
            return deserialize(objectNode);
        }

        return toMap(objectNode);
    }

    protected boolean isOldFormat(ObjectNode objectNode) {
        final List<String> oldFormatKeys = Arrays.asList("_children", "_nodeFactory", "_class");
        final Iterator<String> iterator = objectNode.fieldNames();

        while (iterator.hasNext()) {
            String field = iterator.next();
            return oldFormatKeys.contains(field);
        }

        return false;
    }

    protected Map<String, String> deserialize(ObjectNode root) {
        JsonNode children = root.get("_children");
        Map<String, String> result = new LinkedHashMap<>();
        children.fields().forEachRemaining(entry -> {
            result.put(entry.getKey(), entry.getValue().get("_value").toString());
        });

        return result;
    }

    private Map<String, String> toMap(ObjectNode objectNode) {
        Map<String, String> result = new LinkedHashMap<>();
        objectNode.fields().forEachRemaining(entry -> {
            result.put(entry.getKey(), entry.getValue().toString());
        });

        return result;
    }
}

上面的解串器应该处理这两种格式。

据我了解您的问题,对于第一个模型,您在保存或读取数据库时并没有真正的问题,但是,一旦您想要获取这些数据,您会注意到输出难以阅读.所以你的问题是获取一个可读性好的输出,然后你不需要改变第一个模型,而是扩展这些 classes 并覆盖 toString 方法来改变它的行为正在获取。

至少有三个 class要扩展:

  • TextNode:您不能覆盖 toString 方法,因为自定义 class 只打印值

  • ObjectNode:我可以看到在这个 class 中至少有四个字段是您想要获取值的:code, 消息描述。它们是 TextNode 类型,因此您可以用扩展的 classes 替换它们。然后覆盖 toString 方法,以便它为每个字段打印 fieldName: field.toString()

  • JsonNode :然后您可以扩展此 class 并使用上面创建的自定义 classes,覆盖 toString 方法,以便它根据需要打印并使用它而不是常见的 JsonNode

这样工作会让你避免保存或读取数据的方式,而只是为了影响视图。

你可以将其视为SOLID原则的一小部分,尤其是OCP(开闭原则:避免改变class 行为,但扩展它以创建自定义行为)和 LSP(里氏替换原则:子类型必须在行为上可替换其基类型)。

Michal Ziober 的回答并没有完全解决问题,因为我们需要告诉 SpringData MongoDb 我们希望他使用自定义反序列化器 (注释模型不适用于 Spring 数据 mongodb):

  1. 定义自定义解串器
public class ErrorMapJsonDeserializer extends JsonDeserializer<Map<String, Object>> {

    @Override
    public Map<String, Object> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        TreeNode root = p.readValueAsTree();
        if (!root.isObject()) {
            // ignore everything except JSON Object
            return Collections.emptyMap();
        }
        ObjectNode objectNode = (ObjectNode) root;
        if (isOldFormat(objectNode)) {
            return deserialize(objectNode);
        }
        return toMap(objectNode);
    }

    protected boolean isOldFormat(ObjectNode objectNode) {
        final List<String> oldFormatKeys = Arrays.asList("_children", "_nodeFactory", "_class");
        final Iterator<String> iterator = objectNode.fieldNames();
        while (iterator.hasNext()) {
            String field = iterator.next();
            return oldFormatKeys.contains(field);
        }
        return false;
    }

    protected Map<String, Object> deserialize(ObjectNode root) {
        JsonNode children = root.get("_children");
        if (children.isArray()) {
            children = children.get(0);
            children = children.get("_children");
        }
        return extractValues(children);
    }

    private Map<String, Object> extractValues(JsonNode children) {
        Map<String, Object> result = new LinkedHashMap<>();
        children.fields().forEachRemaining(entry -> {
            String key = entry.getKey();
            if (!key.equals("_class"))
                result.put(key, entry.getValue().get("_value").toString());
        });
        return result;
    }


    private Map<String, Object> toMap(ObjectNode objectNode) {
        Map<String, Object> result = new LinkedHashMap<>();
        objectNode.fields().forEachRemaining(entry -> {
            result.put(entry.getKey(), entry.getValue().toString());
        });
        return result;
    }
}
  1. 创建自定义 mongo 转换器并为其提供自定义反序列化器。

实际上我们并没有直接向他提供序列化器,而是通过配置了自定义反序列化器的 ObjectMapper

public class CustomMappingMongoConverter extends MappingMongoConverter {

    //The configured objectMapper that will be passed during instatiation
    private ObjectMapper objectMapper; 

    public CustomMappingMongoConverter(DbRefResolver dbRefResolver, MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext, ObjectMapper objectMapper) {
        super(dbRefResolver, mappingContext);
        this.objectMapper = objectMapper;
    }

    @Override
    public <S> S read(Class<S> clazz, Bson dbObject) {
        try {
            return objectMapper.readValue(dbObject.toString(), clazz);
        } catch (IOException e) {
            throw new RuntimeException(dbObject.toString(), e);
        }
    }


    //in case you want to serialize with your custom objectMapper as well
    @Override
    public void write(Object obj, Bson dbo) {
        String string = null;
        try {
            string = objectMapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(string, e);
        }
        ((DBObject) dbo).putAll((DBObject) BasicDBObject.parse(string));
    }

}
  1. 创建并配置对象映射器,然后实例化自定义 MongoMappingConverter 并将其添加到 Mongo 配置
public class MongoConfiguration extends AbstractMongoClientConfiguration {

   
    //... other configuration method beans
   
    @Bean
    @Override
    public MappingMongoConverter mappingMongoConverter() throws Exception {
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory());
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.registerModule(new SimpleModule() {
            {
                addDeserializer(Map.class, new ErrorMapJsonDeserializer());
            }
        });
        return new CustomMappingMongoConverter(dbRefResolver, mongoMappingContext(), objectMapper);
    }
}