【问题标题】:How to pass through properties in JSON messages with Jackson and MongoDB?如何使用 Jackson 和 MongoDB 传递 JSON 消息中的属性?
【发布时间】: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;
}

但是,如果我不映射所有其他属性,它们将会丢失。

如果我确实映射它们,那么每次消息结构发生变化时我都必须更新这个微服务的模型,这需要努力并且容易出错。

【问题讨论】:

  • @Yuva 看到了。 @JsonAnySetter 是最接近的,但不清楚它如何处理复杂的属性,这也使得模型可变。
  • 有一个带有@JsonAnySetter 方法的混合类怎么样?请参阅wiki.fasterxml.com/JacksonMixInAnnotations。能帮上忙吗?使用混合类,可以在不接触目标类的情况下配置反序列化。
  • @Yuva 这看起来很有趣。
  • 是否需要持久化不感兴趣的属性? (你提到了 MongoDb。)

标签: java json spring jackson microservices


【解决方案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 映射到杰克逊?
          • 您不需要做任何事情,只需将字段添加到您的模型中
          猜你喜欢
          • 2018-12-26
          • 2011-12-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-07-01
          • 2016-10-25
          相关资源
          最近更新 更多