如何从 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):
- 定义自定义解串器
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;
}
}
- 创建自定义 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));
}
}
- 创建并配置对象映射器,然后实例化自定义 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);
}
}
我们在 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):
- 定义自定义解串器
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;
}
}
- 创建自定义 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));
}
}
- 创建并配置对象映射器,然后实例化自定义 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);
}
}