Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Feign is a declarative HTTP client library with a modular design:
- `spring/` - Spring MVC annotation support
- `hystrix/` - Circuit breaker integration
- `micrometer/`, `dropwizard-metrics4/5/` - Metrics integration
- `validation/`, `validation-jakarta/` - JSR-303 / Jakarta Bean Validation via the `MethodInterceptor` extension point
- `http-cache/` - Conditional revalidation (`ETag` / `Last-Modified` / `304 Not Modified`) via the `MethodInterceptor` extension point

### Key Design Patterns
- **Builder Pattern**: `Feign.builder()` for configuring clients
Expand Down
12 changes: 12 additions & 0 deletions core/src/main/java/feign/AsyncFeign.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import feign.interceptor.MethodInterceptor;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
Expand Down Expand Up @@ -187,6 +188,16 @@ public AsyncBuilder<C> requestInterceptors(Iterable<RequestInterceptor> requestI
return super.requestInterceptors(requestInterceptors);
}

@Override
public AsyncBuilder<C> methodInterceptor(MethodInterceptor methodInterceptor) {
return super.methodInterceptor(methodInterceptor);
}

@Override
public AsyncBuilder<C> methodInterceptors(Iterable<MethodInterceptor> methodInterceptors) {
return super.methodInterceptors(methodInterceptors);
}

