在spring-boot中将protobuf作为JSON发送

8

我正在使用具体的定义与protobufs。

message Hash {
    string category = 1;
    repeated KVPair content = 2;
}

message KVPair {
    string key = 1;
    string value = 2;
}

我希望将这个Spring Boot应用程序发送为JSON格式。我已经将以下包添加到我的Gradle依赖项中:
compile group: 'com.google.protobuf', name: 'protobuf-java', version: '3.6.1'

当我尝试使用以下代码输出生成的哈希对象时:
@RestController
@RequestMapping("/api/crm/")
public class KVController {

    private final KVService kvService;

    public KVController(KVService kvService) {
        this.kvService = kvService;
    }

    @GetMapping("kv/{category}")
    public Hash getHash(@PathVariable String category) {
        Hash hash = kvService.retrieve(category);
        return hash;
    }
}

它抛出了一个致命错误异常:
``` Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle (through reference chain: com.blaazha.crm.proto.Hash["unknownFields"]->com.google.protobuf.UnknownFieldSet["defaultInstanceForType"]) at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77) ~[jackson-databind-2.9.6.jar:2.9.6] at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1191) ~[jackson-databind-2.9.6.jar:2.9.6] at com.fasterxml.jackson.databind.ser.BeanPropertyWriter._handleSelfReference(BeanPropertyWriter.java:944) ~[jackson-databind-2.9.6.jar:2.9.6] at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:721) ~[jackson-databind-2.9.6.jar:2.9.6] at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719) ~[jackson-databind-2.9.6.jar:2.9.6] at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155) ~[jackson-databind-2.9.6.jar:2.9.6] at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727) ~[jackson-databind-2.9.6.jar:2.9.6] at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719) ~[jackson-databind-2.9.6.jar:2.9.6] at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155) ~[jackson-databind-2.9.6.jar:2.9.6] at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480) ~[jackson-databind-2.9.6.jar:2.9.6] at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:319) ~[jackson-databind-2.9.6.jar:2.9.6] at com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1396) ~[jackson-databind-2.9.6.jar:2.9.6] at com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:913) ~[jackson-databind-2.9.6.jar:2.9.6] at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:286) ~[spring-web-4.3.18.RELEASE.jar:4.3.18.RELEASE] ... 58 common frames omitted ```

kvService仅从redis返回数据。它解析哈希数据类型(https://redis.io/topics/data-types)到proto中定义的哈希对象。其中,哈希->类别是哈希的主键,哈希redis数据类型中的值转换为proto中定义的KVPair。我无法展示所有源代码,因为它调用其他系统且源代码非常长。

kvService返回有效的哈希对象,但当我返回此哈希对象并且Spring尝试将其转换为JSON时,会发生异常。

我的build.gradle中的重要依赖项:

def versions = [
        logback: '1.2.3',
        owner: '1.0.10',
        jackson: '2.9.6',

        guava: '25.1-jre',
        guice: '4.2.0',
        grpc: '1.9.1',
        protoc: '3.5.1',

        redis: '2.9.0',
]

依赖关系 {

compile group: 'ch.qos.logback', name: 'logback-classic', version: versions.logback
compile group: 'org.aeonbits.owner', name: 'owner', version: versions.owner

compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: versions.jackson
compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: versions.jackson
compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: versions.jackson

compile group: 'com.google.guava', name: 'guava', version: versions.guava
compile group: 'com.google.inject', name: 'guice', version: versions.guice
compile group: 'io.grpc', name: 'grpc-netty', version: versions.grpc
compile group: 'io.grpc', name: 'grpc-protobuf', version: versions.grpc
compile group: 'io.grpc', name: 'grpc-stub', version: versions.grpc
compile 'org.glassfish:javax.annotation:10.0-b28'


compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.2.1'
compile group: 'javax.activation', name: 'activation', version: '1.1.1'

compile group: 'redis.clients', name: 'jedis', version: versions.redis

正如您在我的protobuf定义中所看到的,没有任何自我引用。

是否有可能解决这个问题?


代码的哪一行发生了异常?你能分享一下你的kvservice代码吗?我认为有了这些信息,其他人回答你的问题会更容易。 - f-CJ
6个回答

8
如果您正在使用 Spring WebFlux 并尝试通过 produces application/json 来处理所有返回 protobuf Message 类型的映射,请按照以下步骤进行设置:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
    configurer.defaultCodecs().jackson2JsonEncoder(
        new Jackson2JsonEncoder(Jackson2ObjectMapperBuilder.json().serializerByType(
                Message.class, new JsonSerializer<Message>() {
                    @Override
                    public void serialize(Message value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
                        String str = JsonFormat.printer().omittingInsignificantWhitespace().print(value);
                        gen.writeRawValue(str);
                    }
                }
        ).build())
    );
}

它在序列化方面运行得很完美,有没有什么想法来适应反序列化。 - Karthik Prasad

4

UnknownFieldSet(通过生成的方法Hash.getUnknownFields()访问)包含getter getDefaultInstanceForType(),该方法返回UnknownFieldSet的单例实例。 这个单例实例在getDefaultInstanceForType()中引用自身,而Jackson-databind无法自动处理这种情况(请参见下面的编辑2)。

您可能想要使用来自com.google.protobuf:protobuf-java-utilJsonFormat,它使用规范编码而不是Jackson。

祝你好运!

编辑> 对于Spring,有ProtobufJsonFormatHttpMessageConverter

编辑2> 当然,您可以使用Mix-in Annotations来处理此情况,但在我看来,JsonFormat绝对是正确的选择...


这个有用吗?我应该在哪里更新httpmessageconverter? - Prasath
1
@Prasath 看一下这里,例如 这里 或者 这里。祝你好运! - vlp

2

我通过在Spring主应用程序中添加以下bean来解决了我的问题。

尝试了许多StackOverflow上的答案都没有成功。您可以按照此处定义的方式将protobuf类型添加到注册表中,这可能会帮助您获得一些成功,因为其他常见答案对我无效。

@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
    JsonFormat.TypeRegistry typeRegistry = JsonFormat.TypeRegistry.newBuilder()
            .add(MyType.getDescriptor())
            .add(MyOtherType.getDescriptor())
            .build();

    JsonFormat.Printer printer = JsonFormat.printer().usingTypeRegistry(typeRegistry)
            .includingDefaultValueFields();
    return o -> o.serializerByType(Message.class, new JsonSerializer<Message>() {
        @Override
        public void serialize(Message message, JsonGenerator gen, SerializerProvider serializers) throws IOException {
            gen.writeRawValue(printer.print(message));
        }
    });
}

