From 3feca42834ec05f3acd22844da2f0b5e7036469a Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Thu, 21 Nov 2024 15:49:46 +0000 Subject: [PATCH] Move exception handling into autoconfiguration --- .../grpc/sample/GrpcServerApplication.java | 11 ++-- .../sample/GrpcServerIntegrationTests.java | 26 ++++++++++ .../modules/ROOT/partials/_configprops.adoc | 5 +- ...GrpcExceptionHandlerAutoConfiguration.java | 50 +++++++++++++++++++ ...itional-spring-configuration-metadata.json | 6 +++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...essGrpcServerFactoryAutoConfiguration.java | 6 ++- 7 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/exception/GrpcExceptionHandlerAutoConfiguration.java diff --git a/samples/grpc-server/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java b/samples/grpc-server/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java index 08d7509..0caa6ce 100644 --- a/samples/grpc-server/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java +++ b/samples/grpc-server/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java @@ -3,10 +3,8 @@ package org.springframework.grpc.sample; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; -import org.springframework.grpc.server.GlobalServerInterceptor; -import org.springframework.grpc.server.exception.GrpcExceptionHandlerInterceptor; +import org.springframework.grpc.server.exception.GrpcExceptionHandler; -import io.grpc.ServerInterceptor; import io.grpc.Status; @SpringBootApplication @@ -16,15 +14,14 @@ public class GrpcServerApplication { SpringApplication.run(GrpcServerApplication.class, args); } - @GlobalServerInterceptor @Bean - public ServerInterceptor globalInterceptor() { - return new GrpcExceptionHandlerInterceptor(exception -> { + public GrpcExceptionHandler globalInterceptor() { + return exception -> { if (exception instanceof IllegalArgumentException) { return Status.INVALID_ARGUMENT.withDescription(exception.getMessage()); } return null; - }); + }; } } diff --git a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerIntegrationTests.java b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerIntegrationTests.java index 49501a6..e702128 100644 --- a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerIntegrationTests.java +++ b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerIntegrationTests.java @@ -82,6 +82,32 @@ class GrpcServerIntegrationTests { } + @Nested + @SpringBootTest("spring.grpc.server.exception-handler.enabled=false") + class ServerWithUnhandledException { + + @Test + void specificErrorResponse(@Autowired GrpcChannelFactory channels) { + SimpleGrpc.SimpleBlockingStub client = SimpleGrpc + .newBlockingStub(channels.createChannel("0.0.0.0:0").build()); + assertThat(assertThrows(StatusRuntimeException.class, + () -> client.sayHello(HelloRequest.newBuilder().setName("error").build())) + .getStatus() + .getCode()).isEqualTo(Code.UNKNOWN); + } + + @Test + void defaultErrorResponseIsUnknown(@Autowired GrpcChannelFactory channels) { + SimpleGrpc.SimpleBlockingStub client = SimpleGrpc + .newBlockingStub(channels.createChannel("0.0.0.0:0").build()); + assertThat(assertThrows(StatusRuntimeException.class, + () -> client.sayHello(HelloRequest.newBuilder().setName("internal").build())) + .getStatus() + .getCode()).isEqualTo(Code.UNKNOWN); + } + + } + @Nested @SpringBootTest(properties = { "spring.grpc.server.host=0.0.0.0", "spring.grpc.server.port=0", "spring.grpc.inprocess.enabled=false" }) diff --git a/spring-grpc-docs/src/main/antora/modules/ROOT/partials/_configprops.adoc b/spring-grpc-docs/src/main/antora/modules/ROOT/partials/_configprops.adoc index b863675..59a5b0b 100644 --- a/spring-grpc-docs/src/main/antora/modules/ROOT/partials/_configprops.adoc +++ b/spring-grpc-docs/src/main/antora/modules/ROOT/partials/_configprops.adoc @@ -17,6 +17,7 @@ |spring.grpc.client.default-channel.ssl.enabled | | Whether to enable SSL support. Enabled automatically if "bundle" is provided unless specified otherwise. |spring.grpc.client.default-channel.user-agent | | |spring.grpc.server.address | | The address to bind to. could be a host:port combination or a pseudo URL like static://host:port. Can not be set if host or port are set independently. +|spring.grpc.server.exception-handling.enabled | `+++true+++` | Whether to enable user-defined global exception handling on the gRPC server. |spring.grpc.server.health.actuator.enabled | | Whether to adapt Actuator health checks into gRPC health checks. |spring.grpc.server.health.actuator.endpoints | | List of Actuator health checks to adapt into gRPC health checks. |spring.grpc.server.health.enabled | | Whether to auto-configure Health feature on the gRPC server. @@ -30,7 +31,9 @@ |spring.grpc.server.keep-alive.timeout | | Maximum time to wait for read activity after sending a keep alive ping. If sender does not receive an acknowledgment within this time, it will close the connection (default 20s). |spring.grpc.server.max-inbound-message-size | | Maximum message size allowed to be received by the server (default 4MiB). |spring.grpc.server.max-inbound-metadata-size | | Maximum metadata size allowed to be received by the server (default 8KiB). -|spring.grpc.server.port | | Server port to listen on. When the value is 0, a random available port is selected. The default is 9090. +|spring.grpc.server.observations.enabled | `+++true+++` | Whether to enable Observations on the server. +|spring.grpc.server.port | `+++9090+++` | Server port to listen on. When the value is 0, a random available port is selected. The default is 9090. +|spring.grpc.server.reflection.enabled | `+++true+++` | Whether to enable Reflection on the gRPC server. |spring.grpc.server.shutdown-grace-period | | Maximum time to wait for the server to gracefully shutdown. When the value is negative, the server waits forever. When the value is 0, the server will force shutdown immediately. The default is 30 seconds. |spring.grpc.server.ssl.bundle | | SSL bundle name. |spring.grpc.server.ssl.client-auth | | Client authentication mode. diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/exception/GrpcExceptionHandlerAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/exception/GrpcExceptionHandlerAutoConfiguration.java new file mode 100644 index 0000000..b3ccbc2 --- /dev/null +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/exception/GrpcExceptionHandlerAutoConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.grpc.autoconfigure.server.exception; + +import java.util.stream.Collectors; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.exception.CompositeGrpcExceptionHandler; +import org.springframework.grpc.server.exception.GrpcExceptionHandler; +import org.springframework.grpc.server.exception.GrpcExceptionHandlerInterceptor; + +import io.grpc.Grpc; + +@AutoConfiguration +@ConditionalOnClass(Grpc.class) +@ConditionalOnBean(GrpcExceptionHandler.class) +@ConditionalOnMissingBean(GrpcExceptionHandlerInterceptor.class) +@ConditionalOnProperty(prefix = "spring.grpc.server.exception-handler", name = "enabled", havingValue = "true", + matchIfMissing = true) +public class GrpcExceptionHandlerAutoConfiguration { + + @GlobalServerInterceptor + @Bean + public GrpcExceptionHandlerInterceptor globalExceptionHandlerInterceptor( + ObjectProvider exceptionHandler) { + return new GrpcExceptionHandlerInterceptor(new CompositeGrpcExceptionHandler( + exceptionHandler.orderedStream().collect(Collectors.toList()).toArray(new GrpcExceptionHandler[0]))); + } + +} diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 156af1b..bbb55ff 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -11,6 +11,12 @@ "description": "Whether to enable Reflection on the gRPC server.", "defaultValue": true }, + { + "name": "spring.grpc.server.exception-handling.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable user-defined global exception handling on the gRPC server.", + "defaultValue": true + }, { "name": "spring.grpc.server.observations.enabled", "type": "java.lang.Boolean", diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 48d95eb..0351d81 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -4,3 +4,4 @@ org.springframework.grpc.autoconfigure.server.GrpcServerAutoConfiguration org.springframework.grpc.autoconfigure.server.GrpcServerHealthAutoConfiguration org.springframework.grpc.autoconfigure.server.GrpcServerObservationAutoConfiguration org.springframework.grpc.autoconfigure.server.GrpcServerReflectionAutoConfiguration +org.springframework.grpc.autoconfigure.server.exception.GrpcExceptionHandlerAutoConfiguration diff --git a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactoryAutoConfiguration.java b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactoryAutoConfiguration.java index 1d59bfc..295b0e8 100644 --- a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactoryAutoConfiguration.java +++ b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactoryAutoConfiguration.java @@ -37,11 +37,13 @@ import io.grpc.inprocess.InProcessServerBuilder; @ConditionalOnNotWebApplication public class InProcessGrpcServerFactoryAutoConfiguration { + private String address = InProcessServerBuilder.generateName(); + @Bean @ConditionalOnBean(BindableService.class) InProcessGrpcServerFactory grpcServerFactory(GrpcServiceDiscoverer grpcServicesDiscoverer, List> customizers) { - InProcessGrpcServerFactory factory = new InProcessGrpcServerFactory("test", customizers); + InProcessGrpcServerFactory factory = new InProcessGrpcServerFactory(address, customizers); grpcServicesDiscoverer.findServices().forEach(factory::addService); return factory; } @@ -49,7 +51,7 @@ public class InProcessGrpcServerFactoryAutoConfiguration { @Bean InProcessGrpcChannelFactory grpcChannelFactory() { InProcessGrpcChannelFactory factory = new InProcessGrpcChannelFactory(); - factory.setVirtualTargets(path -> "test"); + factory.setVirtualTargets(path -> address); return factory; }