Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding support for meta-annotations #1458

Merged
merged 2 commits into from
Jul 19, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
112 changes: 110 additions & 2 deletions annotation-error-decoder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ GitHub github = Feign.builder()
```

## Leveraging the annotations and priority order
For annotation decoding to work, the class must be annotated with `@ErrorHandling` tags.
For annotation decoding to work, the class must be annotated with `@ErrorHandling` tags or meta-annotations.
The tags are valid in both the class level as well as method level. They will be treated from 'most specific' to
'least specific' in the following order:
* A code specific exception defined on the method
Expand Down Expand Up @@ -248,4 +248,112 @@ interface GitHub3 extends FeignClientBase {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
}
```
```

## Meta-annotations
When you want to share the same configuration of one `@ErrorHandling` annotation the `@ErrorHandling` annotation
can be moved to a meta-annotation. Then later on this meta-annotation can be used on a method or at class level to
reduce the amount duplicated code. A meta-annotation is a special annotation that contains the `@ErrorHandling`
annotation and possibly other annotations, e.g. Spring-Rest annotations.

There are some limitations and rules to keep in mind when using meta-annotation:
- inheritance for meta-annotations when using interface inheritance is supported and is following the same rules as for
interface inheritance (see above)
- `@ErrorHandling` has **precedence** over any meta-annotation when placed together on a class or method
- a meta-annotation on a child interface (method or class) has **precedence** over the error handling defined in the
parent interface
- having a meta-annotation on a meta-annotation is not supported, only the annotations on a type are checked for a
`@ErrorHandling`
- when multiple meta-annotations with an `@ErrorHandling` annotation are present on a class or method the first one
which is returned by java API is used to figure out the error handling, the others are not considered, so it is
advisable to have only one meta-annotation on each method or class as the order is not guaranteed.
- **no merging** of configurations is supported, e.g. multiple meta-annotations on the same type, meta-annotation with
`@ErrorHandling` on the same type

Example:

Let's assume multiple methods need to handle the response-code `404` in the same way but differently what is
specified in the `@ErrorHandling` annotation on the class-level. In that case, to avoid also duplicate annotation definitions
on the affected methods a meta-annotation can reduce the amount of code to be written to handle this `404` differently.

In the following code the status-code `404` is handled on a class level which throws an `UnknownItemException` for all
methods inside this interface. For the methods `contributors` and `languages` a different exceptions needs to be thrown,
in this case it is a `NoDataFoundException`. The `teams`method will still use the exception defined by the class-level
error handling annotation. To simplify the code a meta-annotation can be created and be used in the interface to keep
the interface small and readable.

```java
@ErrorHandling(
codeSpecific = {
@ErrorCodes(codes = {404}, generate = NoDataFoundException.class),
},
defaultException = GithubRemoteException.class)
@Retention(RetentionPolicy.RUNTIME)
@interface NoDataErrorHandling {
}
```

Having this meta-annotation in place it can be used to transform the interface into a much smaller one, keeping the same
behavior.
- `contributers` will throw a `NoDataFoundException` for status code `404` as defined on method level and a
`GithubRemoteException` for all other status codes
- `languages` will throw a `NoDataFoundException` for status code `404` as defined on method level and a
`GithubRemoteException` for all other status codes
- `teams` will throw a `UnknownItemException` for status code `404` as defined on class level and a
`ClassLevelDefaultException` for all other status codes

Before:
```java
@ErrorHandling(codeSpecific =
{
@ErrorCodes( codes = {404}, generate = UnknownItemException.class)
},
defaultException = ClassLevelDefaultException.class
)
interface GitHub {
@ErrorHandling(codeSpecific =
{
@ErrorCodes( codes = {404}, generate = NoDataFoundException.class)
},
defaultException = GithubRemoteException.class
)
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);

@ErrorHandling(codeSpecific =
{
@ErrorCodes( codes = {404}, generate = NoDataFoundException.class)
},
defaultException = GithubRemoteException.class
)
@RequestLine("GET /repos/{owner}/{repo}/languages")
Map<String, Integer> languages(@Param("owner") String owner, @Param("repo") String repo);

@ErrorHandling
@RequestLine("GET /repos/{owner}/{repo}/team")
List<Team> languages(@Param("owner") String owner, @Param("repo") String repo);
}
```

After:
```java
@ErrorHandling(codeSpecific =
{
@ErrorCodes( codes = {404}, generate = UnknownItemException.class)
},
defaultException = ClassLevelDefaultException.class
)
interface GitHub {
@NoDataErrorHandling
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);

@NoDataErrorHandling
@RequestLine("GET /repos/{owner}/{repo}/languages")
Map<String, Integer> languages(@Param("owner") String owner, @Param("repo") String repo);