2

要将 protobuf 对象转换为 JSON,您应该使用来自包 com.google.protobuf.util.JsonFormat 的以下类:

JsonFormat.printer().print()

1

以下是基于vlp's答案的示例。请按照以下步骤操作:

  1. 声明一个类型为ProtobufJsonFormatHttpMessageConverter的Bean,例如:
@Configuration
public class AppConfig {
    @Bean
    public ProtobufJsonFormatHttpMessageConverter protobufHttpMessageConverter() {
        return new ProtobufJsonFormatHttpMessageConverter(
                JsonFormat.parser().ignoringUnknownFields(),
                JsonFormat.printer().omittingInsignificantWhitespace()
        );
    }
}

2. 在您的控制器方法中添加 produces = MediaType.APPLICATION_JSON_VALUE。例如:
@GetMapping(value = "/get-one-item", produces = MediaType.APPLICATION_JSON_VALUE)
public Item getItems() {
    //Item is a Protobuf message
    return itemService.getOneItem();
}

0
使用Spring Boot Web MVC时,您需要创建一个转换器。
@Component
public class ProtobufToJsonConverter extends AbstractHttpMessageConverter<Message> {

  private final JsonFormat.Printer printer;

  private final JsonFormat.Parser parser;

  public ProtobufToJsonConverter() {
    parser = JsonFormat.parser()
      .ignoringUnknownFields();
    printer = JsonFormat.printer()
      .includingDefaultValueFields()
      .omittingInsignificantWhitespace();
  }

  @Override
  protected boolean canRead(MediaType mediaType) {

    return MediaType.APPLICATION_JSON.equals(mediaType);
  }

  @Override
  protected boolean canWrite(MediaType mediaType) {

    return MediaType.APPLICATION_JSON.equals(mediaType);
  }

  @Override
  protected @NotNull Message readInternal(@NotNull Class<? extends Message> clazz, @NotNull HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {

    try {
      final Message.Builder builder = (Message.Builder)clazz.getDeclaredMethod("newBuilder").invoke(null);
      parser
        .merge(new InputStreamReader(inputMessage.getBody()), builder);
      return builder.build();
    } catch (ReflectiveOperationException e) {
      throw new IOException(e);
    }
  }

  @Override
  protected boolean supports(@NotNull Class<?> clazz) {

    return Message.class.isAssignableFrom(clazz);
  }

  @Override
  protected void writeInternal(@NotNull Message message, @NotNull HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {

    outputMessage.getBody().write(printer.print(message).getBytes(StandardCharsets.UTF_8));

  }


}

不需要注册它,但组件必须通过将其放在与您的@SpringBootApplication@ComponentScan相同或子包中进行扫描。一旦扫描完成,它就可以用于任何HTTP转换。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接