SpringBoot使用protobuf生成的model进行传参无法序列化

  1. 接口配置如下:
    @RequestMapping(value = {"/get"}, produces = {"application/x-protobuf"})
    public UidResponse get(UidRequest uidRequest) {
     return UidResponse.newBuilder().setUuid(1L).build();
    }
  1. 配置HttpMessageConverter
    @Bean
    ProtobufHttpMessageConverter protobufHttpMessageConverter() {
     return new ProtobufJsonFormatHttpMessageConverter();
    }

配置后不生效,报错信息如下:

2021-09-02 11:54:09.603  WARN 54285 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class com.x.x.x.x.proto.UidResponse] with preset Content-Type 'null']

最开始怀疑是ProtobufHttpMessageConverter没有注册到HttpMessageConverter的列表中,于是开始跟踪源码发现HttpMessageConverter列表中已经添加上了:


org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse)

既然已经添加进去了,那为什么不走转换器的逻辑,继续往下走发现:


converter.canWrite(valueType, selectedMediaType)

converter.canWrite(valueType, selectedMediaType)这个方法返回false导致body没有进行适配,下面进行判空逻辑后就会抛出异常:

    throw new HttpMessageNotWritableException(
     "No converter for [" + valueType + "] with preset Content-Type '" +           contentType + "'");

继续跟下去,canWrite是调用了父类org.springframework.http.converter.AbstractHttpMessageConverter#canWrite:

    @Override
    public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
     return supports(clazz) && canWrite(mediaType);
    }

而ProtobufHttpMessageConverter其实已经重写了supports与canWrite方法:

    @Override
    protected boolean supports(Class<?> clazz) {
     return Message.class.isAssignableFrom(clazz);
    }
    @Override
    protected boolean canWrite(@Nullable MediaType mediaType) {
     return (super.canWrite(mediaType) ||
     (this.protobufFormatSupport != null && this.protobufFormatSupport.supportsWriteOnly(mediaType)));
    }

这里发现根源其实是Message.class.isAssignableFrom(clazz)返回的false。那么难道protobuf生成的model不是com.google.protobuf.Message的子类吗?

    public  final class xxx extends
     com.google.protobuf.GeneratedMessageLite<
     xxx, xxx.Builder> implements
     xxxOrBuilder {
     private xxx() {
     }
    }

查看生成的java文件发现model确实不是继承的GeneratedMessageV3,而是继承了GeneratedMessageLite。至此发现了导致问题的根源。

  1. GeneratedMessageV3与GeneratedMessageLite的区别

    经过一番搜索后发现是由于optimize_for选项导致的,optimize_for选项的功能如下:

    optimize_for是文件级别的选项,Protocol Buffer定义三种优化级别SPEED/CODE_SIZE/LITE_RUNTIME。缺省情况下是SPEED。 SPEED: 表示生成的代码运行效率高,但是由此生成的代码编译后会占用更多的空间。 CODE_SIZE: 和SPEED恰恰相反,代码运行效率较低,但是由此生成的代码编译后会占用更少的空间,通常用于资源有限的平台,如Mobile。 LITE_RUNTIME: 生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲Protocol Buffer提供的反射功能为代价的。该.proto文件生成的所有Java类的父类均为com.google.protobuf.GeneratedMessageLite,而非com.google.protobuf.GeneratedMessage,同时与之对应的Builder类则均继承自com.google.protobuf.MessageLiteOrBuilder,而非com.google.protobuf.MessageOrBuilder。

    MessageLite接口是Message的父接口,在MessageLite中将缺少Protocol Buffer对反射的支持,而此功能均在Message接口中提供了接口规范,同时又在其实现类GeneratedMessage中给予了最小功能的实现。

  2. 因此删除proto定义文件中的option optimize_for = LITE_RUNTIME;即可。如果proto定义文件没有发现此选项那么可以查看项目构建文件是否有此选项控制,如build.gradle:

generateProtoTasks {
     all().each { task ->
       task.builtins {
         java {
     // 相当于开启 option optimize_for = LITE_RUNTIME;
           option 'lite'
         }
       }
     }
}
  1. 总结:

    由于proto编译开启了optimize_for优化,导致即使配置了ProtobufHttpMessageConverter,也无法序列化生成的model。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容