转载自:让 SpringBoot 的 jackson 支持 JavaBean 嵌套的 protobuf - 一杯半盏 - 博客园 (cnblogs.com)
问题背景
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); }; } }
完美解决