@ErrorHandling
@RequestLine("GET /repos/{owner}/{repo}/team")
List<Team> languages(@Param("owner") String owner, @Param("repo") String repo);
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import feign.Response;
import feign.codec.Decoder;
import feign.codec.ErrorDecoder;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -93,9 +95,10 @@ Map<String, MethodErrorHandler> generateErrorHandlerMapFromApi(Class<?> apiType)
Map<String, MethodErrorHandler> methodErrorHandlerMap =
new HashMap<String, MethodErrorHandler>();
for (Method method : apiType.getMethods()) {
if (method.isAnnotationPresent(ErrorHandling.class)) {
ErrorHandling methodLevelAnnotation = getErrorHandlingAnnotation(method);
if (methodLevelAnnotation != null) {
ErrorHandlingDefinition methodErrorHandling =
readAnnotation(method.getAnnotation(ErrorHandling.class), responseBodyDecoder);
readAnnotation(methodLevelAnnotation, responseBodyDecoder);
ExceptionGenerator methodDefault = methodErrorHandling.defaultThrow;
if (methodDefault.getExceptionType().equals(ErrorHandling.NO_DEFAULT.class)) {
methodDefault = classLevelDefault;
Expand All @@ -113,8 +116,9 @@ Map<String, MethodErrorHandler> generateErrorHandlerMapFromApi(Class<?> apiType)
}

Optional<ErrorHandling> readErrorHandlingIncludingInherited(Class<?> apiType) {
if (apiType.isAnnotationPresent(ErrorHandling.class)) {
return Optional.of(apiType.getAnnotation(ErrorHandling.class));
ErrorHandling apiTypeAnnotation = getErrorHandlingAnnotation(apiType);
if (apiTypeAnnotation != null) {
return Optional.of(apiTypeAnnotation);
}
for (Class<?> parentInterface : apiType.getInterfaces()) {
Optional<ErrorHandling> errorHandling =
Expand All @@ -130,6 +134,19 @@ Optional<ErrorHandling> readErrorHandlingIncludingInherited(Class<?> apiType) {
return Optional.empty();
}

private static ErrorHandling getErrorHandlingAnnotation(AnnotatedElement element) {
ErrorHandling annotation = element.getAnnotation(ErrorHandling.class);
if (annotation == null) {
for (Annotation metaAnnotation : element.getAnnotations()) {
annotation = metaAnnotation.annotationType().getAnnotation(ErrorHandling.class);
if (annotation != null) {
break;
}
}
}
return annotation;
}

static ErrorHandlingDefinition readAnnotation(ErrorHandling errorHandling,
Decoder responseBodyDecoder) {
ExceptionGenerator defaultException = new ExceptionGenerator.Builder()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Copyright 2012-2021 The Feign 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
*
* http://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 feign.error;

import static org.assertj.core.api.Assertions.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;

@RunWith(Parameterized.class)
public class AnnotationErrorDecoderAnnotationInheritanceTest extends
AbstractAnnotationErrorDecoderTest<AnnotationErrorDecoderAnnotationInheritanceTest.TestClientInterfaceWithWithMetaAnnotation> {
@Override
public Class<TestClientInterfaceWithWithMetaAnnotation> interfaceAtTest() {
return TestClientInterfaceWithWithMetaAnnotation.class;
}

@Parameters(
name = "{0}: When error code ({1}) on method ({2}) should return exception type ({3})")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][] {
{"Test Code Specific At Method", 402, "method1Test", MethodLevelDefaultException.class},
{"Test Code Specific At Method", 403, "method1Test", MethodLevelNotFoundException.class},
{"Test Code Specific At Method", 404, "method1Test", MethodLevelNotFoundException.class},
{"Test Code Specific At Method", 402, "method2Test", ClassLevelDefaultException.class},
{"Test Code Specific At Method", 403, "method2Test", MethodLevelNotFoundException.class},
{"Test Code Specific At Method", 404, "method2Test", ClassLevelNotFoundException.class},
});
}

@Parameter // first data value (0) is default
public String testType;

@Parameter(1)
public int errorCode;

@Parameter(2)
public String method;

@Parameter(3)
public Class<? extends Exception> expectedExceptionClass;

@Test
public void test() throws Exception {
AnnotationErrorDecoder decoder =
AnnotationErrorDecoder.builderFor(TestClientInterfaceWithWithMetaAnnotation.class).build();

assertThat(decoder.decode(feignConfigKey(method), testResponse(errorCode)).getClass())
.isEqualTo(expectedExceptionClass);
}

@ClassError
interface TestClientInterfaceWithWithMetaAnnotation {
@MethodError
void method1Test();

@ErrorHandling(
codeSpecific = {@ErrorCodes(codes = {403}, generate = MethodLevelNotFoundException.class)})
void method2Test();
}

@ErrorHandling(
codeSpecific = {@ErrorCodes(codes = {404}, generate = ClassLevelNotFoundException.class),},
defaultException = ClassLevelDefaultException.class)
@Retention(RetentionPolicy.RUNTIME)
@interface ClassError {
}

@ErrorHandling(
codeSpecific = {
@ErrorCodes(codes = {404, 403}, generate = MethodLevelNotFoundException.class),},
defaultException = MethodLevelDefaultException.class)
@Retention(RetentionPolicy.RUNTIME)
@interface MethodError {
}

static class ClassLevelDefaultException extends Exception {
public ClassLevelDefaultException() {}
}
static class ClassLevelNotFoundException extends Exception {
public ClassLevelNotFoundException() {}
}
static class MethodLevelDefaultException extends Exception {
public MethodLevelDefaultException() {}
}
static class MethodLevelNotFoundException extends Exception {
public MethodLevelNotFoundException() {}
}
}