Skip to content

Commit 5345da5

Browse files
authored
Merge pull request #3346 from OpenFeign/method-interceptor
Add MethodInterceptor extension point with feign-validation and feign-http-cache modules
2 parents 19585f1 + eb0daad commit 5345da5

28 files changed

Lines changed: 1903 additions & 19 deletions

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ Feign is a declarative HTTP client library with a modular design:
4242
- `spring/` - Spring MVC annotation support
4343
- `hystrix/` - Circuit breaker integration
4444
- `micrometer/`, `dropwizard-metrics4/5/` - Metrics integration
45+
- `validation/`, `validation-jakarta/` - JSR-303 / Jakarta Bean Validation via the `MethodInterceptor` extension point
46+
- `http-cache/` - Conditional revalidation (`ETag` / `Last-Modified` / `304 Not Modified`) via the `MethodInterceptor` extension point
4547

4648
### Key Design Patterns
4749
- **Builder Pattern**: `Feign.builder()` for configuring clients

core/src/main/java/feign/AsyncFeign.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import feign.codec.Decoder;
2323
import feign.codec.Encoder;
2424
import feign.codec.ErrorDecoder;
25+
import feign.interceptor.MethodInterceptor;
2526
import java.util.concurrent.CompletableFuture;
2627
import java.util.concurrent.ExecutorService;
2728
import java.util.concurrent.Executors;
@@ -187,6 +188,16 @@ public AsyncBuilder<C> requestInterceptors(Iterable<RequestInterceptor> requestI
187188
return super.requestInterceptors(requestInterceptors);
188189
}
189190

