Skip to content

Commit 00b04bc

Browse files
authored
Use bytecode transformation to reduce code duplication on jakarta modules (OpenFeign#2269)
Co-authored-by: Marvin Froeder <[email protected]>
1 parent 77759bb commit 00b04bc

13 files changed

Lines changed: 309 additions & 273 deletions

File tree

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ jobs:
9191
- run:
9292
name: 'Test'
9393
command: |
94-
./mvnw -ntp -B test
94+
./mvnw -ntp -B verify
9595
- verify-formatting
9696

9797
deploy:

jakarta/pom.xml

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -30,38 +30,21 @@
3030
<properties>
3131
<main.java.version>11</main.java.version>
3232
<main.basedir>${project.basedir}/..</main.basedir>
33-
</properties>
34-
35-
<dependencies>
36-
<dependency>
37-
<groupId>${project.groupId}</groupId>
38-
<artifactId>feign-core</artifactId>
39-
</dependency>
4033

41-
<dependency>
42-
<groupId>jakarta.ws.rs</groupId>
43-
<artifactId>jakarta.ws.rs-api</artifactId>
44-
<version>3.1.0</version>
45-
</dependency>
34+
<moditect.skip>true</moditect.skip>
35+
</properties>
4636

47-
<!-- for example -->
48-
<dependency>
37+
<distributionManagement>
38+
<relocation>
4939
<groupId>${project.groupId}</groupId>
50-
<artifactId>feign-gson</artifactId>
51-
<scope>test</scope>
52-
</dependency>
40+
<artifactId>feign-jaxrs3</artifactId>
41+
</relocation>
42+
</distributionManagement>
5343

44+
<dependencies>
5445
<dependency>
5546
<groupId>${project.groupId}</groupId>
56-
<artifactId>feign-core</artifactId>
57-
<type>test-jar</type>
58-
<scope>test</scope>
59-
</dependency>
60-
<dependency>
61-
<groupId>${project.groupId}</groupId>
62-
<artifactId>feign-jaxrs</artifactId>
63-
<type>test-jar</type>
64-
<scope>test</scope>
47+
<artifactId>feign-jaxrs3</artifactId>
6548
</dependency>
6649
</dependencies>
6750
</project>

jakarta/src/main/java/feign/jaxrs/JakartaContract.java

Lines changed: 5 additions & 224 deletions
Original file line numberDiff line numberDiff line change
@@ -13,228 +13,9 @@
1313
*/
1414
package feign.jaxrs;
1515

16-
import static feign.Util.checkState;
17-
import static feign.Util.emptyToNull;
18-
import static feign.Util.removeValues;
16+
import feign.jaxrs3.JAXRS3Contract;
1917

20-
import feign.DeclarativeContract;
21-
import feign.MethodMetadata;
22-
import feign.Request;
23-
import jakarta.ws.rs.*;
24-
import jakarta.ws.rs.container.Suspended;
25-
import jakarta.ws.rs.core.Context;
26-
import java.lang.annotation.Annotation;
27-
import java.lang.reflect.Field;
28-
import java.lang.reflect.Method;
29-
import java.util.Collections;
30-
31-
public class JakartaContract extends DeclarativeContract {
32-
33-
static final String ACCEPT = "Accept";
34-
static final String CONTENT_TYPE = "Content-Type";
35-
36-
// Protected so unittest can call us
37-
// XXX: Should parseAndValidateMetadata(Class, Method) be public instead? The old deprecated
38-
// parseAndValidateMetadata(Method) was public..
39-
@Override
40-
protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
41-
return super.parseAndValidateMetadata(targetType, method);
42-
}
43-
44-
public JakartaContract() {
45-
super.registerClassAnnotation(
46-
Path.class,
47-
(path, data) -> {
48-
if (path != null && !path.value().isEmpty()) {
49-
String pathValue = path.value();
50-
if (!pathValue.startsWith("/")) {
51-
pathValue = "/" + pathValue;
52-
}
53-
if (pathValue.endsWith("/")) {
54-
// Strip off any trailing slashes, since the template has already had slashes
55-
// appropriately
56-
// added
57-
pathValue = pathValue.substring(0, pathValue.length() - 1);
58-
}
59-
// jax-rs allows whitespace around the param name, as well as an optional regex. The
60-
// contract
61-
// should
62-
// strip these out appropriately.
63-
pathValue = pathValue.replaceAll("\\{\\s*(.+?)\\s*(:.+?)?\\}", "\\{$1\\}");
64-
data.template().uri(pathValue);
65-
}
66-
});
67-
super.registerClassAnnotation(Consumes.class, this::handleConsumesAnnotation);
68-
super.registerClassAnnotation(Produces.class, this::handleProducesAnnotation);
69-
70-
registerMethodAnnotation(
71-
methodAnnotation -> {
72-
final Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
73-
final HttpMethod http = annotationType.getAnnotation(HttpMethod.class);
74-
return http != null;
75-
},
76-
(methodAnnotation, data) -> {
77-
final Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
78-
final HttpMethod http = annotationType.getAnnotation(HttpMethod.class);
79-
checkState(
80-
data.template().method() == null,
81-
"Method %s contains multiple HTTP methods. Found: %s and %s",
82-
data.configKey(),
83-
data.template().method(),
84-
http.value());
85-
data.template().method(Request.HttpMethod.valueOf(http.value()));
86-
});
87-
88-
super.registerMethodAnnotation(
89-
Path.class,
90-
(path, data) -> {
91-
final String pathValue = emptyToNull(path.value());
92-
if (pathValue == null) {
93-
return;
94-
}
95-
String methodAnnotationValue = path.value();
96-
if (!methodAnnotationValue.startsWith("/") && !data.template().url().endsWith("/")) {
97-
methodAnnotationValue = "/" + methodAnnotationValue;
98-
}
99-
// jax-rs allows whitespace around the param name, as well as an optional regex. The
100-
// contract
101-
// should
102-
// strip these out appropriately.
103-
methodAnnotationValue =
104-
methodAnnotationValue.replaceAll("\\{\\s*(.+?)\\s*(:.+?)?\\}", "\\{$1\\}");
105-
data.template().uri(methodAnnotationValue, true);
106-
});
107-
super.registerMethodAnnotation(Consumes.class, this::handleConsumesAnnotation);
108-
super.registerMethodAnnotation(Produces.class, this::handleProducesAnnotation);
109-
110-
// parameter with unsupported jax-rs annotations should not be passed as body params.
111-
// this will prevent interfaces from becoming unusable entirely due to single (unsupported)
112-
// endpoints.
113-
// https://github.com/OpenFeign/feign/issues/669
114-
super.registerParameterAnnotation(Suspended.class, (ann, data, i) -> data.ignoreParamater(i));
115-
super.registerParameterAnnotation(Context.class, (ann, data, i) -> data.ignoreParamater(i));
116-
// trying to minimize the diff
117-
registerParamAnnotations();
118-
}
119-
120-
private void handleProducesAnnotation(Produces produces, MethodMetadata data) {
121-
final String[] serverProduces =
122-
removeValues(produces.value(), mediaType -> emptyToNull(mediaType) == null, String.class);
123-
checkState(serverProduces.length > 0, "Produces.value() was empty on %s", data.configKey());
124-
data.template().header(ACCEPT, Collections.emptyList()); // remove any previous produces
125-
data.template().header(ACCEPT, serverProduces);
126-
}
127-
128-
private void handleConsumesAnnotation(Consumes consumes, MethodMetadata data) {
129-
final String[] serverConsumes =
130-
removeValues(consumes.value(), mediaType -> emptyToNull(mediaType) == null, String.class);
131-
checkState(serverConsumes.length > 0, "Consumes.value() was empty on %s", data.configKey());
132-
data.template().header(CONTENT_TYPE, serverConsumes);
133-
}
134-
135-
protected void registerParamAnnotations() {
136-
137-
registerParameterAnnotation(
138-
PathParam.class,
139-
(param, data, paramIndex) -> {
140-
final String name = param.value();
141-
checkState(
142-
emptyToNull(name) != null, "PathParam.value() was empty on parameter %s", paramIndex);
143-
nameParam(data, name, paramIndex);
144-
});
145-
registerParameterAnnotation(
146-
QueryParam.class,
147-
(param, data, paramIndex) -> {
148-
final String name = param.value();
149-
checkState(
150-
emptyToNull(name) != null,
151-
"QueryParam.value() was empty on parameter %s",
152-
paramIndex);
153-
final String query = addTemplatedParam(name);
154-
data.template().query(name, query);
155-
nameParam(data, name, paramIndex);
156-
});
157-
registerParameterAnnotation(
158-
HeaderParam.class,
159-
(param, data, paramIndex) -> {
160-
final String name = param.value();
161-
checkState(
162-
emptyToNull(name) != null,
163-
"HeaderParam.value() was empty on parameter %s",
164-
paramIndex);
165-
final String header = addTemplatedParam(name);
166-
data.template().header(name, header);
167-
nameParam(data, name, paramIndex);
168-
});
169-
registerParameterAnnotation(
170-
FormParam.class,
171-
(param, data, paramIndex) -> {
172-
final String name = param.value();
173-
checkState(
174-
emptyToNull(name) != null, "FormParam.value() was empty on parameter %s", paramIndex);
175-
data.formParams().add(name);
176-
nameParam(data, name, paramIndex);
177-
});
178-
179-
// Reflect over the Bean Param looking for supported parameter annotations
180-
registerParameterAnnotation(
181-
BeanParam.class,
182-
(param, data, paramIndex) -> {
183-
final Field[] aggregatedParams =
184-
data.method().getParameters()[paramIndex].getType().getDeclaredFields();
185-
186-
for (Field aggregatedParam : aggregatedParams) {
187-
188-
if (aggregatedParam.isAnnotationPresent(PathParam.class)) {
189-
final String name = aggregatedParam.getAnnotation(PathParam.class).value();
190-
checkState(
191-
emptyToNull(name) != null,
192-
"BeanParam parameter %s contains PathParam with empty .value() on field %s",
193-
paramIndex,
194-
aggregatedParam.getName());
195-
nameParam(data, name, paramIndex);
196-
}
197-
198-
if (aggregatedParam.isAnnotationPresent(QueryParam.class)) {
199-
final String name = aggregatedParam.getAnnotation(QueryParam.class).value();
200-
checkState(
201-
emptyToNull(name) != null,
202-
"BeanParam parameter %s contains QueryParam with empty .value() on field %s",
203-
paramIndex,
204-
aggregatedParam.getName());
205-
final String query = addTemplatedParam(name);
206-
data.template().query(name, query);
207-
nameParam(data, name, paramIndex);
208-
}
209-
210-
if (aggregatedParam.isAnnotationPresent(HeaderParam.class)) {
211-
final String name = aggregatedParam.getAnnotation(HeaderParam.class).value();
212-
checkState(
213-
emptyToNull(name) != null,
214-
"BeanParam parameter %s contains HeaderParam with empty .value() on field %s",
215-
paramIndex,
216-
aggregatedParam.getName());
217-
final String header = addTemplatedParam(name);
218-
data.template().header(name, header);
219-
nameParam(data, name, paramIndex);
220-
}
221-
222-
if (aggregatedParam.isAnnotationPresent(FormParam.class)) {
223-
final String name = aggregatedParam.getAnnotation(FormParam.class).value();
224-
checkState(
225-
emptyToNull(name) != null,
226-
"BeanParam parameter %s contains FormParam with empty .value() on field %s",
227-
paramIndex,
228-
aggregatedParam.getName());
229-
data.formParams().add(name);
230-
nameParam(data, name, paramIndex);
231-
}
232-
}
233-
});
234-
}
235-
236-
// Not using override as the super-type's method is deprecated and will be removed.
237-
private String addTemplatedParam(String name) {
238-
return String.format("{%s}", name);
239-
}
240-
}
18+
/**
19+
* @deprecated use {@link JAXRS3Contract} instead
20+
*/
21+
public class JakartaContract extends JAXRS3Contract {}

