Skip to content

Commit 3e6e093

Browse files
authored
Spring4 contract (OpenFeign#1069)
* Move DeclarativeContract to new file * Get spring4 contract to compile with feign10 * Move to declarative contract * Brought spring 4 contract back to life * Remove old badges * Throw error when contract mark a method as ignored
1 parent 3358350 commit 3e6e093

10 files changed

Lines changed: 404 additions & 2 deletions

File tree

core/src/main/java/feign/Contract.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method me
8989
for (Annotation methodAnnotation : method.getAnnotations()) {
9090
processAnnotationOnMethod(data, methodAnnotation, method);
9191
}
92+
if (data.isIgnored()) {
93+
return data;
94+
}
9295
checkState(data.template().method() != null,
9396
"Method %s not annotated with HTTP method type (ex. GET, POST)",
9497
data.configKey());

core/src/main/java/feign/MethodMetadata.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public final class MethodMetadata implements Serializable {
3838
private Map<Integer, Boolean> indexToEncoded = new LinkedHashMap<Integer, Boolean>();
3939
private transient Map<Integer, Expander> indexToExpander;
4040
private BitSet parameterToIgnore = new BitSet();
41+
private boolean ignored;
4142

4243
MethodMetadata() {}
4344

@@ -204,6 +205,12 @@ public boolean isAlreadyProcessed(Integer index) {
204205
|| parameterToIgnore.get(index);
205206
}
206207

208+
public void ignoreMethod() {
209+
this.ignored = true;
210+
}
207211

212+
public boolean isIgnored() {
213+
return ignored;
214+
}
208215

209216
}

core/src/main/java/feign/ReflectiveFeign.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,14 @@ public Map<String, MethodHandler> apply(Target key) {
162162
} else {
163163
buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder);
164164
}
165-
result.put(md.configKey(),
166-
factory.create(key, md, buildTemplate, options, decoder, errorDecoder));
165+
if (md.isIgnored()) {
166+
result.put(md.configKey(), args -> {
167+
throw new IllegalStateException(md.configKey() + " is not a method handled by feign");
168+
});
169+
} else {
170+
result.put(md.configKey(),
171+
factory.create(key, md, buildTemplate, options, decoder, errorDecoder));
172+
}
167173
}
168174
return result;
169175
}

core/src/main/java/feign/RequestTemplate.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,21 @@ public RequestTemplate header(String name, Iterable<String> values) {
675675
return appendHeader(name, values);
676676
}
677677

