如何使用 Jackson 和 MongoDB 传递 JSON 消息中的属性?
Posted
技术标签:
【中文标题】如何使用 Jackson 和 MongoDB 传递 JSON 消息中的属性?【英文标题】:How to pass through properties in JSON messages with Jackson and MongoDB? 【发布时间】:2017-04-23 12:33:07 【问题描述】:我们有一个微服务,它从队列中获取一些 JSON 数据,对其进行一点处理,然后将处理结果进一步发送 - 再次通过队列。在我们不直接使用 JSONObject
的微服务中,我们使用 Jackson 将 JSON 映射到 Java 类。
处理时,微服务只对传入消息的一些属性感兴趣,而不是全部。想象一下它只是接收
"operand1": 3,
"operand2": 5,
/* other properties may come here */
并发送:
"operand1": 3,
"operand2": 5,
"multiplicationResult": 15,
/* other properties may come here */
如果不将它们显式映射到我的类中,我如何才能隧道或传递我对此服务不感兴趣的消息的其他属性?
对于这个微服务来说,有这样的结构就足够了:
public class Task
public double operand1;
public double operand2;
public double multiplicationResult;
但是,如果我不映射所有其他属性,它们将会丢失。
如果我确实映射它们,那么每次消息结构发生变化时我都必须更新这个微服务的模型,这需要努力并且容易出错。
【问题讨论】:
检查wiki.fasterxml.com/JacksonHowToIgnoreUnknown是否有帮助? @Yuva 看到了。@JsonAnySetter
是最接近的,但不清楚它如何处理复杂的属性,这也使得模型可变。
有一个带有@JsonAnySetter 方法的混合类怎么样?请参阅wiki.fasterxml.com/JacksonMixInAnnotations。能帮上忙吗?使用混合类,可以在不接触目标类的情况下配置反序列化。
@Yuva 这看起来很有趣。
是否需要持久化不感兴趣的属性? (你提到了 MongoDb。)
【参考方案1】:
在敏捷结构的情况下,最简单的方法是使用 Map 而不是自定义 POJO:
很容易从 JSON 中读取,例如使用JsonParser parser
(java 文档here):
Map<String, Object> fields =
parser.readValueAs(new TypeReference<Map<String, Object>>() );
使用BasicDBObject
(java 文档here)很容易写入MongoDB:
DBCollection collection = db.getCollection("tasks");
collection.insert(new BasicDBObject(fields));
你甚至可以像这样用Task
包裹Map
来实现它:
public class Task
private final Map<String, Object> fields;
public final double operand1;
// And so on...
@JsonCreator
public Task(Map<String, Object> fields)
this.fields = fields;
this.operand1 = 0; /* Extract desired values from the Map */
@JsonValue
public Map<String, Object> toMap()
return this.fields;
如果需要,也可以使用自定义JsonDeserializer
(在这种情况下,Task
必须用@JsonDeserialize(using = TaskDeserializer.class)
注释):
public class TaskDeserializer extends JsonDeserializer<Task>
@Override
public Task deserialize(JsonParser parser, DeserializationContext context)
throws IOException, JsonProcessingException
Map<String, Object> fields = parser.readValueAs(new TypeReference<Map<String, Object>>() );
return new Task(fields);
【讨论】:
请注意,这里不需要自定义反序列化器:只需将@JsonCreator
用于采用Map
的构造函数;和@JsonValue
,如果您还想从内部Map
序列化。除非您完全控制所有处理,否则通常最好避免使用自定义(反)序列化程序。
@StaxMan,感谢您的评论!我非常同意你的观点:在这种情况下,自定义反序列化器是多余的【参考方案2】:
所以,如果我正确理解你想要什么:
服务 A -> 发送 json 字符串 -> 服务 B -> 发送相同的 json 字符串 -> 服务 C
服务 A 创建 JSON 字符串 服务 B 只关心 JSON 字符串的几个参数 服务 C 只关心 JSON 字符串的几个参数如果这是您想要的,我建议您存储整个 json 字符串,然后将其传递,并通过在服务 B 和 C 中使用 IgnoreUnknownProperties,您可以根据需要解析您的对象。然而,这需要您自己解析它,但这很容易使用 ObjectMapper 完成,您可以创建它并将其放入您的 spring 上下文中并在任何地方使用它。
例子:
// Reading data
String jsonString restTemplate.getForObject(request, String.class);
MyClass actualObj = new ObjectMapper().readValue(jsonString, MyClass.class);
actualObj.setOriginalJson(jsonString);
// Writing data
String jsonString = actualObj.getOriginalJson();
restTemplate.postForLocation(URI.create(url), jsonString);
@JsonIgnoreProperties(ignoreUnknown = true)
public class MyClass
private String property1;
private String property2;
@JsonIgnore
private String originalJson;
public String getOriginalJson()
return originalJson;
public void setOriginalJson(String originalJson)
this.originalJson = originalJson;
您可以轻松地将字符串 + MyClass 包装在一个对象中,或者在 MyClass 中创建一个变量来保存字符串以供将来使用(如我的示例所示)。
这是我看到的最简单的方法,您可以保留数据而不必修改对象来包含它。
另外注意,不要每次都创建new ObjectMapper(),创建一次,放到spring上下文中,然后通过Autowired获取到你要使用的地方。
【讨论】:
【参考方案3】:正如前面所说,@JsonAnyGetter
和@JsonAnySetter
可能是您的最佳选择。
我认为你可以尽可能灵活地做到类型安全。
我想到的第一件事就是准确区分您需要的属性和其余的属性。
核心
Calculation.java
一个简单的不可变“计算”对象。 当然,它可以以任何其他方式设计,但我相信不变性使它更简单、更可靠。
final class Calculation
private final double a;
private final double b;
private final Operation operation;
private final Double result;
private Calculation(final double a, final double b, final Operation operation, final Double result)
this.a = a;
this.b = b;
this.operation = operation;
this.result = result;
static Calculation calculation(final double a, final double b, final Operation operation, final Double result)
return new Calculation(a, b, operation, result);
Calculation calculate()
return new Calculation(a, b, operation, operation.applyAsDouble(a, b));
double getA()
return a;
double getB()
return b;
Operation getOperation()
return operation;
Double getResult()
return result;
Operation.java
一个简单的在枚举中定义的计算策略,因为 Jackson 使用枚举非常好。
enum Operation
implements DoubleBinaryOperator
ADD
@Override
public double applyAsDouble(final double a, final double b)
return a + b;
,
SUBTRACT
@Override
public double applyAsDouble(final double a, final double b)
return a - b;
,
MULTIPLY
@Override
public double applyAsDouble(final double a, final double b)
return a * b;
,
DIVIDE
@Override
public double applyAsDouble(final double a, final double b)
return a / b;
杰克逊映射
AbstractTask.java
请注意,该类旨在提供一个值,但将其余部分收集到卫星地图中,该卫星地图由使用@JsonAnySetter
和@JsonAnyGetter
注释的方法管理。
地图和方法可以安全地声明为private
,因为杰克逊并不真正关心保护级别(这很棒)。
此外,它以不可变的方式设计,除了可以浅拷贝到新值的底层映射。
abstract class AbstractTask<V>
implements Supplier<V>
@JsonIgnore
private final Map<String, Object> rest = new LinkedHashMap<>();
protected abstract AbstractTask<V> toTask(V value);
final <T extends AbstractTask<V>> T with(final V value)
final AbstractTask<V> dto = toTask(value);
dto.rest.putAll(rest);
@SuppressWarnings("unchecked")
final T castDto = (T) dto;
return castDto;
@JsonAnySetter
@SuppressWarnings("unused")
private void set(final String name, final Object value)
rest.put(name, value);
@JsonAnyGetter
@SuppressWarnings("unused")
private Map<String, Object> getRest()
return rest;
CalculationTask.java
这是一个定义具体计算任务的类。
同样,Jackson 与私有字段和方法完美配合,因此可以封装整个复杂性。
我可以看到的一个缺点是 JSON 属性被声明为用于序列化和反序列化,但它也可以被认为是一个优点。
请注意,@JsonGetter
参数在这里不是必需的,但我只是将属性名称加倍以用于输入和输出操作。
没有任何任务打算手动实例化 - 让 Jackson 来做吧。
final class CalculationTask
extends AbstractTask<Calculation>
private final Calculation calculation;
private CalculationTask(final Calculation calculation)
this.calculation = calculation;
@JsonCreator
@SuppressWarnings("unused")
private static CalculationTask calculationTask(
@JsonProperty("a") final double a,
@JsonProperty("b") final double b,
@JsonProperty("operation") final Operation operation,
@JsonProperty("result") final Double result
)
return new CalculationTask(calculation(a, b, operation, result));
@Override
public Calculation get()
return calculation;
@Override
protected AbstractTask<Calculation> toTask(final Calculation calculation)
return new CalculationTask(calculation);
@JsonGetter("a")
@SuppressWarnings("unused")
private double getA()
return calculation.getA();
@JsonGetter("b")
@SuppressWarnings("unused")
private double getB()
return calculation.getB();
@JsonGetter("operation")
@SuppressWarnings("unused")
private Operation getOperation()
return calculation.getOperation();
@JsonGetter("result")
@SuppressWarnings("unused")
private Double getResult()
return calculation.getResult();
客户端-服务器交互
CalculationController.java
这是一个简单的 GET/PUT/DELETE 控制器,用于集成测试,或者只是用 curl
手动测试。
@RestController
@RequestMapping("/")
final class CalculationController
private final CalculationService processService;
@Autowired
@SuppressWarnings("unused")
CalculationController(final CalculationService processService)
this.processService = processService;
@RequestMapping(method = GET, value = "id")
@ResponseStatus(OK)
@SuppressWarnings("unused")
CalculationTask get(@PathVariable("id") final String id)
return processService.get(id);
@RequestMapping(method = PUT, value = "id")
@ResponseStatus(NO_CONTENT)
@SuppressWarnings("unused")
void put(@PathVariable("id") final String id, @RequestBody final CalculationTask task)
processService.put(id, task);
@RequestMapping(method = DELETE, value = "id")
@ResponseStatus(NO_CONTENT)
@SuppressWarnings("unused")
void delete(@PathVariable("id") final String id)
processService.delete(id);
ControllerExceptionHandler.java
由于下面在 DAO 类中声明的 get
和 delete
方法抛出 NoSuchElementException
,因此可以轻松地将异常映射到 HTTP 404。
@ControllerAdvice
final class ControllerExceptionHandler
@ResponseStatus(NOT_FOUND)
@ExceptionHandler(NoSuchElementException.class)
@SuppressWarnings("unused")
void handleNotFound()
应用程序本身
CalculationService.java
只是一个包含一些“业务”逻辑的简单服务。
@Service
final class CalculationService
private final CalculationDao processDao;
@Autowired
CalculationService(final CalculationDao processDao)
this.processDao = processDao;
CalculationTask get(final String id)
return processDao.get(id);
void put(final String id, final CalculationTask task)
processDao.put(id, task.with(task.get().calculate()));
void delete(final String id)
processDao.delete(id);
数据层
CalculationMapping.java
只是一个持有者类,以便在 Spring Data 中使用 MongoDB 存储库,指定目标 MongoDB 文档集合名称。
@Document(collection = "calculations")
public final class CalculationTaskMapping
extends org.bson.Document
@Id
@SuppressWarnings("unused")
private String id;
ICalculationRepository.java
CalculationMapping
类的 Spring Data MongoDB CRUD 存储库。
下面使用这个存储库。
@Repository
interface ICalculationRepository
extends MongoRepository<CalculationTaskMapping, String>
CalculationDao.java
DAO 组件在演示本身并没有做太多的工作,更多的是把持久化工作委托给它的超类,并且很容易被 Spring Framework 找到。
@Component
final class CalculationDao
extends AbstractDao<CalculationTask, CalculationTaskMapping, String>
@Autowired
CalculationDao(@SuppressWarnings("TypeMayBeWeakened") final ICalculationRepository calculationRepository, final ObjectMapper objectMapper)
super(CalculationTaskMapping.class, calculationRepository, objectMapper);
AbstractDao.java
这是持久化整个原始对象的核心。
ObjectMapper
实例用于根据 Jackson 注释指定的序列化规则将任务转换为它们各自的任务映射(参见 convertValue
方法)。
由于演示使用 Spring Data MongoDB,映射类实际上是 Map<String, Object>
并继承 Document
类。
不幸的是,面向 Map
的映射似乎不适用于 Spring Data MongoDB 注释,如 @Id
、@Field
等(更多信息请参阅 How do I combine java.util.Map-based mappings with the Spring Data MongoDB annotations (@Id, @Field, ...)?)。
但是,只要您不想映射任意文档,它就可以证明是合理的。
abstract class AbstractDao<T, M extends Document, ID extends Serializable>
private final Class<M> mappingClass;
private final CrudRepository<M, ID> crudRepository;
private final ObjectMapper objectMapper;
protected AbstractDao(final Class<M> mappingClass, final CrudRepository<M, ID> crudRepository, final ObjectMapper objectMapper)
this.mappingClass = mappingClass;
this.crudRepository = crudRepository;
this.objectMapper = objectMapper;
final void put(final ID id, final T task)
final M taskMapping = objectMapper.convertValue(task, mappingClass);
taskMapping.put(ID_FIELD_NAME, id);
if ( crudRepository.exists(id) )
crudRepository.delete(id);
crudRepository.save(taskMapping);
final CalculationTask get(final ID id)
final Map<String, Object> rawTask = crudRepository.findOne(id);
if ( rawTask == null )
throw new NoSuchElementException();
rawTask.remove(ID_FIELD_NAME);
return objectMapper.convertValue(rawTask, CalculationTask.class);
final void delete(final ID id)
final M taskMapping = crudRepository.findOne(id);
if ( taskMapping == null )
throw new NoSuchElementException();
crudRepository.delete(id);
Spring Boot 应用程序
EntryPoint.class
还有一个 Spring Boot 演示,它将所有这些作为一个监听端口 9000 的单个 HTTP 应用程序运行。
@SpringBootApplication
@Configuration
@EnableWebMvc
public class EntryPoint
extends SpringBootServletInitializer
@Override
protected SpringApplicationBuilder configure(final SpringApplicationBuilder builder)
return builder.sources(EntryPoint.class);
@Bean
EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer()
return c -> c.setPort(9000);
@Bean
ObjectMapper objectMapper()
return new ObjectMapper()
.setSerializationInclusion(NON_NULL)
.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
@SuppressWarnings("resource")
public static void main(final String... args)
SpringApplication.run(EntryPoint.class, args);
使用curl
测试应用程序
(mongodb-shell)
> use test
切换到本地数据库
(bash)
$ curl -v -X GET http://localhost:9000/foo
> GET /foo HTTP/1.1 > 用户代理:curl/7.35.0 > 主机:本地主机:9000 > 接受:/ >
(mongodb-shell)
> db.calculations.find()
(空)
(bash)
$ curl -v -X PUT -H 'Content-Type: application/json' \
--data '"a":3,"b":4,"operation":"MULTIPLY","result":12,"foo":"FOO","bar":"BAR"' \
http://localhost:9000/foo
> PUT /foo HTTP/1.1 > 用户代理:curl/7.35.0 > 主机:本地主机:9000 > 接受:/ > 内容类型:application/json > 内容长度:72 >
(mongodb-shell)
> db.calculations.find()
“_id”:“foo”,“_class”:“q41036545.CalculationTaskMapping”,“a”:3,“b”:4,“操作”:“MULTIPLY”,“结果”:12,“foo " : "FOO", "bar" : "BAR"
(bash)
$ curl -v -X GET http://localhost:9000/foo
> GET /foo HTTP/1.1 > 用户代理:curl/7.35.0 > 主机:本地主机:9000 > 接受:/ > "a":3.0,"b":4.0,"operation":"MULTIPLY","result":12.0,"foo":"FOO","bar":"BAR"
(bash)
curl -v -X DELETE http://localhost:9000/foo
> 删除 /foo HTTP/1.1 > 用户代理:curl/7.35.0 > 主机:本地主机:9000 > 接受:/ >
(mongodb-shell)
> db.calculations.find()
(空)
源代码可以在https://github.com/lyubomyr-shaydariv/q41036545找到
【讨论】:
【参考方案4】:另一种可能性:使用@JsonAnySetter
和/或@JsonAnyGetter
创建嵌入的“动态”属性。这里有一个例子:
http://www.cowtowncoder.com/blog/archives/2011/07/entry_458.html
但基本思想是可以通过 @JsonAnySetter
带注释的方法(或较新版本中的字段)从 JSON 绑定“其他任何内容”;以及通过具有Map
值的类似@JsonAnyGetter
方法/字段来获得序列化的“额外属性”。
【讨论】:
【参考方案5】:在 Spring/Javaconfig 中配置一个 ObjectMapper bean 并设置属性以忽略缺失的属性。在需要的地方注入相同的 bean。
@Bean
public ObjectMapper objectMapper()
return new ObjectMapper()
.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
【讨论】:
如果我忽略未知属性,我将失去它们。 糟糕,我以为您将 JSON 有效负载保留在队列中 不,我必须扩展/更改它(有点)。【参考方案6】:将@JsonView 添加到您的 POJO,如下所示:
public class Task
@JsonView(ServiceView.class)
public double operand1;
@JsonView(ServiceView.class)
public double operand2;
@JsonView(ServiceView.class)
public double multiplicationResult;
public Object otherField;
public interface ServiceView
然后使用 com.fasterxml.jackson.databind.ObjectMapper
将其发送到您的服务,如下所示:
myService.send(new ObjectMapper().writerWithView(ServiceView.class)
.writeValueAsString(task));
【讨论】:
【参考方案7】:这些都不是很好,但您可以做的是将结构拆分为两部分 - 由您的微服务反序列化的结构化部分,以及包含其他 JSON 的单独“附加字段”字段,然后您可以修改此字段内的 JSON 而不更改 Task
。您可以将嵌套 JSON 添加为 String
。
public class Task
public double operand1;
public double operand2;
public double multiplicationResult;
public String additionalFields
或者,您可以添加一个Map<String, Object>
,这将允许您添加键值对,但同样,您会失去类型安全性:
public class Task
public double operand1;
public double operand2;
public double multiplicationResult;
public Map<String, Object> additionalFields
【讨论】:
我如何将additionalFields
映射到杰克逊?
您不需要做任何事情,只需将字段添加到您的模型中以上是关于如何使用 Jackson 和 MongoDB 传递 JSON 消息中的属性?的主要内容,如果未能解决你的问题,请参考以下文章
使用 Jackson PTH 和 Spring Data MongoDB DBRef 生成额外目标属性的 Java 到 JSON 序列化
java 将Jackson与Spring Data MongoDB一起使用
无法在 MongoDB 中使用带有参数的构造函数 NO_CONSTRUCTOR 实例化 com.fasterxml.jackson.databind.node.ObjectNode