jaxrs/pom.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,23 @@
7070
</execution>
7171
</executions>
7272
</plugin>
73+
<plugin>
74+
<groupId>org.eclipse.transformer</groupId>
75+
<artifactId>org.eclipse.transformer.maven</artifactId>
76+
<version>0.2.0</version>
77+
<executions>
78+
<execution>
79+
<id>jakarta-ee</id>
80+
<phase>package</phase>
81+
<goals>
82+
<goal>run</goal>
83+
</goals>
84+
<configuration>
85+
<classifier>jakarta</classifier>
86+
</configuration>
87+
</execution>
88+
</executions>
89+
</plugin>
7390
</plugins>
7491
</build>
7592
</project>

jaxrs2/pom.xml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,26 @@
101101
<scope>test</scope>
102102
</dependency>
103103
</dependencies>
104+
105+
<build>
106+
<plugins>
107+
<plugin>
108+
<groupId>org.eclipse.transformer</groupId>
109+
<artifactId>org.eclipse.transformer.maven</artifactId>
110+
<version>0.2.0</version>
111+
<executions>
112+
<execution>
113+
<id>jakarta-ee</id>
114+
<phase>package</phase>
115+
<goals>
116+
<goal>run</goal>
117+
</goals>
118+
<configuration>
119+
<classifier>jakarta</classifier>
120+
</configuration>
121+
</execution>
122+
</executions>
123+
</plugin>
124+
</plugins>
125+
</build>
104126
</project>

