从 SpringBoot 获取 protobuf 消息对象列表
Geting list of protobuf message objects from the SpringBoot
我想从 Spring 启动应用程序中获取 protobuf 消息对象的列表。
我确实设法从应用程序中获取了一个 protobuf 消息对象,但获取它们的列表会引发异常。
...
2020-01-24 14:57:02.359 ERROR 15883 --- [nio-8081-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class com.google.protobuf.UnknownFieldSet$Parser]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.google.protobuf.UnknownFieldSet$Parser and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.ImmutableCollections$ListN[0]->com.example.demo.Lecture["unknownFields"]->com.google.protobuf.UnknownFieldSet["parserForType"])] with root cause
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.google.protobuf.UnknownFieldSet$Parser and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.ImmutableCollections$ListN[0]->com.example.demo.Lecture["unknownFields"]->com.google.protobuf.UnknownFieldSet["parserForType"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77) ~[jackson-databind-2.10.2.jar:2.10.2]
...
我的代码(简化版)。
tl;博士
- 创建Spring启动应用程序
- 从
proto
文件生成 class
- 尝试 return
List
生成的 class 个对象 (RESTful)
我的代码(简化版)。
控制器
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@org.springframework.web.bind.annotation.RestController
@RequestMapping("/timetable")
public class RestController {
@PostMapping("/single") // Works
private Lecture getLecture(@RequestBody Lecture lecture) {
log.info("Single2 got: {}", lecture);
return Lecture.newBuilder(lecture)
.setDuration(lecture.getDuration() +1)
.build();
}
@GetMapping("/list") // Does not work
private @ResponseBody List<Lecture> getLectures() {
return List.of(
Lecture.newBuilder()
.setDuration(1)
.setWeekDay(Lecture.WeekDay.MONDAY)
.setModule(Module.newBuilder().setName("Math1").build())
.build()
// ...
);
}
}
应用程序
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
import org.springframework.http.converter.protobuf.ProtobufJsonFormatHttpMessageConverter;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
@Primary
ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufJsonFormatHttpMessageConverter();
}
}
pom.xml
<!-- ... -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://dzone.com/articles/exposing-microservices-over-rest-protocol-buffers-->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.11.1</version>
</dependency>
<dependency>
<groupId>com.googlecode.protobuf-java-format</groupId>
<artifactId>protobuf-java-format</artifactId>
<version>1.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java-util -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.11.1</version>
</dependency>
</dependencies>
<!-- ... -->
我使用以下方法生成消息对象:
#!/bin/bash
SRC_DIR=../proto
DST_DIR=../../../target/
mkdir -p $DST_DIR
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/college.proto
原始文件
syntax = "proto3";
package my.college;
option java_multiple_files = true;
option java_package = "com.example.demo";
message Module {
string name = 1;
// ... other
}
message Lecture {
WeekDay weekDay = 1;
Module module = 2;
uint32 duration = 3;
// ... other
enum WeekDay {
SUNDAY = 0;
MONDAY = 1;
// ...
}
}
我确实找到了 simmilar issue,但没有解决方案。
解决方法
我找不到问题的解决方案,所以想出了一个解决方法。
我没有返回生成的 protobuf 消息对象,而是返回了这些对象的包装器。使用 Lombok 注释可以完成:
import lombok.Data;
@Data // Lombok magic
public class Module {
private String name;
// ...
public Module(ie.gmit.proto.Module moduleProto){
this.name = moduleProto.getName();
// ...
}
}
这种变通方法感觉还不错,因为它使用标准 Spring 引导依赖项。
说明
Spring 将选择 HttpMessageConverter
与您的响应主体的对象类型相匹配的适当转换器。在这种情况下,它可能会选择 ProtobufJsonFormatHttpMessageConverter
中的 MappingJackson2HttpMessageConverter
,因为您的响应主体的类型为 List.
MappingJackson2HttpMessageConverter
实施 HttpMessageConverter<Object>
ProtobufJsonFormatHttpMessageConverter
实施 HttpMessageConverter<Message>
由于ProtobufJsonFormatHttpMessageConverter
不支持序列化List类型,我们可以改为告诉MappingJackson2HttpMessageConverter
如何通过配置序列化Message类型。
解决方案
Jackson2ObjectMapperBuilderCustomizer
类型的 bean 可用于为消息类型注册序列化程序。
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return o -> o.serializerByType(Message.class, new JsonSerializer<Message>() {
@Override
public void serialize(Message value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeRawValue(JsonFormat.printer().print(value));
}
});
}
我想从 Spring 启动应用程序中获取 protobuf 消息对象的列表。
我确实设法从应用程序中获取了一个 protobuf 消息对象,但获取它们的列表会引发异常。
...
2020-01-24 14:57:02.359 ERROR 15883 --- [nio-8081-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class com.google.protobuf.UnknownFieldSet$Parser]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.google.protobuf.UnknownFieldSet$Parser and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.ImmutableCollections$ListN[0]->com.example.demo.Lecture["unknownFields"]->com.google.protobuf.UnknownFieldSet["parserForType"])] with root cause
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.google.protobuf.UnknownFieldSet$Parser and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.ImmutableCollections$ListN[0]->com.example.demo.Lecture["unknownFields"]->com.google.protobuf.UnknownFieldSet["parserForType"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77) ~[jackson-databind-2.10.2.jar:2.10.2]
...
我的代码(简化版)。
tl;博士
- 创建Spring启动应用程序
- 从
proto
文件生成 class - 尝试 return
List
生成的 class 个对象 (RESTful)
我的代码(简化版)。
控制器
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@org.springframework.web.bind.annotation.RestController
@RequestMapping("/timetable")
public class RestController {
@PostMapping("/single") // Works
private Lecture getLecture(@RequestBody Lecture lecture) {
log.info("Single2 got: {}", lecture);
return Lecture.newBuilder(lecture)
.setDuration(lecture.getDuration() +1)
.build();
}
@GetMapping("/list") // Does not work
private @ResponseBody List<Lecture> getLectures() {
return List.of(
Lecture.newBuilder()
.setDuration(1)
.setWeekDay(Lecture.WeekDay.MONDAY)
.setModule(Module.newBuilder().setName("Math1").build())
.build()
// ...
);
}
}
应用程序
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
import org.springframework.http.converter.protobuf.ProtobufJsonFormatHttpMessageConverter;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
@Primary
ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufJsonFormatHttpMessageConverter();
}
}
pom.xml
<!-- ... -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://dzone.com/articles/exposing-microservices-over-rest-protocol-buffers-->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.11.1</version>
</dependency>
<dependency>
<groupId>com.googlecode.protobuf-java-format</groupId>
<artifactId>protobuf-java-format</artifactId>
<version>1.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java-util -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.11.1</version>
</dependency>
</dependencies>
<!-- ... -->
我使用以下方法生成消息对象:
#!/bin/bash
SRC_DIR=../proto
DST_DIR=../../../target/
mkdir -p $DST_DIR
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/college.proto
原始文件
syntax = "proto3";
package my.college;
option java_multiple_files = true;
option java_package = "com.example.demo";
message Module {
string name = 1;
// ... other
}
message Lecture {
WeekDay weekDay = 1;
Module module = 2;
uint32 duration = 3;
// ... other
enum WeekDay {
SUNDAY = 0;
MONDAY = 1;
// ...
}
}
我确实找到了 simmilar issue,但没有解决方案。
解决方法
我找不到问题的解决方案,所以想出了一个解决方法。
我没有返回生成的 protobuf 消息对象,而是返回了这些对象的包装器。使用 Lombok 注释可以完成:
import lombok.Data;
@Data // Lombok magic
public class Module {
private String name;
// ...
public Module(ie.gmit.proto.Module moduleProto){
this.name = moduleProto.getName();
// ...
}
}
这种变通方法感觉还不错,因为它使用标准 Spring 引导依赖项。
说明
Spring 将选择 HttpMessageConverter
与您的响应主体的对象类型相匹配的适当转换器。在这种情况下,它可能会选择 ProtobufJsonFormatHttpMessageConverter
中的 MappingJackson2HttpMessageConverter
,因为您的响应主体的类型为 List.
MappingJackson2HttpMessageConverter
实施 HttpMessageConverter<Object>
ProtobufJsonFormatHttpMessageConverter
实施 HttpMessageConverter<Message>
由于ProtobufJsonFormatHttpMessageConverter
不支持序列化List类型,我们可以改为告诉MappingJackson2HttpMessageConverter
如何通过配置序列化Message类型。
解决方案
Jackson2ObjectMapperBuilderCustomizer
类型的 bean 可用于为消息类型注册序列化程序。
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return o -> o.serializerByType(Message.class, new JsonSerializer<Message>() {
@Override
public void serialize(Message value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeRawValue(JsonFormat.printer().print(value));
}
});
}