678+
/**
679+
* Clear on reader from {@link RequestTemplate}
680+
*
681+
* @param name of the header.
682+
* @return a RequestTemplate for chaining.
683+
*/
684+
public RequestTemplate removeHeader(String name) {
685+
if (name == null || name.isEmpty()) {
686+
throw new IllegalArgumentException("name is required.");
687+
}
688+
this.headers.remove(name);
689+
690+
return this;
691+
}
692+
678693
/**
679694
* Create a Header Template.
680695
*

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
<module>ribbon</module>
4242
<module>sax</module>
4343
<module>slf4j</module>
44+
<module>spring4</module>
4445
<module>soap</module>
4546
<module>reactive</module>
4647
<module>example-github</module>
@@ -229,6 +230,12 @@
229230
<version>${project.version}</version>
230231
</dependency>
231232

233+
<dependency>
234+
<groupId>${project.groupId}</groupId>
235+
<artifactId>feign-mock</artifactId>
236+
<version>${project.version}</version>
237+
</dependency>
238+
232239
<dependency>
233240
<groupId>${project.groupId}</groupId>
234241
<artifactId>feign-okhttp</artifactId>

spring4/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Feign Spring
2+
This module overrides OpenFeign/feign annotation processing to instead use standard ones supplied by the spring annotations specification.
3+
4+
5+
## Currently Supported Annotation Processing
6+
Feign only supports processing java interfaces (not abstract or concrete classes).
7+
8+
ISE is raised when any annotation's value is empty or null. Ex. `Path("")` raises an ISE.
9+
10+
Here are a list of behaviors currently supported.
11+
### Type Annotations
12+
#### `@RequestMapping`
13+
Appends the ```value``` to `Target.url()`. Can have tokens corresponding to `@PathVariable` annotations.
14+
The ```method``` sets the request method.
15+
The ```produces``` adds the first value as the `Accept` header.
16+
The ```consume``` adds the first value as the `Content-Type` header.
17+
### Method Annotations
18+
#### `@RequestMapping`
19+
Appends the value to `Target.url()`. Can have tokens corresponding to `@PathVariable` annotations.
20+
The method sets the request method.
21+
### Parameter Annotations
22+
#### `@PathVariable`
23+
Links the value of the corresponding parameter to a template variable declared in the path.
24+
#### `@RequestParam`
25+
Links the value of the corresponding parameter to a query parameter. When invoked, null will skip the query param.

spring4/pom.xml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
4+
Copyright 2012-2019 The Feign Authors
5+
6+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7+
in compliance with the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software distributed under the License
12+
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13+
or implied. See the License for the specific language governing permissions and limitations under
14+
the License.
15+
16+
-->
17+
<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">
18+
<modelVersion>4.0.0</modelVersion>
19+
20+
<parent>
21+
<groupId>io.github.openfeign</groupId>
22+
<artifactId>parent</artifactId>
23+
<version>10.4.1-SNAPSHOT</version>
24+
</parent>
25+
26+
<artifactId>feign-spring4</artifactId>
27+
<name>Feign spring</name>
28+
<description>Feign Contracts for Spring4</description>
29+
30+
<properties>
31+
<main.basedir>${project.basedir}/..</main.basedir>
32+
33+
<spring.version>4.3.6.RELEASE</spring.version>
34+
<hamcrest.version>1.3</hamcrest.version>
35+
</properties>
36+
37+
<dependencies>
38+
<dependency>
39+
<groupId>${project.groupId}</groupId>
40+
<artifactId>feign-core</artifactId>
41+
</dependency>
42+
<dependency>
43+
<groupId>org.springframework</groupId>
44+
<artifactId>spring-web</artifactId>
45+
<version>${spring.version}</version>
46+
</dependency>
47+
48+
<!-- testing -->
49+
<dependency>
50+
<groupId>${project.groupId}</groupId>
51+
<artifactId>feign-mock</artifactId>
52+
<scope>test</scope>
53+
</dependency>
54+
<dependency>
55+
<groupId>${project.groupId}</groupId>
56+
<artifactId>feign-jackson</artifactId>
57+
<scope>test</scope>
58+
</dependency>
59+
<dependency>
60+
<groupId>org.hamcrest</groupId>
61+
<artifactId>hamcrest-core</artifactId>
62+
<version>${hamcrest.version}</version>
63+
<scope>test</scope>
64+
</dependency>
65+
<dependency>
66+
<groupId>org.hamcrest</groupId>
67+
<artifactId>hamcrest-library</artifactId>
68+
<version>${hamcrest.version}</version>
69+
<scope>test</scope>
70+
</dependency>
71+
</dependencies>
72+
73+
</project>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Copyright 2012-2019 The Feign Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
package feign.spring;
15+
16+
import java.lang.annotation.Annotation;
17+
import java.lang.reflect.Method;
18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import org.springframework.web.bind.annotation.ExceptionHandler;
21+
import org.springframework.web.bind.annotation.PathVariable;
22+
import org.springframework.web.bind.annotation.RequestBody;
23+
import org.springframework.web.bind.annotation.RequestMapping;
24+
import org.springframework.web.bind.annotation.RequestParam;
25+
import org.springframework.web.bind.annotation.ResponseBody;
26+
import feign.Contract.BaseContract;
27+
import feign.DeclarativeContract;
28+
import feign.MethodMetadata;
29+
30+
public class SpringContract extends DeclarativeContract {
31+
32+
static final String ACCEPT = "Accept";
33+
static final String CONTENT_TYPE = "Content-Type";
34+
35+
public SpringContract() {
36+
registerClassAnnotation(RequestMapping.class, (requestMapping, data) -> {
37+
appendMappings(data, requestMapping.value());
38+
39+
if (requestMapping.method().length == 1)
40+
data.template().method(requestMapping.method()[0].name());
41+
42+
handleProducesAnnotation(data, requestMapping.produces());
43+
handleConsumesAnnotation(data, requestMapping.consumes());
44+
});
45+
46+
registerMethodAnnotation(RequestMapping.class, (requestMapping, data) -> {
47+
String[] mappings = requestMapping.value();
48+
appendMappings(data, mappings);
49+
50+
if (requestMapping.method().length == 1)
51+
data.template().method(requestMapping.method()[0].name());
52+
});
53+
54+
registerMethodAnnotation(ResponseBody.class, (body, data) -> {
55+
handleConsumesAnnotation(data, "application/json");
56+
});
57+
registerMethodAnnotation(ExceptionHandler.class, (ann, data) -> {
58+
data.ignoreMethod();
59+
});
60+
registerParameterAnnotation(PathVariable.class, (parameterAnnotation, data, paramIndex) -> {
61+
String name = PathVariable.class.cast(parameterAnnotation).value();
62+
nameParam(data, name, paramIndex);
63+
});
64+
65+
registerParameterAnnotation(RequestBody.class, (body, data, paramIndex) -> {
66+
handleProducesAnnotation(data, "application/json");
67+
});
68+
registerParameterAnnotation(RequestParam.class, (parameterAnnotation, data, paramIndex) -> {
69+
String name = RequestParam.class.cast(parameterAnnotation).value();
70+
Collection<String> query = addTemplatedParam(data.template().queries().get(name), name);
71+
data.template().query(name, query);
72+
nameParam(data, name, paramIndex);
73+
});
74+
75+
}
76+
77+
private void appendMappings(MethodMetadata data, String[] mappings) {
78+
for (int i = 0; i < mappings.length; i++) {
79+
String methodAnnotationValue = mappings[i];
80+
if (!methodAnnotationValue.startsWith("/") && !data.template().url().endsWith("/")) {
81+
methodAnnotationValue = "/" + methodAnnotationValue;
82+
}
83+
if (data.template().url().endsWith("/") && methodAnnotationValue.startsWith("/")) {
84+
methodAnnotationValue = methodAnnotationValue.substring(1);
85+
}
86+
87+
data.template().uri(data.template().url() + methodAnnotationValue);
88+
}
89+
}
90+
91+
private void handleProducesAnnotation(MethodMetadata data, String... produces) {
92+
if (produces.length == 0)
93+
return;
94+
data.template().removeHeader(ACCEPT); // remove any previous produces
95+
data.template().header(ACCEPT, produces[0]);
96+
}
97+
98+
private void handleConsumesAnnotation(MethodMetadata data, String... consumes) {
99+
if (consumes.length == 0)
100+
return;
101+
data.template().removeHeader(CONTENT_TYPE); // remove any previous consumes
102+
data.template().header(CONTENT_TYPE, consumes[0]);
103+
}
104+
105+
protected Collection<String> addTemplatedParam(Collection<String> possiblyNull, String name) {
106+
if (possiblyNull == null) {
107+
possiblyNull = new ArrayList<String>();
108+
}
109+
possiblyNull.add(String.format("{%s}", name));
110+
return possiblyNull;
111+
}
112+
113+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Copyright 2012-2019 The Feign Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
package feign.spring;
15+
16+
public class Data {
17+
18+
private String content;
19+
20+
public String getContent() {
21+
return content;
22+
}
23+
24+
public void setContent(String content) {
25+
this.content = content;
26+
}
27+
28+
}

0 commit comments

Comments
 (0)