OpenFeign Deep Dive: Dynamic Proxies, Request Flow, and Spring Cloud Integration

This article provides a comprehensive walkthrough of OpenFeign’s request processing pipeline, covering dynamic proxy generation, annotation parsing, method handler routing, request template building, interceptor execution, logging, client implementation, and Spring Cloud OpenFeign extensions such as Hystrix and Sentinel, illustrated with code snippets and diagrams.

Java Architecture Diary
Java Architecture Diary
Java Architecture Diary
OpenFeign Deep Dive: Dynamic Proxies, Request Flow, and Spring Cloud Integration

0. Intro Demo

This code is an OpenFeign example that fetches all contributors of a GitHub repository and creates an issue; it is recommended to start debugging with DEBUG level to read the source code.

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);
}

Using JDK Dynamic Proxy to Generate Interface Proxy

Dynamic Proxy Generates 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 Annotation Information

To parse the annotations of GitHub.contributors, Feign provides a Contract implementation. The default contract processes @RequestLine, @Body, @Headers, @Param, @QueryMap, and @HeaderMap annotations.

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
    } else if (annotationType == Body.class) {
      // @Body handling
    } else if (annotationType == Headers.class) {
      // @Headers handling
    }
  }

  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
      } else if (annotationType == QueryMap.class) {
        // @QueryMap handling
      } else if (annotationType == HeaderMap.class) {
        // @HeaderMap handling
      }
    }
    return isHttpAnnotation;
  }
}

Spring MVC Extension Annotations

SpringMvcContract adds support for Spring MVC annotations (e.g., @RequestMapping, @GetMapping, @PostMapping) to 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) {
    // handle parameter annotations
    return isHttpAnnotation;
  }
}

MethodHandler Request Processing Logic

MethodHandler Routing

According to the request method, the call 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
        return response;
      } catch (RetryableException e) {
        // retry logic
      }
    }
  }
}

Building Request Template Based on Parameters

Form submission vs. direct body submission.

Executing Request Interceptors to Build Final Request

Request targetRequest(RequestTemplate template) {
  for (RequestInterceptor interceptor : requestInterceptors) {
    interceptor.apply(template);
  }
  return target.apply(template);
}

Request Logging Handling

Log level configuration.

public enum Level {
  NONE,   // no output
  BASIC,  // method, URL, status, time
  HEADERS,// headers + basic info
  FULL    // headers, body + basic info
}

Client Executes Final Request

The default client uses java.net, but can be replaced with HttpClient or OKHttp for higher performance.

class Default implements Client {
  private final SSLSocketFactory sslContextFactory;
  private final HostnameVerifier hostnameVerifier;

  @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

Default encoder implementation.

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()));
    }
  }
}

Default error decoder implementation.

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.

The above describes OpenFeign’s request processing flow; the following explains how spring-cloud-open-feign initializes and runs.

Extension: Spring Cloud OpenFeign

EnableFeignClients Parsing

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
}

Adding @EnableFeignClients on the main class activates spring-cloud-open-feign features.

The Import(FeignClientsRegistrar.class) imports FeignClientsRegistrar, which scans for @FeignClient and registers them in the container.

FeignClientsRegistrar

class FeignClientsRegistrar {
  @Override
  public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    registerFeignClients(metadata, registry);
  }

  public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    // scan base packages for @FeignClient 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 like url, path, name, contextId, type, decode404, fallback, fallbackFactory
    definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
    // register bean definition
  }
}

Default Configuration

public class FeignAutoConfiguration {
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingClass("feign.hystrix.HystrixFeign")
    protected static class DefaultFeignTargeterConfiguration {
        @Bean
        @ConditionalOnMissingBean
        public Targeter feignTargeter() {
            return new DefaultTargeter();
        }
    }
}

If the feign-hystrix module is not included, the flow remains the same as the original; calling a Feign client method triggers the dynamic proxy and MethodHandler.

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(new InvocationHandlerFactory() {
        @Override
        public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
          return new HystrixInvocationHandler(target, dispatch, setterFactory, nullableFallbackFactory);
        }
      });
      super.contract(new HystrixDelegatingContract(contract));
      return super.build();
    }
  }
}

HystrixInvocationHandler wraps method execution with a HystrixCommand.

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

/** {@link Feign.Builder} like {@link HystrixFeign.Builder}. */
public final class SentinelFeign {
}

SentinelInvocationHandler wraps requests with Sentinel for flow control and fallback.

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);
      result = methodHandler.invoke(args);
    } catch (Throwable ex) {
      // fallback logic
    } finally {
      ContextUtil.exit();
    }
    return result;
  }
}

Summary Sequence Diagram

Future Plans

Upcoming articles will cover Ribbon, Hystrix, Sentinel, Nacos and other components with source code analysis.

Image resources are available from the public account “JAVA架构日记”.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaHTTPSpring CloudOpenFeign
Java Architecture Diary
Written by

Java Architecture Diary

Committed to sharing original, high‑quality technical articles; no fluff or promotional content.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.