- 接口配置如下:
@RequestMapping(value = {"/get"}, produces = {"application/x-protobuf"})
public UidResponse get(UidRequest uidRequest) {
return UidResponse.newBuilder().setUuid(1L).build();
}
- 配置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列表中已经添加上了:

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

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。至此发现了导致问题的根源。
-
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中给予了最小功能的实现。
因此删除proto定义文件中的option optimize_for = LITE_RUNTIME;即可。如果proto定义文件没有发现此选项那么可以查看项目构建文件是否有此选项控制,如build.gradle:
generateProtoTasks {
all().each { task ->
task.builtins {
java {
// 相当于开启 option optimize_for = LITE_RUNTIME;
option 'lite'
}
}
}
}
-
总结:
由于proto编译开启了optimize_for优化,导致即使配置了ProtobufHttpMessageConverter,也无法序列化生成的model。