@Override
public AsyncBuilder<C> invocationHandlerFactory(
InvocationHandlerFactory invocationHandlerFactory) {
Expand Down Expand Up @@ -215,6 +226,7 @@ public AsyncFeign<C> internalBuild() {
client,
retryer,
requestInterceptors,
methodInterceptors,
responseHandler,
logger,
logLevel,
Expand Down
60 changes: 43 additions & 17 deletions core/src/main/java/feign/AsynchronousMethodHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

import feign.InvocationHandlerFactory.MethodHandler;
import feign.Request.Options;
import feign.interceptor.Invocation;
import feign.interceptor.MethodInterceptor;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -56,23 +58,39 @@ private AsynchronousMethodHandler(
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = methodHandlerConfiguration.getBuildTemplateFromArgs().create(argv);
Options options = findOptions(argv);
Retryer retryer = this.methodHandlerConfiguration.getRetryer().clone();
try {
if (methodInfo.isAsyncReturnType()) {
return executeAndDecode(template, options, retryer);
} else {
return executeAndDecode(template, options, retryer).join();
}
} catch (CompletionException e) {
throw e.getCause();
}
Invocation invocation =
new Invocation(
methodHandlerConfiguration.getTarget(),
methodHandlerConfiguration.getMetadata(),
template,
argv);

MethodInterceptor.Chain endOfChain =
inv -> {
Retryer retryer = this.methodHandlerConfiguration.getRetryer().clone();
try {
if (methodInfo.isAsyncReturnType()) {
return executeAndDecode(inv, options, retryer);
} else {
return executeAndDecode(inv, options, retryer).join();
}
} catch (CompletionException e) {
throw e.getCause();
}
};
MethodInterceptor.Chain chain =
methodHandlerConfiguration.getMethodInterceptors().stream()
.reduce(MethodInterceptor::andThen)
.map(interceptor -> interceptor.apply(endOfChain))
.orElse(endOfChain);
return chain.next(invocation);
}

private CompletableFuture<Object> executeAndDecode(
RequestTemplate template, Options options, Retryer retryer) {
Invocation invocation, Options options, Retryer retryer) {
CancellableFuture<Object> resultFuture = new CancellableFuture<>();

executeAndDecode(template, options)
executeAndDecode(invocation, options)
.whenComplete(
(response, throwable) -> {
if (throwable != null) {
Expand All @@ -85,7 +103,7 @@ private CompletableFuture<Object> executeAndDecode(
methodHandlerConfiguration.getLogLevel());
}

resultFuture.setInner(executeAndDecode(template, options, retryer));
resultFuture.setInner(executeAndDecode(invocation, options, retryer));
}
} else {
resultFuture.complete(response);
Expand Down Expand Up @@ -154,7 +172,8 @@ private boolean shouldRetry(
}
}

private CompletableFuture<Object> executeAndDecode(RequestTemplate template, Options options) {
private CompletableFuture<Object> executeAndDecode(Invocation invocation, Options options) {
RequestTemplate template = invocation.requestTemplate();
Request request = targetRequest(template);

if (methodHandlerConfiguration.getLogLevel() != Logger.Level.NONE) {
Expand All @@ -170,9 +189,12 @@ private CompletableFuture<Object> executeAndDecode(RequestTemplate template, Opt
return client
.execute(request, options, Optional.ofNullable(requestContext))
.thenApply(
response ->
// TODO: remove in Feign 12
ensureRequestIsSet(response, template, request))
response -> {
// TODO: remove in Feign 12
Response withRequest = ensureRequestIsSet(response, template, request);
invocation.response(withRequest);
return withRequest;
})
.exceptionally(
throwable -> {
CompletionException completionException =
Expand Down Expand Up @@ -237,6 +259,7 @@ static class Factory<C> implements MethodHandler.Factory<C> {
private final AsyncClient<C> client;
private final Retryer retryer;
private final List<RequestInterceptor> requestInterceptors;
private final List<MethodInterceptor> methodInterceptors;
private final AsyncResponseHandler responseHandler;
private final Logger logger;
private final Logger.Level logLevel;
Expand All @@ -249,6 +272,7 @@ static class Factory<C> implements MethodHandler.Factory<C> {
AsyncClient<C> client,
Retryer retryer,
List<RequestInterceptor> requestInterceptors,
List<MethodInterceptor> methodInterceptors,
AsyncResponseHandler responseHandler,
Logger logger,
Logger.Level logLevel,
Expand All @@ -259,6 +283,7 @@ static class Factory<C> implements MethodHandler.Factory<C> {
this.client = checkNotNull(client, "client");
this.retryer = checkNotNull(retryer, "retryer");
this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors");
this.methodInterceptors = checkNotNull(methodInterceptors, "methodInterceptors");
this.responseHandler = responseHandler;
this.logger = checkNotNull(logger, "logger");
this.logLevel = checkNotNull(logLevel, "logLevel");
Expand All @@ -280,6 +305,7 @@ public MethodHandler create(Target<?> target, MethodMetadata metadata, C request
target,
retryer,
requestInterceptors,
methodInterceptors,
logger,
logLevel,
buildTemplateFromArgs,
Expand Down
40 changes: 40 additions & 0 deletions core/src/main/java/feign/BaseBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import feign.codec.DefaultErrorDecoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import feign.interceptor.MethodInterceptor;
import feign.interceptor.MethodInterceptors;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
Expand All @@ -42,6 +44,7 @@ public abstract class BaseBuilder<B extends BaseBuilder<B, T>, T> implements Clo

protected List<RequestInterceptor> requestInterceptors = new ArrayList<>();
protected List<ResponseInterceptor> responseInterceptors = new ArrayList<>();
protected List<MethodInterceptor> methodInterceptors = new ArrayList<>();
protected Logger.Level logLevel = Logger.Level.NONE;
protected Contract contract = new DefaultContract();
protected Retryer retryer = new DefaultRetryer();
Expand Down Expand Up @@ -219,6 +222,27 @@ public B responseInterceptor(ResponseInterceptor responseInterceptor) {
return thisB;
}

/**
* Adds a single {@link MethodInterceptor} to the builder. Method interceptors run after contract
* resolution and wrap the entire HTTP exchange (request interceptors, HTTP execution, response
* interceptors, decoding). They have access to raw method arguments via {@link Invocation}.
*/
@Experimental
public B methodInterceptor(MethodInterceptor methodInterceptor) {
this.methodInterceptors.add(methodInterceptor);
return thisB;
}

/** Sets the full set of method interceptors, overwriting any previously configured. */
@Experimental
public B methodInterceptors(Iterable<MethodInterceptor> methodInterceptors) {
this.methodInterceptors.clear();
for (MethodInterceptor methodInterceptor : methodInterceptors) {
this.methodInterceptors.add(methodInterceptor);
}
return thisB;
}

/** Allows you to override how reflective dispatch works inside of Feign. */
public B invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
this.invocationHandlerFactory = invocationHandlerFactory;
Expand Down Expand Up @@ -305,6 +329,21 @@ B enrich() {
capabilities);
clone.responseInterceptors = responseInterceptors.interceptors();

// enrich each method interceptor, then enrich the list as a whole
MethodInterceptor[] methodArray = clone.methodInterceptors.toArray(new MethodInterceptor[0]);
for (int i = 0; i < methodArray.length; i++) {
methodArray[i] =
(MethodInterceptor)
Capability.enrich(methodArray[i], MethodInterceptor.class, capabilities);
}
MethodInterceptors methodInterceptors =
(MethodInterceptors)
Capability.enrich(
new MethodInterceptors(Arrays.asList(methodArray)),
MethodInterceptors.class,
capabilities);
clone.methodInterceptors = methodInterceptors.interceptors();

return clone;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
Expand All @@ -322,6 +361,7 @@ List<Field> getFieldsToEnrich() {
// interceptor lists are enriched per-element then as a whole via custom types
.filter(field -> !Objects.equals(field.getName(), "requestInterceptors"))
.filter(field -> !Objects.equals(field.getName(), "responseInterceptors"))
.filter(field -> !Objects.equals(field.getName(), "methodInterceptors"))
// skip primitive types
.filter(field -> !field.getType().isPrimitive())
// skip enumerations
Expand Down
12 changes: 12 additions & 0 deletions core/src/main/java/feign/Feign.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import feign.interceptor.MethodInterceptor;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
Expand Down Expand Up @@ -169,6 +170,16 @@ public Builder requestInterceptors(Iterable<RequestInterceptor> requestIntercept
return super.requestInterceptors(requestInterceptors);
}

@Override
public Builder methodInterceptor(MethodInterceptor methodInterceptor) {
return super.methodInterceptor(methodInterceptor);
}

@Override
public Builder methodInterceptors(Iterable<MethodInterceptor> methodInterceptors) {
return super.methodInterceptors(methodInterceptors);
}

@Override
public Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
return super.invocationHandlerFactory(invocationHandlerFactory);
Expand Down Expand Up @@ -219,6 +230,7 @@ public Feign internalBuild() {
client,
retryer,
requestInterceptors,
methodInterceptors,
responseHandler,
logger,
logLevel,
Expand Down
33 changes: 33 additions & 0 deletions core/src/main/java/feign/MethodHandlerConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import static feign.Util.checkNotNull;

import feign.interceptor.MethodInterceptor;
import java.util.Collections;
import java.util.List;

public class MethodHandlerConfiguration {
Expand All @@ -29,6 +31,8 @@ public class MethodHandlerConfiguration {

private final List<RequestInterceptor> requestInterceptors;

private final List<MethodInterceptor> methodInterceptors;

private final Logger logger;

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

public List<MethodInterceptor> getMethodInterceptors() {
return methodInterceptors;
}

public Logger getLogger() {
return logger;
}
Expand Down Expand Up @@ -85,10 +93,35 @@ public MethodHandlerConfiguration(
RequestTemplate.Factory buildTemplateFromArgs,
Request.Options options,
ExceptionPropagationPolicy propagationPolicy) {
this(
metadata,
target,
retryer,
requestInterceptors,
Collections.emptyList(),
logger,
logLevel,
buildTemplateFromArgs,
options,
propagationPolicy);
}

public MethodHandlerConfiguration(
MethodMetadata metadata,
Target<?> target,
Retryer retryer,
List<RequestInterceptor> requestInterceptors,
List<MethodInterceptor> methodInterceptors,
Logger logger,
Logger.Level logLevel,
RequestTemplate.Factory buildTemplateFromArgs,
Request.Options options,
ExceptionPropagationPolicy propagationPolicy) {
this.target = checkNotNull(target, "target");
this.retryer = checkNotNull(retryer, "retryer for %s", target);
this.requestInterceptors =
checkNotNull(requestInterceptors, "requestInterceptors for %s", target);
this.methodInterceptors = checkNotNull(methodInterceptors, "methodInterceptors for %s", target);
this.logger = checkNotNull(logger, "logger for %s", target);
this.logLevel = checkNotNull(logLevel, "logLevel for %s", target);
this.metadata = checkNotNull(metadata, "metadata for %s", target);
Expand Down
Loading