Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
ST-DDT committed Jan 17, 2021
2 parents 04d7044 + ffc477b commit b5e2f6f
Show file tree
Hide file tree
Showing 25 changed files with 1,834 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/en/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ customization you need for your project.
- Server
- [Getting Started](server/getting-started.md)
- [Configuration](server/configuration.md)
- [Exception Handling](server/exception-handling.md)
- [Contextual Data / Scoped Beans](server/contextual-data.md)
- [Testing the Service](server/testing.md)
- [Security](server/security.md)
Expand Down
2 changes: 2 additions & 0 deletions docs/en/server/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This section describes how you can configure your grpc-spring-boot-starter appli

- [Getting Started](getting-started.md)
- *Configuration*
- [Exception Handling](exception-handling.md)
- [Contextual Data / Scoped Beans](contextual-data.md)
- [Testing the Service](testing.md)
- [Security](security.md)
Expand Down Expand Up @@ -121,6 +122,7 @@ public GrpcServerConfigurer keepAliveServerConfigurer() {

- [Getting Started](getting-started.md)
- *Configuration*
- [Exception Handling](exception-handling.md)
- [Contextual Data / Scoped Beans](contextual-data.md)
- [Testing the Service](testing.md)
- [Security](security.md)
Expand Down
2 changes: 2 additions & 0 deletions docs/en/server/contextual-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This section describes how you can store contextual / per request data.

- [Getting Started](getting-started.md)
- [Configuration](configuration.md)
- [Exception Handling](exception-handling.md)
- *Contextual Data / Scoped Beans*
- [Testing the Service](testing.md)
- [Security](security.md)
Expand Down Expand Up @@ -61,6 +62,7 @@ public void grpcMethod(Request request, StreamObserver<Response> responseObserve

- [Getting Started](getting-started.md)
- [Configuration](configuration.md)
- [Exception Handling](exception-handling.md)
- *Contextual Data / Scoped Beans*
- [Testing the Service](testing.md)
- [Security](security.md)
Expand Down
119 changes: 119 additions & 0 deletions docs/en/server/exception-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Exception Handling inside GrpcService

[<- Back to Index](../index.md)

This section describes how you can handle exceptions inside GrpcService layer without cluttering up your code.

## Table of Contents <!-- omit in toc -->

- [Proper exception handling](#proper-exception-handling)
- [Detailed explanation](#detailed-explanation)
- [Priority of mapped exceptions](#priority-of-mapped-exceptions)
- [Sending Metadata in response](#sending-metadata-in-response)
- [Overview of returnable types](#overview-of-returnable-types)

## Additional Topics <!-- omit in toc -->

- [Getting Started](getting-started.md)
- [Configuration](configuration.md)
- [Contextual Data](contextual-data.md)
- *Exception Handling*
- [Testing the Service](testing.md)
- [Security](security.md)

## Proper exception handling

If you are already familiar with spring's [error handling](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-error-handling),
you should see some similarities with the exception handling for gRPC.

_An explanation for the following class:_

```java
@GrpcAdvice
public class GrpcExceptionAdvice {


@GrpcExceptionHandler
public Status handleInvalidArgument(IllegalArgumentException e) {
return Status.INVALID_ARGUMENT.withDescription("Your description").withCause(e);
}

@GrpcExceptionHandler(ResourceNotFoundException.class)
public StatusException handleResourceNotFoundException(ResourceNotFoundException e) {
Status status = Status.NOT_FOUND.withDescription("Your description").withCause(e);
Metadata metadata = ...
return status.asException(metadata);
}

}
```

- `@GrpcAdvice` marks a class to be checked up for exception handling methods
- `@GrpcExceptionHandler` marks the annotated method to be executed, in case of the _specified_ exception being thrown
- f.e. if your application throws `IllegalArgumentException`,
then the `handleInvalidArgument(IllegalArgumentException e)` method will be executed
- The method must either return a `io.grpc.Status`, `StatusException`, or `StatusRuntimeException`
- If you handle server errors, you might want to log the exception/stacktrace inside the exception handler

> **Note:** Cause is not transmitted from server to client - as stated in [official docs](https://grpc.github.io/grpc-java/javadoc/io/grpc/Status.html#withCause-java.lang.Throwable-)
> So we recommend adding it to the `Status`/`StatusException` to avoid the loss of information on the server side.
## Detailed explanation

### Priority of mapped exceptions

Given this method with specified exception in the annotation *and* as a method argument

```java
@GrpcExceptionHandler(ResourceNotFoundException.class)
public StatusException handleResourceNotFoundException(ResourceNotFoundException e) {
// your exception handling
}
```

If the `GrpcExceptionHandler` annotation contains at least one exception type, then only those will be
considered for exception handling for that method. The method parameters must be "compatible" with the specified
exception types. If the annotation does not specify any handled exception types, then all method parameters are being
used instead.

_("Compatible" means that the exception type in annotation is either the same class or a superclass of one of the
listed method parameters)_

### Sending Metadata in response

In case you want to send metadata in your exception response, let's have a look at the following example.

```java
@GrpcExceptionHandler
public StatusRuntimeException handleResourceNotFoundException(IllegalArgumentException e) {
Status status = Status.INVALID_ARGUMENT.withDescription("Your description");
Metadata metadata = ...
return status.asRuntimeException(metadata);
}
```

If you do not need `Metadata` in your response, just return your specified `Status`.

### Overview of returnable types

Here is a small overview of possible mapped return types with `@GrpcExceptionHandler` and if custom `Metadata` can be
returned:

| Return Type | Supports Custom Metadata |
| ----------- | --------------- |
| `Status` | &cross; |
| `StatusException` | &#10004; |
| `StatusRuntimeException` | &#10004; |

## Additional Topics <!-- omit in toc -->

- [Getting Started](getting-started.md)
- [Configuration](configuration.md)
- [Contextual Data](contextual-data.md)
- *Exception Handling*
- [Testing the Service](testing.md)
- [Security](security.md)

----------

[<- Back to Index](../index.md)
2 changes: 2 additions & 0 deletions docs/en/server/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This section describes the steps necessary to convert your application into a gr

- *Getting started*
- [Configuration](configuration.md)
- [Exception Handling](exception-handling.md)
- [Contextual Data / Scoped Beans](contextual-data.md)
- [Testing the Service](testing.md)
- [Security](security.md)
Expand Down Expand Up @@ -316,6 +317,7 @@ See [here](testing.md#grpcurl) for `gRPCurl` example command output and addition

- *Getting Started*
- [Configuration](configuration.md)
- [Exception Handling](exception-handling.md)
- [Contextual Data / Scoped Beans](contextual-data.md)
- [Testing the Service](testing.md)
- [Security](security.md)
Expand Down
2 changes: 2 additions & 0 deletions docs/en/server/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ We strongly recommend enabling at least transport layer security.

- [Getting Started](getting-started.md)
- [Configuration](configuration.md)
- [Exception Handling](exception-handling.md)
- [Contextual Data / Scoped Beans](contextual-data.md)
- [Testing the Service](testing.md)
- *Security*
Expand Down Expand Up @@ -279,6 +280,7 @@ public void methodX(Request request, StreamObserver<Response> responseObserver)

- [Getting Started](getting-started.md)
- [Configuration](configuration.md)
- [Exception Handling](exception-handling.md)
- [Contextual Data / Scoped Beans](contextual-data.md)
- [Testing the Service](testing.md)
- *Security*
Expand Down
2 changes: 2 additions & 0 deletions docs/en/server/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Please refer to [Tests with Grpc-Stubs](../client/testing.md).

- [Getting Started](getting-started.md)
- [Configuration](configuration.md)
- [Exception Handling](exception-handling.md)
- [Contextual Data / Scoped Beans](contextual-data.md)
- *Testing the Service*
- [Security](security.md)
Expand Down Expand Up @@ -368,6 +369,7 @@ For more information regarding `gRPCurl` please refer to their [official documen

- [Getting Started](getting-started.md)
- [Configuration](configuration.md)
- [Exception Handling](exception-handling.md)
- [Contextual Data / Scoped Beans](contextual-data.md)
- *Testing the Service*
- [Security](security.md)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2016-2021 Michael Zhang <yidongnan@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package net.devh.boot.grpc.server.advice;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.stereotype.Component;

/**
* Special {@link Component @Component} to declare global gRPC exception handling.
*
* Every class annotated with {@link GrpcAdvice @GrpcAdvice} is marked to be scanned for
* {@link GrpcExceptionHandler @GrpcExceptionHandler} annotations.
* <p>
*
* @author Andjelko Perisic (andjelko.perisic@gmail.com)
* @see GrpcExceptionHandler
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface GrpcAdvice {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2016-2021 Michael Zhang <yidongnan@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package net.devh.boot.grpc.server.advice;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils.MethodFilter;

import lombok.extern.slf4j.Slf4j;

/**
* A discovery class to find all Beans annotated with {@link GrpcAdvice @GrpcAdvice} and for all found beans a second
* search is performed looking for methods with {@link GrpcExceptionHandler @GrpcExceptionHandler}.
*
* @author Andjelko Perisic (andjelko.perisic@gmail.com)
* @see GrpcAdvice
* @see GrpcExceptionHandler
*/
@Slf4j
public class GrpcAdviceDiscoverer implements InitializingBean, ApplicationContextAware {

/**
* A filter for selecting {@code @GrpcExceptionHandler} methods.
*/
public static final MethodFilter EXCEPTION_HANDLER_METHODS =
method -> AnnotatedElementUtils.hasAnnotation(method, GrpcExceptionHandler.class);

private ApplicationContext applicationContext;
private Map<String, Object> annotatedBeans;
private Set<Method> annotatedMethods;

@Override
public void setApplicationContext(final ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}

@Override
public void afterPropertiesSet() {
annotatedBeans = applicationContext.getBeansWithAnnotation(GrpcAdvice.class);
annotatedBeans.forEach(
(key, value) -> log.debug("Found gRPC advice: " + key + ", class: " + value.getClass().getName()));

annotatedMethods = findAnnotatedMethods();
}

private Set<Method> findAnnotatedMethods() {
return this.annotatedBeans.values().stream()
.map(Object::getClass)
.map(this::findAnnotatedMethods)
.flatMap(Collection::stream)
.collect(Collectors.toSet());
}

private Set<Method> findAnnotatedMethods(final Class<?> clazz) {
return MethodIntrospector.selectMethods(clazz, EXCEPTION_HANDLER_METHODS);
}

public Map<String, Object> getAnnotatedBeans() {
Assert.state(annotatedBeans != null, "@GrpcAdvice annotation scanning failed.");
return annotatedBeans;
}

public Set<Method> getAnnotatedMethods() {
Assert.state(annotatedMethods != null, "@GrpcExceptionHandler annotation scanning failed.");
return annotatedMethods;
}

}

0 comments on commit b5e2f6f

Please sign in to comment.