191+
@Override
192+
public AsyncBuilder<C> methodInterceptor(MethodInterceptor methodInterceptor) {
193+
return super.methodInterceptor(methodInterceptor);
194+
}
195+
196+
@Override
197+
public AsyncBuilder<C> methodInterceptors(Iterable<MethodInterceptor> methodInterceptors) {
198+
return super.methodInterceptors(methodInterceptors);
199+
}
200+
190201
@Override
191202
public AsyncBuilder<C> invocationHandlerFactory(
192203
InvocationHandlerFactory invocationHandlerFactory) {
@@ -215,6 +226,7 @@ public AsyncFeign<C> internalBuild() {
215226
client,
216227
retryer,
217228
requestInterceptors,
229+
methodInterceptors,
218230
responseHandler,
219231
logger,
220232
logLevel,

core/src/main/java/feign/AsynchronousMethodHandler.java

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
import feign.InvocationHandlerFactory.MethodHandler;
2323
import feign.Request.Options;
24+
import feign.interceptor.Invocation;
25+
import feign.interceptor.MethodInterceptor;
2426
import java.io.IOException;
2527
import java.util.List;
2628
import java.util.Optional;
@@ -56,23 +58,39 @@ private AsynchronousMethodHandler(
5658
public Object invoke(Object[] argv) throws Throwable {
5759
RequestTemplate template = methodHandlerConfiguration.getBuildTemplateFromArgs().create(argv);
5860
Options options = findOptions(argv);
59-
Retryer retryer = this.methodHandlerConfiguration.getRetryer().clone();
60-
try {
61-
if (methodInfo.isAsyncReturnType()) {
62-
return executeAndDecode(template, options, retryer);
63-
} else {
64-
return executeAndDecode(template, options, retryer).join();
65-
}
66-
} catch (CompletionException e) {
67-
throw e.getCause();
68-
}
61+
Invocation invocation =
62+
new Invocation(
63+
methodHandlerConfiguration.getTarget(),
64+
methodHandlerConfiguration.getMetadata(),
65+
template,
66+
argv);
67+
68+
MethodInterceptor.Chain endOfChain =
69+
inv -> {
70+
Retryer retryer = this.methodHandlerConfiguration.getRetryer().clone();
71+
try {
72+
if (methodInfo.isAsyncReturnType()) {
73+
return executeAndDecode(inv, options, retryer);
74+
} else {
75+
return executeAndDecode(inv, options, retryer).join();
76+
}
77+
} catch (CompletionException e) {
78+
throw e.getCause();
79+
}
80+
};
81+
MethodInterceptor.Chain chain =
82+
methodHandlerConfiguration.getMethodInterceptors().stream()
83+
.reduce(MethodInterceptor::andThen)
84+
.map(interceptor -> interceptor.apply(endOfChain))
85+
.orElse(endOfChain);
86+
return chain.next(invocation);
6987
}
7088

7189
private CompletableFuture<Object> executeAndDecode(
72-
RequestTemplate template, Options options, Retryer retryer) {
90+
Invocation invocation, Options options, Retryer retryer) {
7391
CancellableFuture<Object> resultFuture = new CancellableFuture<>();
7492

75-
executeAndDecode(template, options)
93+
executeAndDecode(invocation, options)
7694
.whenComplete(
7795
(response, throwable) -> {
7896
if (throwable != null) {
@@ -85,7 +103,7 @@ private CompletableFuture<Object> executeAndDecode(
85103
methodHandlerConfiguration.getLogLevel());
86104
}
87105

88-
resultFuture.setInner(executeAndDecode(template, options, retryer));
106+
resultFuture.setInner(executeAndDecode(invocation, options, retryer));
89107
}
90108
} else {
91109
resultFuture.complete(response);
@@ -154,7 +172,8 @@ private boolean shouldRetry(
154172
}
155173
}
156174

157-
private CompletableFuture<Object> executeAndDecode(RequestTemplate template, Options options) {
175+
private CompletableFuture<Object> executeAndDecode(Invocation invocation, Options options) {
176+
RequestTemplate template = invocation.requestTemplate();
158177
Request request = targetRequest(template);
159178

160179
if (methodHandlerConfiguration.getLogLevel() != Logger.Level.NONE) {
@@ -170,9 +189,12 @@ private CompletableFuture<Object> executeAndDecode(RequestTemplate template, Opt
170189
return client
171190
.execute(request, options, Optional.ofNullable(requestContext))
172191
.thenApply(
173-
response ->
174-
// TODO: remove in Feign 12
175-
ensureRequestIsSet(response, template, request))
192+
response -> {
193+
// TODO: remove in Feign 12
194+
Response withRequest = ensureRequestIsSet(response, template, request);
195+
invocation.response(withRequest);
196+
return withRequest;
197+
})
176198
.exceptionally(
177199
throwable -> {
178200
CompletionException completionException =
@@ -237,6 +259,7 @@ static class Factory<C> implements MethodHandler.Factory<C> {
237259
private final AsyncClient<C> client;
238260
private final Retryer retryer;
239261
private final List<RequestInterceptor> requestInterceptors;
262+
private final List<MethodInterceptor> methodInterceptors;
240263
private final AsyncResponseHandler responseHandler;
241264
private final Logger logger;
242265
private final Logger.Level logLevel;
@@ -249,6 +272,7 @@ static class Factory<C> implements MethodHandler.Factory<C> {
249272
AsyncClient<C> client,
250273
Retryer retryer,
251274
List<RequestInterceptor> requestInterceptors,
275+
List<MethodInterceptor> methodInterceptors,
252276
AsyncResponseHandler responseHandler,
253277
Logger logger,
254278
Logger.Level logLevel,
@@ -259,6 +283,7 @@ static class Factory<C> implements MethodHandler.Factory<C> {
259283
this.client = checkNotNull(client, "client");
260284
this.retryer = checkNotNull(retryer, "retryer");
261285
this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors");
286+
this.methodInterceptors = checkNotNull(methodInterceptors, "methodInterceptors");
262287
this.responseHandler = responseHandler;
263288
this.logger = checkNotNull(logger, "logger");
264289
this.logLevel = checkNotNull(logLevel, "logLevel");
@@ -280,6 +305,7 @@ public MethodHandler create(Target<?> target, MethodMetadata metadata, C request
280305
target,
281306
retryer,
282307
requestInterceptors,
308+
methodInterceptors,
283309
logger,
284310
logLevel,
285311
buildTemplateFromArgs,

core/src/main/java/feign/BaseBuilder.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import feign.codec.DefaultErrorDecoder;
2828
import feign.codec.Encoder;
2929
import feign.codec.ErrorDecoder;
30+
import feign.interceptor.MethodInterceptor;
31+
import feign.interceptor.MethodInterceptors;
3032
import java.lang.reflect.Field;
3133
import java.lang.reflect.ParameterizedType;
3234
import java.lang.reflect.Type;
@@ -42,6 +44,7 @@ public abstract class BaseBuilder<B extends BaseBuilder<B, T>, T> implements Clo
4244

4345
protected List<RequestInterceptor> requestInterceptors = new ArrayList<>();
4446
protected List<ResponseInterceptor> responseInterceptors = new ArrayList<>();
47+
protected List<MethodInterceptor> methodInterceptors = new ArrayList<>();
4548
protected Logger.Level logLevel = Logger.Level.NONE;
4649
protected Contract contract = new DefaultContract();
4750
protected Retryer retryer = new DefaultRetryer();
@@ -219,6 +222,27 @@ public B responseInterceptor(ResponseInterceptor responseInterceptor) {
219222
return thisB;
220223
}
221224

225+
/**
226+
* Adds a single {@link MethodInterceptor} to the builder. Method interceptors run after contract
227+
* resolution and wrap the entire HTTP exchange (request interceptors, HTTP execution, response
228+
* interceptors, decoding). They have access to raw method arguments via {@link Invocation}.
229+
*/
230+
@Experimental
231+
public B methodInterceptor(MethodInterceptor methodInterceptor) {
232+
this.methodInterceptors.add(methodInterceptor);
233+
return thisB;
234+
}
235+
236+
/** Sets the full set of method interceptors, overwriting any previously configured. */
237+
@Experimental
238+
public B methodInterceptors(Iterable<MethodInterceptor> methodInterceptors) {
239+
this.methodInterceptors.clear();
240+
for (MethodInterceptor methodInterceptor : methodInterceptors) {
241+
this.methodInterceptors.add(methodInterceptor);
242+
}
243+
return thisB;
244+
}
245+
222246
/** Allows you to override how reflective dispatch works inside of Feign. */
223247
public B invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
224248
this.invocationHandlerFactory = invocationHandlerFactory;
@@ -305,6 +329,21 @@ B enrich() {
305329
capabilities);
306330
clone.responseInterceptors = responseInterceptors.interceptors();
307331

332+
// enrich each method interceptor, then enrich the list as a whole
333+
MethodInterceptor[] methodArray = clone.methodInterceptors.toArray(new MethodInterceptor[0]);
334+
for (int i = 0; i < methodArray.length; i++) {
335+
methodArray[i] =
336+
(MethodInterceptor)
337+
Capability.enrich(methodArray[i], MethodInterceptor.class, capabilities);
338+
}
339+
MethodInterceptors methodInterceptors =
340+
(MethodInterceptors)
341+
Capability.enrich(
342+
new MethodInterceptors(Arrays.asList(methodArray)),
343+
MethodInterceptors.class,
344+
capabilities);
345+
clone.methodInterceptors = methodInterceptors.interceptors();
346+
308347
return clone;
309348
} catch (CloneNotSupportedException e) {
310349
throw new AssertionError(e);
@@ -322,6 +361,7 @@ List<Field> getFieldsToEnrich() {
322361
// interceptor lists are enriched per-element then as a whole via custom types
323362
.filter(field -> !Objects.equals(field.getName(), "requestInterceptors"))
324363
.filter(field -> !Objects.equals(field.getName(), "responseInterceptors"))
364+
.filter(field -> !Objects.equals(field.getName(), "methodInterceptors"))
325365
// skip primitive types
326366
.filter(field -> !field.getType().isPrimitive())
327367
// skip enumerations

core/src/main/java/feign/Feign.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import feign.codec.Decoder;
2222
import feign.codec.Encoder;
2323
import feign.codec.ErrorDecoder;
24+
import feign.interceptor.MethodInterceptor;
2425
import java.io.IOException;
2526
import java.lang.reflect.Method;
2627
import java.lang.reflect.Type;
@@ -169,6 +170,16 @@ public Builder requestInterceptors(Iterable<RequestInterceptor> requestIntercept
169170
return super.requestInterceptors(requestInterceptors);
170171
}
171172

173+
@Override
174+
public Builder methodInterceptor(MethodInterceptor methodInterceptor) {
175+
return super.methodInterceptor(methodInterceptor);
176+
}
177+
178+
@Override
179+
public Builder methodInterceptors(Iterable<MethodInterceptor> methodInterceptors) {
180+
return super.methodInterceptors(methodInterceptors);
181+
}
182+
172183
@Override
173184
public Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
174185
return super.invocationHandlerFactory(invocationHandlerFactory);
@@ -219,6 +230,7 @@ public Feign internalBuild() {
219230
client,
220231
retryer,
221232
requestInterceptors,
233+
methodInterceptors,
222234
responseHandler,
223235
logger,
224236
logLevel,

core/src/main/java/feign/MethodHandlerConfiguration.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import static feign.Util.checkNotNull;
1919

20+
import feign.interceptor.MethodInterceptor;
21+
import java.util.Collections;
2022
import java.util.List;
2123

2224
public class MethodHandlerConfiguration {
@@ -29,6 +31,8 @@ public class MethodHandlerConfiguration {
2931

3032
private final List<RequestInterceptor> requestInterceptors;
3133

34+
private final List<MethodInterceptor> methodInterceptors;
35+
3236
private final Logger logger;
3337

3438
private final Logger.Level logLevel;
@@ -55,6 +59,10 @@ public List<RequestInterceptor> getRequestInterceptors() {
5559
return requestInterceptors;
5660
}
5761

62+
public List<MethodInterceptor> getMethodInterceptors() {
63+
return methodInterceptors;
64+
}
65+
5866
public Logger getLogger() {
5967
return logger;
6068
}
@@ -85,10 +93,35 @@ public MethodHandlerConfiguration(
8593
RequestTemplate.Factory buildTemplateFromArgs,
8694
Request.Options options,
8795
ExceptionPropagationPolicy propagationPolicy) {
96+
this(
97+
metadata,
98+
target,
99+
retryer,
100+
requestInterceptors,
101+
Collections.emptyList(),
102+
logger,
103+
logLevel,
104+
buildTemplateFromArgs,
105+
options,
106+
propagationPolicy);
107+
}
108+
109+
public MethodHandlerConfiguration(
110+
MethodMetadata metadata,
111+
Target<?> target,
112+
Retryer retryer,
113+
List<RequestInterceptor> requestInterceptors,
114+
List<MethodInterceptor> methodInterceptors,
115+
Logger logger,
116+
Logger.Level logLevel,
117+
RequestTemplate.Factory buildTemplateFromArgs,
118+
Request.Options options,
119+
ExceptionPropagationPolicy propagationPolicy) {
88120
this.target = checkNotNull(target, "target");
89121
this.retryer = checkNotNull(retryer, "retryer for %s", target);
90122
this.requestInterceptors =
91123
checkNotNull(requestInterceptors, "requestInterceptors for %s", target);
124+
this.methodInterceptors = checkNotNull(methodInterceptors, "methodInterceptors for %s", target);
92125
this.logger = checkNotNull(logger, "logger for %s", target);
93126
this.logLevel = checkNotNull(logLevel, "logLevel for %s", target);
94127
this.metadata = checkNotNull(metadata, "metadata for %s", target);

0 commit comments

Comments
 (0)