jaxrs2/src/main/java/feign/jaxrs2/JAXRS2Contract.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
* Please refer to the <a href="https://github.com/Netflix/feign/tree/master/feign-jaxrs2">Feign
2727
* JAX-RS 2 README</a>.
2828
*/
29-
public final class JAXRS2Contract extends JAXRSContract {
29+
public class JAXRS2Contract extends JAXRSContract {
3030

3131
public JAXRS2Contract() {
3232
// parameter with unsupported jax-rs annotations should not be passed as body params.

jaxrs3/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Feign Jakarta
2+
This module overrides annotation processing to instead use standard ones supplied by the Jakarta specification. This is currently targeted at the 3.1 spec.
3+
4+
## Limitations
5+
While it may appear possible to reuse the same interface across client and server, bear in mind that Jakarta resource
6+
annotations were not designed to be processed by clients. Finally, Jakarta is a large spec and attempts to implement
7+
it completely would be a project larger than feign itself. In other words, this implementation is *best efforts* and
8+
concedes far from 100% compatibility with server interface behavior.
9+
10+
## Currently Supported Annotation Processing
11+
Feign only supports processing java interfaces (not abstract or concrete classes).
12+
13+
Here are a list of behaviors currently supported.
14+
### Type Annotations
15+
#### `@Path`
16+
Appends the value to `Target.url()`. Can have tokens corresponding to `@PathParam` annotations.
17+
### Method Annotations
18+
#### `@HttpMethod` meta-annotation (present on `@GET`, `@POST`, etc.)
19+
Sets the request method.
20+
#### `@Path`
21+
Appends the value to `Target.url()`. Can have tokens corresponding to `@PathParam` annotations.
22+
#### `@Produces`
23+
Adds all values into the `Accept` header.
24+
#### `@Consumes`
25+
Adds the first value as the `Content-Type` header.
26+
### Parameter Annotations
27+
#### `@PathParam`
28+
Links the value of the corresponding parameter to a template variable declared in the path.
29+
#### `@QueryParam`
30+
Links the value of the corresponding parameter to a query parameter. When invoked, null will skip the query param.
31+
#### `@HeaderParam`
32+
Links the value of the corresponding parameter to a header.
33+
#### `@FormParam`
34+
Links the value of the corresponding parameter to a key passed to `Encoder.Text<Map<String, Object>>.encode()`.
35+
#### `@BeanParm`
36+
Aggregates the above supported parameter annotations under a single value object.

0 commit comments

Comments
 (0)