Skip to content

Commit 7cf0e59

Browse files
authored
Merge pull request #2 from saintf/master
Initial commit of Feign annotation error decoder
2 parents 966b672 + e9d84f5 commit 7cf0e59

17 files changed

+1194
-12
lines changed

.gitignore

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
*.class
2-
3-
# Mobile Tools for Java (J2ME)
4-
.mtj.tmp/
5-
6-
# Package Files #
7-
*.jar
8-
*.war
9-
*.ear
10-
11-
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
12-
hs_err_pid*
1+
.idea
2+
target
3+
*.iml

README.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
Annotation Error Decoder
2+
=========================
3+
4+
This module allows to annotate Feign's interfaces with annotations to generate Exceptions based on error codes
5+
6+
To use AnnotationErrorDecoder with Feign, add the Annotation Error Decoder module to your classpath. Then, configure
7+
Feign to use the AnnotationErrorDecoder:
8+
9+
```java
10+
GitHub github = Feign.builder()
11+
.errorDecoder(
12+
AnnotationErrorDecoder.builderFor(GitHub.class).build()
13+
)
14+
.target(GitHub.class, "https://api.github.com");
15+
```
16+
17+
## Leveraging the annotations and priority order
18+
For annotation decoding to work, the class must be annotated with `@ErrorHandling` tags.
19+
The tags are valid in both the class level as well as method level. They will be treated from 'most specific' to
20+
'least specific' in the following order:
21+
* A code specific exception defined on the method
22+
* A code specific exception defined on the class
23+
* The default exception of the method
24+
* The default exception of the class
25+
26+
```java
27+
@ErrorHandling(codeSpecific =
28+
{
29+
@ErrorCodes( codes = {401}, generate = UnAuthorizedException.class),
30+
@ErrorCodes( codes = {403}, generate = ForbiddenException.class),
31+
@ErrorCodes( codes = {404}, generate = UnknownItemException.class),
32+
},
33+
defaultException = ClassLevelDefaultException.class
34+
)
35+
interface GitHub {
36+
37+
@ErrorHandling(codeSpecific =
38+
{
39+
@ErrorCodes( codes = {404}, generate = NonExistentRepoException.class),
40+
@ErrorCodes( codes = {502, 503, 504}, generate = RetryAfterCertainTimeException.class),
41+
},
42+
defaultException = FailedToGetContributorsException.class
43+
)
44+
@RequestLine("GET /repos/{owner}/{repo}/contributors")
45+
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
46+
}
47+
```
48+
In the above example, error responses to 'contributors' would hence be mapped as follows by status codes:
49+
50+
| Code | Exception | Reason |
51+
| ----------- | -------------------------------- | --------------------- |
52+
| 401 | `UnAuthorizedException` | from Class definition |
53+
| 403 | `ForbiddenException` | from Class definition |
54+
| 404 | `NonExistenRepoException` | from Method definition, note that the class generic exception won't be thrown here |
55+
| 502,503,504 | `RetryAfterCertainTimeException` | from method definition. Note that you can have multiple error codes generate the same type of exception |
56+
| Any Other | `FailedToGetContributorsException` | from Method default |
57+
58+
For a class level default exception to be thrown, the method must not have a `defaultException` defined, nor must the error code
59+
be mapped at either the method or class level.
60+
61+
If the return code cannot be mapped to any code and no default exceptions have been configured, then the decoder will
62+
drop to a default decoder (by default, the standard one provided by feign). You can change the default drop-into decoder
63+
as follows:
64+
65+
```java
66+
GitHub github = Feign.builder()
67+
.errorDecoder(
68+
AnnotationErrorDecoder.builderFor(GitHub.class)
69+
.withDefaultDecoder(new MyOtherErrorDecoder())
70+
.build()
71+
)
72+
.target(GitHub.class, "https://api.github.com");
73+
```
74+
75+
76+
## Complex Exceptions
77+
78+
Finally, Any exception can be used if they have a default constructor:
79+
80+
```java
81+
class DefaultConstructorException extends Exception {}
82+
```
83+
84+
However, if you want to have parameters (such as the body in the error response or headers), you have to annotate its
85+
constructor appropriately (the body annotation is optional, provided there aren't paramters which will clash)
86+
87+
All the following examples are valid exceptions:
88+
```java
89+
class JustBody extends Exception {
90+
91+
@FeignExceptionConstructor
92+
public JustBody(String body) {
93+
94+
}
95+
}
96+
//Headers must be of type Map<String, Collection<String>>
97+
class BodyAndHeaders extends Exception {
98+
99+
@FeignExceptionConstructor
100+
public BodyAndHeaders(@ResponseBody String body, @ResponseHeaders Map<String, Collection<String>> headers) {
101+
102+
}
103+
}
104+
class JustHeaders extends Exception {
105+
106+
@FeignExceptionConstructor
107+
public JustHeaders(@ResponseHeaders Map<String, Collection<String>> headers) {
108+
109+
}
110+
}
111+
```
112+
113+
If you want to have the body decoded, you'll need to pass a decoder at construction time (just as for normal responses):
114+
115+
```java
116+
GitHub github = Feign.builder()
117+
.errorDecoder(
118+
AnnotationErrorDecoder.builderFor(GitHub.class)
119+
.withResponseBodyDecoder(new JacksonDecoder())
120+
.build()
121+
)
122+
.target(GitHub.class, "https://api.github.com");
123+
```
124+
125+
This will enable you to create exceptions where the body is a complex pojo:
126+
127+
```java
128+
class ComplexPojoException extends Exception {
129+
130+
@FeignExceptionConstructor
131+
public ComplexPojoException(GithubExceptionResponse body) {
132+
133+
}
134+
}
135+
//The pojo can then be anything you'd like provided the decoder can manage it
136+
class GithubExceptionResponse {
137+
public String message;
138+
public int githubCode;
139+
public List<String> urlsForHelp;
140+
}
141+
```

pom.xml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
2+
<modelVersion>4.0.0</modelVersion>
3+
4+
<parent>
5+
<groupId>io.github.openfeign</groupId>
6+
<artifactId>parent</artifactId>
7+
<version>9.5.0</version>
8+
</parent>
9+
10+
<artifactId>feign-annotation-error-decoder</artifactId>
11+
<version>0.1.0</version>
12+
<name>Feign Annotation Error Decoder</name>
13+
<description>Feign Annotation Error Decoder</description>
14+
15+
<dependencies>
16+
<dependency>
17+
<groupId>${project.groupId}</groupId>
18+
<artifactId>feign-core</artifactId>
19+
<version>${project.parent.version}</version>
20+
</dependency>
21+
22+
<dependency>
23+
<groupId>${project.groupId}</groupId>
24+
<artifactId>feign-core</artifactId>
25+
<version>${project.parent.version}</version>
26+
<type>test-jar</type>
27+
<scope>test</scope>
28+
</dependency>
29+
30+
<dependency>
31+
<groupId>com.squareup.okhttp3</groupId>
32+
<artifactId>mockwebserver</artifactId>
33+
<scope>test</scope>
34+
</dependency>
35+
</dependencies>
36+
</project>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package feign.error;
2+
3+
import feign.Response;
4+
import feign.codec.Decoder;
5+
import feign.codec.ErrorDecoder;
6+
7+
import java.lang.reflect.Method;
8+
import java.util.HashMap;
9+
import java.util.Map;
10+
11+
import static feign.Feign.configKey;
12+
13+
public class AnnotationErrorDecoder implements ErrorDecoder {
14+
15+
private final Map<String, MethodErrorHandler> errorHandlerMap;
16+
private final ErrorDecoder defaultDecoder;
17+
18+
19+
AnnotationErrorDecoder(Map<String, MethodErrorHandler> errorHandlerMap, ErrorDecoder defaultDecoder) {
20+
this.errorHandlerMap = errorHandlerMap;
21+
this.defaultDecoder = defaultDecoder;
22+
}
23+
24+
@Override
25+
public Exception decode(String methodKey, Response response) {
26+
if(errorHandlerMap.containsKey(methodKey)) {
27+
return errorHandlerMap.get(methodKey).decode(response);
28+
}
29+
return defaultDecoder.decode(methodKey, response);
30+
}
31+
32+
33+
public static AnnotationErrorDecoder.Builder builderFor(Class<?> apiType) {
34+
return new Builder(apiType);
35+
}
36+
37+
public static class Builder {
38+
private final Class<?> apiType;
39+
private ErrorDecoder defaultDecoder = new ErrorDecoder.Default();
40+
private Decoder responseBodyDecoder = new Decoder.Default();
41+
42+
43+
public Builder(Class<?> apiType) {
44+
this.apiType = apiType;
45+
}
46+
47+
public Builder withDefaultDecoder(ErrorDecoder defaultDecoder) {
48+
this.defaultDecoder = defaultDecoder;
49+
return this;
50+
}
51+
52+
public Builder withResponseBodyDecoder(Decoder responseBodyDecoder) {
53+
this.responseBodyDecoder = responseBodyDecoder;
54+
return this;
55+
}
56+
57+
public AnnotationErrorDecoder build() {
58+
Map<String, MethodErrorHandler> errorHandlerMap = generateErrorHandlerMapFromApi(apiType);
59+
return new AnnotationErrorDecoder(errorHandlerMap, defaultDecoder);
60+
}
61+
62+
Map<String, MethodErrorHandler> generateErrorHandlerMapFromApi(Class<?> apiType) {
63+
64+
ExceptionGenerator classLevelDefault = new ExceptionGenerator.Builder()
65+
.withResponseBodyDecoder(responseBodyDecoder)
66+
.withExceptionType(ErrorHandling.NO_DEFAULT.class)
67+
.build();
68+
Map<Integer, ExceptionGenerator> classLevelStatusCodeDefinitions = new HashMap<Integer, ExceptionGenerator>();
69+
70+
if(apiType.isAnnotationPresent(ErrorHandling.class)) {
71+
ErrorHandlingDefinition classErrorHandlingDefinition = readAnnotation(apiType.getAnnotation(ErrorHandling.class), responseBodyDecoder);
72+
classLevelDefault = classErrorHandlingDefinition.defaultThrow;
73+
classLevelStatusCodeDefinitions = classErrorHandlingDefinition.statusCodesMap;
74+
}
75+
76+
Map<String, MethodErrorHandler> methodErrorHandlerMap = new HashMap<String, MethodErrorHandler>();
77+
for(Method method : apiType.getMethods()) {
78+
if(method.isAnnotationPresent(ErrorHandling.class)) {
79+
ErrorHandlingDefinition methodErrorHandling = readAnnotation(method.getAnnotation(ErrorHandling.class), responseBodyDecoder);
80+
ExceptionGenerator methodDefault = methodErrorHandling.defaultThrow;
81+
if(methodDefault.getExceptionType().equals(ErrorHandling.NO_DEFAULT.class)) {
82+
methodDefault = classLevelDefault;
83+
}
84+
85+
MethodErrorHandler methodErrorHandler =
86+
new MethodErrorHandler(methodErrorHandling.statusCodesMap, classLevelStatusCodeDefinitions, methodDefault );
87+
88+
methodErrorHandlerMap.put(configKey(apiType, method), methodErrorHandler);
89+
}
90+
}
91+
92+
return methodErrorHandlerMap;
93+
}
94+
95+
static ErrorHandlingDefinition readAnnotation(ErrorHandling errorHandling, Decoder responseBodyDecoder) {
96+
ExceptionGenerator defaultException = new ExceptionGenerator.Builder()
97+
.withResponseBodyDecoder(responseBodyDecoder)
98+
.withExceptionType(errorHandling.defaultException())
99+
.build();
100+
Map<Integer, ExceptionGenerator> statusCodesDefinition = new HashMap<Integer, ExceptionGenerator>();
101+
102+
for(ErrorCodes statusCodeDefinition : errorHandling.codeSpecific()) {
103+
for(int statusCode : statusCodeDefinition.codes()) {
104+
if(statusCodesDefinition.containsKey(statusCode)) {
105+
throw new IllegalStateException(
106+
"Status Code [" + statusCode + "] " +
107+
"has already been declared to throw [" + statusCodesDefinition.get(statusCode).getExceptionType().getName() + "] " +
108+
"and [" + statusCodeDefinition.generate() + "] - dupe definition");
109+
}
110+
statusCodesDefinition.put(statusCode,
111+
new ExceptionGenerator.Builder()
112+
.withResponseBodyDecoder(responseBodyDecoder)
113+
.withExceptionType(statusCodeDefinition.generate())
114+
.build());
115+
}
116+
}
117+
118+
return new ErrorHandlingDefinition(defaultException, statusCodesDefinition);
119+
}
120+
121+
private static class ErrorHandlingDefinition {
122+
private final ExceptionGenerator defaultThrow;
123+
private final Map<Integer, ExceptionGenerator> statusCodesMap;
124+
125+
126+
private ErrorHandlingDefinition(ExceptionGenerator defaultThrow, Map<Integer, ExceptionGenerator> statusCodesMap) {
127+
this.defaultThrow = defaultThrow;
128+
this.statusCodesMap = statusCodesMap;
129+
}
130+
}
131+
}
132+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package feign.error;
2+
3+
public @interface ErrorCodes {
4+
int[] codes();
5+
Class<? extends Exception> generate();
6+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package feign.error;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@Retention(RetentionPolicy.RUNTIME)
9+
@Target({ ElementType.TYPE, ElementType.METHOD })
10+
public @interface ErrorHandling {
11+
ErrorCodes[] codeSpecific() default {};
12+
Class<? extends Exception> defaultException() default NO_DEFAULT.class;
13+
14+
final class NO_DEFAULT extends Exception {}
15+
}

0 commit comments

Comments
 (0)