如何使用 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 类中声明的 getdelete 方法抛出 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&lt;String, Object&gt; 并继承 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","re​​sult":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&lt;String, Object&gt;,这将允许您添加键值对,但同样,您会失去类型安全性:

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

来自MongoDB BSON的Jackson ObjectMapper

如何在节点js中将变量名作为mongodb的集合名称传递

Jackson 使用简介