问题
I am attempting to write a set of services that will provide a REST interface to external clients, while using gRPC/protobuf for inter-service communication internally. Spring Boot is being utilized to help with the background infrastructure support. I only started learning and working with Google protobuf and gRPC about a week ago.
I have been able to successfully get simpler processing to work from end-to-end, but these are returning a single object (i.e. no streaming). I'm having a problem with a call that is intended to stream a collection of response objects. The other thing to note is that everything I have attempted so far have been GET requests.
The samples below just show the relevant portions that I'm having trouble with.
Details:
- Java 8
- Spring Boot 2.1.6.RELEASE
- Spring Boot gRPC starter 2.5.1.RELEASE (https://yidongnan.github.io/grpc-spring-boot-starter/)
- Google protobuf Java util 3.10.0
Protobuf definitions:
syntax = "proto3";
package experiment;
message BOM {
PartSpec part_spec = 1;
string description = 2;
string category = 3;
}
message PartSpec {
string part_number = 1;
string cage_code = 2;
}
service BOMService {
rpc getBOMList(google.protobuf.Empty) returns (stream BOM) {}
}
"Domain" service:
Implements the service defined by the .proto file. Responds to requests from the "gateway" service.
@GrpcService
public class BOMDomainService extends BOMServiceImplBase {
private final Logger logger = LogManager.getLogger();
private Random rand = new Random(1);
@Override
public void getBOMList(Empty request, StreamObserver<BOM> responseObserver) {
for (int i = 1; i <= rand.nextInt(40); i++) {
PartSpec part = PartSpec.newBuilder().setPartNumber("part-1").setCageCode("cage-1").build();
BOM bom = BOM.newBuilder().setPartSpec(part).setDescription("some part").build();
// Stream BOM response.
responseObserver.onNext(bom);
}
// Complete server response.
responseObserver.onCompleted();
}
}
"Gateway"
Gateway service:
Interacts with the "domain" service via gRPC calls (blocking stub).
@Service
public class BOMGatewayService {
private final Logger logger = LogManager.getLogger();
@GrpcClient("bom-service")
private BOMServiceBlockingStub bomSvcStub;
public List<BOM> getBOMList() {
List<BOM> result = new ArrayList<>();
try {
Iterator<BOM> iter = bomSvcStub.getBOMList(Empty.getDefaultInstance());
while (iter.hasNext()) {
result.add(iter.next());
}
} catch (StatusRuntimeException e) {
logger.warn("RPC failed: {}", e.getStatus());
}
return result;
}
}
Gateway controller:
@RestController
@RequestMapping("/alisn/bom")
public class BOMController {
@Autowired
private BOMGatewayService service;
@GetMapping(value = "/getBOMList", produces = { MediaType.APPLICATION_JSON_VALUE })
public DeferredResult<ResponseEntity<List<BOM>>> getBOMList() {
DeferredResult<ResponseEntity<List<BOM>>> result = new DeferredResult<>();
List<BOM> response = service.getBOMList();
result.setResult(ResponseEntity.ok(response));
return result;
}
}
External "Client" service:
Interacts with the "gateway" service via REST calls. Does not use gRPC, but does use the message classes generated from the .proto file.
NOTE: I am not trying to get the list of "BOM"s here. If I try to use ResponseEntity<List<BOM>>
, the getForEntity()
method throws compiler errors. I'm not sure what I should be doing here and maybe that's part of the problem.
@Service
public class ClientService {
private final Logger logger = LogManager.getLogger();
@Autowired
private RestTemplate restTemplate;
@Scheduled(fixedDelay = 5000)
public void getBOMList() {
ResponseEntity<BOM> response = restTemplate
.getForEntity("http://localhost:7090/alisn/bom/getBOMList", BOM.class);
logger.info("Received:\n{}", response.getBody());
}
}
I have attempted to exercise this in two ways:
- From a client Java Spring Boot service that uses a
RestTemplate
to make the REST call (the "Client" shown above). - Making the HTTP GET request from Postman.
The response from both methods seems to be more or less the same.
Java client method:
Client console:
[2019-09-27 10:01:16.373] scheduling-1 ERROR: support.TaskUtils$LoggingErrorHandler:96 - Unexpected error occurred in scheduled task.
org.springframework.web.client.HttpServerErrorException$InternalServerError: 500 null
at org.springframework.web.client.HttpServerErrorException.create(HttpServerErrorException.java:79) ~[spring-web-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:124) ~[spring-web-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:102) ~[spring-web-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63) ~[spring-web-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:778) ~[spring-web-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:736) ~[spring-web-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:670) ~[spring-web-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:338) ~[spring-web-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at edu.mit.ll.alisn.exp.client.service.ClientService.getBOMList(ClientService.java:26) ~[classes/:?]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_201]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_201]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_201]
at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_201]
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84) ~[spring-context-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) [spring-context-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [?:1.8.0_201]
at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308) [?:1.8.0_201]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180) [?:1.8.0_201]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294) [?:1.8.0_201]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:1.8.0_201]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:1.8.0_201]
at java.lang.Thread.run(Thread.java:748) [?:1.8.0_201]
"Gateway" console:
[2019-09-27 10:01:16.356] http-nio-7090-exec-3 ERROR: [/].[dispatcherServlet]:175 - Servlet.service() for servlet [dispatcherServlet] threw exception
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle (through reference chain: java.util.ArrayList[0]->edu.mit.ll.alisn.exp.proto.BOM["unknownFields"]->com.google.protobuf.UnknownFieldSet["defaultInstanceForType"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77) ~[jackson-databind-2.9.9.jar:2.9.9]
at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1191) ~[jackson-databind-2.9.9.jar:2.9.9]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter._handleSelfReference(BeanPropertyWriter.java:944) ~[jackson-databind-2.9.9.jar:2.9.9]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:721) ~[jackson-databind-2.9.9.jar:2.9.9]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719) ~[jackson-databind-2.9.9.jar:2.9.9]
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155) ~[jackson-databind-2.9.9.jar:2.9.9]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727) ~[jackson-databind-2.9.9.jar:2.9.9]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719) ~[jackson-databind-2.9.9.jar:2.9.9]
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155) ~[jackson-databind-2.9.9.jar:2.9.9]
at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serializeContentsUsing(CollectionSerializer.java:171) ~[jackson-databind-2.9.9.jar:2.9.9]
at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serializeContents(CollectionSerializer.java:116) ~[jackson-databind-2.9.9.jar:2.9.9]
at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serialize(CollectionSerializer.java:107) ~[jackson-databind-2.9.9.jar:2.9.9]
at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serialize(CollectionSerializer.java:25) ~[jackson-databind-2.9.9.jar:2.9.9]
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480) ~[jackson-databind-2.9.9.jar:2.9.9]
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:400) ~[jackson-databind-2.9.9.jar:2.9.9]
at com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1392) ~[jackson-databind-2.9.9.jar:2.9.9]
at com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:913) ~[jackson-databind-2.9.9.jar:2.9.9]
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:287) ~[spring-web-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:103) ~[spring-web-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:290) ~[spring-webmvc-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.handleReturnValue(HttpEntityMethodProcessor.java:225) ~[spring-webmvc-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:82) ~[spring-web-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:122) ~[spring-webmvc-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892) ~[spring-webmvc-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) ~[spring-webmvc-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039) ~[spring-webmvc-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942) ~[spring-webmvc-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005) ~[spring-webmvc-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897) ~[spring-webmvc-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:634) ~[tomcat-embed-core-9.0.21.jar:9.0.21]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882) ~[spring-webmvc-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741) ~[tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.21.jar:9.0.21]
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.filterAndRecordMetrics(WebMvcMetricsFilter.java:114) ~[spring-boot-actuator-2.1.6.RELEASE.jar:2.1.6.RELEASE]
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:104) ~[spring-boot-actuator-2.1.6.RELEASE.jar:2.1.6.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109) ~[spring-web-5.1.8.RELEASE.jar:5.1.8.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:712) [tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.catalina.core.ApplicationDispatcher.doDispatch(ApplicationDispatcher.java:633) [tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.catalina.core.ApplicationDispatcher.dispatch(ApplicationDispatcher.java:601) [tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.catalina.core.AsyncContextImpl$AsyncRunnable.run(AsyncContextImpl.java:566) [tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.catalina.core.AsyncContextImpl.doInternalDispatch(AsyncContextImpl.java:355) [tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199) [tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490) [tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) [tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:235) [tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.coyote.AbstractProcessor.dispatch(AbstractProcessor.java:241) [tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:53) [tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:853) [tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1587) [tomcat-embed-core-9.0.21.jar:9.0.21]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.21.jar:9.0.21]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:1.8.0_201]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:1.8.0_201]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.21.jar:9.0.21]
at java.lang.Thread.run(Thread.java:748) [?:1.8.0_201]
Postman:
{
"timestamp": "2019-09-27T14:03:38.530+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Type definition error: [simple type, class com.google.protobuf.UnknownFieldSet]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle (through reference chain: java.util.ArrayList[0]->edu.mit.ll.alisn.exp.proto.BOM[\"unknownFields\"]->com.google.protobuf.UnknownFieldSet[\"defaultInstanceForType\"])",
"path": "/alisn/bom/getBOMList"
}
Any suggestions about what I'm doing wrong, or suggestions on a better way to do this type of process would be greatly appreciated.
UPDATE:
I am also including the bean configurations for the client and gateway service.
Client:
@Configuration
@EnableScheduling
@ComponentScan(XXXXXX)
public class Configurer {
@Bean
ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufJsonFormatHttpMessageConverter();
}
@Bean
RestTemplate restTemplate(ProtobufHttpMessageConverter hmc) {
return new RestTemplate(Arrays.asList(hmc));
}
}
Gateway:
@Configuration
@ComponentScan(XXXXXX)
public class Configurer {
@Bean
ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufJsonFormatHttpMessageConverter();
}
}
UPDATE:
I have done a simpler experiment where I created a new service that just returns one BOM
object. This works just fine.
I don't understand why this works and the List<BOM>
case does not. The information in the exception thrown seems to point to internal structure within the generated BOM
class.
Gateway console:
[2019-10-02 08:37:25.960] http-nio-7090-exec-3 ERROR: [/].[dispatcherServlet]:175 - 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.ArrayList[0]->edu.mit.ll.alisn.exp.proto.BOM["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.ArrayList[0]->edu.mit.ll.alisn.exp.proto.BOM["unknownFields"]->com.google.protobuf.UnknownFieldSet["parserForType"])
The reference to: java.util.ArrayList[0]->edu.mit.ll.alisn.exp.proto.BOM["unknownFields"]->com.google.protobuf.UnknownFieldSet["parserForType"]
to me seems to indicate a problem with the internal structure of the BOM
class, and yet I can return a single BOM
without any problem.
回答1:
According to the Gateway log, it looks like jackson
is having trouble converting proto to json.
can you try com.googlecode.protobuf.format.JsonFormat
? I am not spring user, but there should be API for custom serialization. or, you can add custom serialization to jackson using JsonFormat
but it sounds little weird.
来源:https://stackoverflow.com/questions/58136430/problem-returning-stream-of-responses-from-grpc-service-to-restful-client