Mastering OpenFeign: From a Simple Demo to Full Spring Cloud Integration
This article walks through a complete OpenFeign tutorial, starting with a basic demo that fetches GitHub contributors and creates issues, then dives into Feign's internal configuration, dynamic proxy generation, annotation parsing, request handling, logging, client execution, error decoding, and finally explains how Spring Cloud OpenFeign initializes and extends these capabilities with Hystrix and Sentinel support.
0. Intro Demo
This code is a simple OpenFeign example that retrieves all contributors of a GitHub repository and creates an issue, recommended for initial debugging and source code exploration.
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
@RequestLine("POST /repos/{owner}/{repo}/issues")
void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);
}
public static class Contributor {
String login;
int contributions;
}
public static class Issue {
String title;
String body;
List<String> assignees;
int milestone;
List<String> labels;
}
public class MyApp {
public static void main(String... args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
List<Contributor> contributors = github.contributors("OpenFeign", "feign");
for (Contributor contributor : contributors) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
}
}Feign.build Dependency Injection
public Feign build() {
SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
logLevel, decode404, closeAfterDecode, propagationPolicy);
ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}JDK Dynamic Proxy Generation
Dynamic Proxy Interface Object
public class ReflectiveFeign extends Feign {
@Override
public <T> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);
return proxy;
}
}Parsing Interface Method Annotations
To parse the annotations of GitHub.contributors, Feign provides a Contract implementation.
Default Parsing Logic
class Default extends Contract.BaseContract {
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
if (annotationType == RequestLine.class) {
// @RequestLine handling logic
} else if (annotationType == Body.class) {
// @Body handling logic
} else if (annotationType == Headers.class) {
// @Headers handling logic
}
}
protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
boolean isHttpAnnotation = false;
for (Annotation annotation : annotations) {
Class<? extends Annotation> annotationType = annotation.annotationType();
if (annotationType == Param.class) {
// @Param handling logic
} else if (annotationType == QueryMap.class) {
// @QueryMap handling logic
} else if (annotationType == HeaderMap.class) {
// @HeaderMap handling logic
}
}
return isHttpAnnotation;
}
}Native Common Annotations
@RequestLine– Method @Param – Parameter @Headers – Method, Type @QueryMap – Parameter @HeaderMap – Parameter @Body – Method
Spring MVC Extension Annotations
SpringMvcContract adds support for Spring MVC annotations in spring-cloud-open-feign.
public class SpringMvcContract {
@Override
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
// Process @RequestMapping on class
}
@Override
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
// Process @RequestMapping, @GetMapping, @PostMapping, etc.
}
@Override
protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
// Process parameter annotations
return isHttpAnnotation;
}
}MethodHandler Request Processing Logic
MethodHandler Routing
Based on the request method, the request is routed to different MethodHandler implementations.
final class SynchronousMethodHandler implements MethodHandler {
@Override
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Options options = findOptions(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
Request request = targetRequest(template);
if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
}
Response response = client.execute(request, options);
// Decode response based on status code
return response;
} catch (RetryableException e) {
// Retry logic
}
}
}
}Building Request Templates
Depending on the method, the request body may be a form submission or a raw body.
Executing Request Interceptors
Request targetRequest(RequestTemplate template) {
for (RequestInterceptor interceptor : requestInterceptors) {
interceptor.apply(template);
}
return target.apply(template);
}Request Logging Levels
public enum Level {
NONE, // No logging
BASIC, // Log method, URL, status, time
HEADERS,// Log headers + basic info
FULL // Log headers, body + basic info
}Client Execution of Final Request
The default client uses java.net and can be replaced with high‑performance implementations such as HttpClient or OKHttp.
class Default implements Client {
@Override
public Response execute(Request request, Request.Options options) throws IOException {
HttpURLConnection connection = convertAndSend(request, options);
return convertResponse(connection, request);
}
}Spring Cloud Load Balancer Client
public class FeignBlockingLoadBalancerClient {
@Override
public Response execute(Request request, Request.Options options) throws IOException {
URI originalUri = URI.create(request.url());
String serviceId = originalUri.getHost();
ServiceInstance instance = loadBalancerClient.choose(serviceId);
String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri).toString();
Request newRequest = Request.create(request.httpMethod(), reconstructedUrl, request.headers(), request.requestBody());
return delegate.execute(newRequest, options);
}
}Response Decoder Handling
class Default implements Encoder {
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) {
if (bodyType == String.class) {
template.body(object.toString());
} else if (bodyType == byte[].class) {
template.body((byte[]) object, null);
} else if (object != null) {
throw new EncodeException(String.format("%s is not a type supported by this encoder.", object.getClass()));
}
}
} public static class Default implements ErrorDecoder {
private final RetryAfterDecoder retryAfterDecoder = new RetryAfterDecoder();
@Override
public Exception decode(String methodKey, Response response) {
FeignException exception = errorStatus(methodKey, response);
Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER));
if (retryAfter != null) {
return new RetryableException(response.status(), exception.getMessage(), response.request().httpMethod(), exception, retryAfter, response.request());
}
return exception;
}
private <T> T firstOrNull(Map<String, Collection<T>> map, String key) {
if (map.containsKey(key) && !map.get(key).isEmpty()) {
return map.get(key).iterator().next();
}
return null;
}
}Injecting a custom ErrorDecoder is common practice.
Spring Cloud OpenFeign Extension
EnableFeignClients Parsing
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
}Adding @EnableFeignClients on the main class activates Spring Cloud OpenFeign functionality.
FeignClientsRegistrar
class FeignClientsRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
registerFeignClients(metadata, registry);
}
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// Scan for @FeignClient within configured base packages and register them
}
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
// Set properties such as url, path, name, contextId, type, decode404, fallback, fallbackFactory
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
// Register bean definition
}
}Default Auto‑Configuration
public class FeignAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("feign.hystrix.HystrixFeign")
protected static class DefaultFeignTargeterConfiguration {
@Bean
@ConditionalOnMissingBean
public Targeter feignTargeter() {
return new DefaultTargeter();
}
}
}HystrixFeign
public final class HystrixFeign {
public static Builder builder() {
return new Builder();
}
public static final class Builder extends Feign.Builder {
@Override
public Feign build(FallbackFactory<?> nullableFallbackFactory) {
super.invocationHandlerFactory((target, dispatch) -> new HystrixInvocationHandler(target, dispatch, setterFactory, nullableFallbackFactory));
super.contract(new HystrixDelegatingContract(contract));
return super.build();
}
}
} final class HystrixInvocationHandler implements InvocationHandler {
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
HystrixCommand<Object> hystrixCommand = new HystrixCommand<Object>(setterMethodMap.get(method)) {
@Override
protected Object run() throws Exception {
return HystrixInvocationHandler.this.dispatch.get(method).invoke(args);
}
};
return hystrixCommand.execute();
}
}SentinelFeign
public final class SentinelFeign {
// Similar to HystrixFeign but uses Sentinel for fault tolerance
} public class SentinelInvocationHandler implements InvocationHandler {
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
try {
ContextUtil.enter(resourceName);
entry = SphU.entry(resourceName, EntryType.OUT, 1, args);
return methodHandler.invoke(args);
} catch (Throwable ex) {
// fallback logic
throw ex;
} finally {
ContextUtil.exit();
}
}
}Summary Sequence Diagram
Future Plans
Upcoming articles will cover source‑code analysis of Ribbon, Hystrix, Sentinel, Nacos and other related components.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Architecture Diary
Committed to sharing original, high‑quality technical articles; no fluff or promotional content.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
