問題背景
REST 項目使用protobuf 來加速項目開發,定義了很多model,vo,最終返回的仍然是JSON.
項目中一般使用 一個Response類,
public class Response<T> {
int code;
String message;
T data;
}
如果需要分頁,則還需要如下的類
public class Pagedata<T> {
long totalcount;
List<T> datas;
}
那么在Controller中,直接返回
Response
.set( Pagedata. set ( Protobuf類 ) )
這種形式,會被Spring的HttpMessageConverter 識別為 Response類,而不是protobuf類,因此選擇了正常的 jackson MessageConverter。
這個時候,會報錯:
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle (through reference chain: com.xxx.crm.proto.xxxx["unknownFields"]-
由此可見 jackson 不支持Protobuf類的JSON序列化。
解決方案
思路一
如果希望被HttpMessageConverter 正確選擇 ProtobufJsonFormatHttpMessageConverter,那么整個類都應該是Protobuf的類。那么要使用
如下的寫法:
import "google/protobuf/any.proto";
option java_outer_classname = "ResponseProto";
message ProtoResponse {
int32 code = 1;
string message = 2;
ProtoPagedData data = 3;
}
message ProtoPagedData {
repeated google.protobuf.Any datas = 1;
int64 totalcount = 2;
}
不管什么類都需要用此Protobuf類來 pack。
@GetMapping(value = "/someUrl")
public Object handler() {
List<FooBarProtobufVO> data = //
ResponseProto.ProtoResponse ok = ResponseProto.ProtoResponse.newBuilder()
.setCode(0)
.setMsg("ok")
.setData(ResponseProto.ProtoPagedData.newBuilder()
.addAllDatas(data.stream().map(Any::pack).collect(Collectors.toList()))
.setTotalcount(all.getTotalElements())
.build())
.build();
return ok;
}
注意:如果使用Any需要使用TypeRegistry顯式注冊你的實際類型,否則使用JsonFormat.printer().print打印的時候,會報錯:Cannot find type for url: type.googleapis.com
這個方式最終是通過ProtobufJsonFormatHttpMessageConverter序列化的。
(我的另一篇文章也指出了,HttpMessageConverter的順序十分重要,這里需要讓ProtobufJsonFormatHttpMessageConverter 在系統的靠前的位置)
思路二
既然protobuf的類不能被jackson正確序列化,那么直接返回一個String,或許使用 JsonFormat也是一個不錯的選擇。
JsonFormat.printer()
.omittingInsignificantWhitespace()
.preservingProtoFieldNames()
.includingDefaultValueFields()
.print(messageOrBuilder);
通過 JsonFormat打印出protobuf JSON形式,但是這個的缺陷是 JsonFormat不支持 list 的 Protobuf類,僅支持單個的protobuf類。
那么只能按照思路一的方式把他套進一個repeated 的 proto中。
得到JSON之后,如果又希望能靈活的往數據結構中增加字段,例如 code/msg/data/ 這種形式,不滿足,還需要增加某些臨時的字段例如 successCount, totalCount, errorCount 等等
這個時候,還需要用FASTJSON 再將這個字符串使用JSON.parseObject 得到 一個 JSONObject,再添加一些字段。這樣比較麻煩,但是也能解決問題。
這種情況返回給HttpMessageConverter處理的是String,因此最終會被StringHttpMessageConverter序列化。
(為了嚴謹,這里因為是StringHttpMessageConverter處理,那么ResponseHeader 的Content-Type是 text/plain;charset=UTF-8
,嚴格來講,如果客戶端沒有正確識別這個JSON字符串,因此還需要在Controller的方法上面,增加額外的produces = MediaType.APPLICATION_JSON_UTF8_VALUE
)
思路三
jackson那么強大,直接讓jackson支持protobuf行不行?
答案是行。
找到jackson的 github項目頁面
然后 發現,readme下方有
jackson-datatype-protobuf for handling datatypes defined by the standard Java protobuf library, developed by HubSpot
NOTE! This is different from jackson-dataformat-protobuf which adds support for encoding/decoding protobuf content but which does NOT depend on standard Java protobuf library
點進入查看 jackson-datatype-protobuf
Jackson module that adds support for serializing and deserializing Google's Protocol Buffers to and from JSON.
Usage
Maven dependency
To use module on Maven-based projects, use following dependency:
<dependency>
<groupId>com.hubspot.jackson</groupId>
<artifactId>jackson-datatype-protobuf</artifactId>
<version><!-- see table below --></version>
</dependency>
那么怎么集成到SpringBoot中呢?
- 引入上述第三方jackson-datatype-protobuf的依賴
- 在項目中引入ProtobufModule。
@Configuration
public class JacksonProtobufSupport {
@Bean
@SuppressWarnings("unchecked")
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return jacksonObjectMapperBuilder -> {
jacksonObjectMapperBuilder.featuresToDisable(
JsonGenerator.Feature.IGNORE_UNKNOWN,
MapperFeature.DEFAULT_VIEW_INCLUSION,
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
);
jacksonObjectMapperBuilder.propertyNamingStrategy(PropertyNamingStrategy.LOWER_CAMEL_CASE);//如果字段都是駝峰命名規則,需要這一句
jacksonObjectMapperBuilder.modulesToInstall(ProtobufModule.class);
};
}
}
完美解決