
Code address of this series: https://github.com/JoJoTec/spring-cloud-parent
First, we give the component structure diagram in the official document: [external chain picture transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-jrw9fluy-1633609229588)( http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/OpenFeign/feign/master/src/docs/overview -mindmap. iuml)]
The components in the official document take the implementation function as the dimension, and here we take the source code implementation as the dimension (because we need to customize these components as needed when we use them later, we need to split and analyze them from the perspective of source code). There may be some small differences.
Contract responsible for parsing class metadata
OpenFeign automatically generates the HTTP API through proxy class metadata. What class metadata is parsed and which class metadata is valid is implemented by specifying a Contract. We can customize the parsing of some class metadata by implementing this Contract. For example, we customize an annotation:
//Available only on methods @java.lang.annotation.Target(METHOD) //Specifies that annotations remain at run time @Retention(RUNTIME) @interface Get { //Request uri String uri(); }
This annotation is very simple. The method marked with this annotation will be automatically encapsulated into a GET request, and the request uri is the return of uri().
Then, we customize a Contract to handle this annotation. Because MethodMetadata is final and package private, we can only inherit Contract Basecontract to customize annotation resolution:
//External customization must inherit BaseContract because the constructor of MethodMetadata generated inside is package private static class CustomizedContract extends Contract.BaseContract { @Override protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) { //The annotation above the processing class is not used here } @Override protected void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method) { //Comments on processing methods Get get = method.getAnnotation(Get.class); //If the GET annotation exists, the HTTP request method of the specified method is GET, and the URI is specified as the return of the annotation uri() if (get != null) { data.template().method(Request.HttpMethod.GET); data.template().uri(get.uri()); } } @Override protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { //The comments above the processing parameters are not used here return false; } }
Then, let's use this Contract:
interface HttpBin { @Get(uri = "/get") String get(); } public static void main(String[] args) { HttpBin httpBin = Feign.builder() .contract(new CustomizedContract()) .target(HttpBin.class, "http://www.httpbin.org"); //It's actually a call http://www.httpbin.org/get String s = httpBin.get(); }
Generally, we will not use this Contract because we generally do not customize annotations in business. This is the function required by the underlying framework. For example, in the spring MVC environment, we need spring MVC compatible annotations. This implementation class is SpringMvcContract.
Encoder and Decoder
Encoder and decoder interface definition:
public interface Decoder { Object decode(Response response, Type type) throws IOException, DecodeException, FeignException; } public interface Encoder { void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException; }
OpenFeign can customize the codec. Here we use fastjason to customize a set of codecs and decoders to understand the principle used.
/** * Deserialization decoder based on fastjason */ static class FastJsonDecoder implements Decoder { @Override public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException { //Read body byte[] body = response.body().asInputStream().readAllBytes(); return JSON.parseObject(body, type); } } /** * Serialization encoder based on fastjason */ static class FastJsonEncoder implements Encoder { @Override public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { if (object != null) { //Code body template.header(CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType()); template.body(JSON.toJSONBytes(object), StandardCharsets.UTF_8); } } }
Then we passed http://httpbin.org/anything To test, this link will return all the elements of the request we sent.
interface HttpBin { @RequestLine("POST /anything") Object postBody(Map<String, String> body); } public static void main(String[] args) { HttpBin httpBin = Feign.builder() .decoder(new FastJsonDecoder()) .encoder(new FastJsonEncoder()) .target(HttpBin.class, "http://www.httpbin.org"); Object o = httpBin.postBody(Map.of("key", "value")); }
Looking at the response, we can see that the json body we sent was correctly received.
At present, the encoder and decoder in OpenFeign project mainly include:
serialize | Additional dependencies need to be added | Implementation class |
---|---|---|
Convert directly to string, default codec | nothing | feign.codec.Encoder.Default and feign codec. Decoder. Default |
gson | feign-gson | feign.gson.GsonEncoder and feign gson. GsonDecoder |
xml | feign-jaxb | feign.jaxb.JAXBEncoder and feign jaxb. JAXBDecoder |
json (jackson) | feign-jackson | feign. jackson. Jackson encoder and feign jackson. JacksonDecoder |
When we use it in the Spring Cloud environment, there is a unified encoder and decoder in Spring MVC, that is, HttpMessageConverters, which are compatible through the glue project, so we can uniformly specify a custom codec with HttpMessageConverters.
Request interceptor
Interface definition of RequestInterceptor:
public interface RequestInterceptor { void apply(RequestTemplate template); }
It can be seen from the interface that the RequestInterceptor actually performs additional operations on the RequestTemplate. Each request is processed by all requestinterceptors.
For example, we can add a specific Header for each request:
interface HttpBin { //For all requests sent to this link, the response will return all elements in the request @RequestLine("GET /anything") String anything(); } static class AddHeaderRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { //Add header template.header("test-header", "test-value"); } } public static void main(String[] args) { HttpBin httpBin = Feign.builder() .requestInterceptor(new AddHeaderRequestInterceptor()) .target(HttpBin.class, "http://www.httpbin.org"); String s = httpBin.anything(); }
Execute the program, and you can see the header added in our sending request in the response.
Http request Client
The underlying Http request Client of OpenFeign can be customized. OpenFeign has encapsulation for different Http clients. By default, it uses the built-in Http request API in Java. Let's look at the interface definition source code of the Client:
public interface Client { /** * Execute request * @param request HTTP request * @param options configuration option * @return * @throws IOException */ Response execute(Request request, Options options) throws IOException; }
Request is the definition of Http request in feign. The implementation of the Client needs to convert the request into the request corresponding to the underlying Http Client and call the appropriate method to make the request. Options are common configurations for requests, including:
public static class Options { //tcp connection establishment timeout private final long connectTimeout; //tcp connection establishment timeout unit private final TimeUnit connectTimeoutUnit; //Request read response timeout private final long readTimeout; //Request read response timeout unit private final TimeUnit readTimeoutUnit; //Follow redirection private final boolean followRedirects; }
At present, the implementation of Client includes the following:
Underlying HTTP client | Dependencies to add | Implementation class |
---|---|---|
Java HttpURLConnection | nothing | feign.Client.Default |
Java 11 HttpClient | feign-java11 | feign.http2client.Http2Client |
Apache HttpClient | feign-httpclient | feign.httpclient.ApacheHttpClient |
Apache HttpClient 5 | feign-hc5 | feign.hc5.ApacheHttp5Client |
Google HTTP Client | feign-googlehttpclient | feign.googlehttpclient.GoogleHttpClient |
Google HTTP Client | feign-googlehttpclient | feign.googlehttpclient.GoogleHttpClient |
jaxRS | feign-jaxrs2 | feign.jaxrs2.JAXRSClient |
OkHttp | feign-okhttp | feign.okhttp.OkHttpClient |
Ribbon | feign-ribbon | feign.ribbon.RibbonClient |
Error decoder correlation
You can specify the error decoder ErrorDecoder and the exception throwing policy ExceptionPropagationPolicy
ErrorDecoder is used to read the HTTP response and judge whether there is an error and throw an exception:
public interface ErrorDecoder { public Exception decode(String methodKey, Response response); }
The decode method of the configured ErrorDecoder will be called only when the response code is not 2xx. The default implementation of ErrorDecoder is:
public static class Default implements ErrorDecoder { @Override public Exception decode(String methodKey, Response response) { //Wrap different response codes into different exceptions FeignException exception = errorStatus(methodKey, response); //Extract the HTTP response header of retry after. If this response header exists, the exception will be encapsulated as RetryableException //For RetryableException, we will know that if this exception is thrown, the retry of the retrier will be triggered 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; } }
It can be seen that the ErrorDecoder may encapsulate a layer of exceptions, which sometimes affects our capture in the outer layer, so we can disassemble this layer of encapsulation by specifying ExceptionPropagationPolicy. ExceptionPropagationPolicy is an enumeration class:
public enum ExceptionPropagationPolicy { //Don't do anything? NONE, //Whether to extract the original exception of RetryableException and throw it as an exception //At present, it only takes effect for RetryableException. Call getCause of exception. If it is not empty, return this cause. Otherwise, return the original exception UNWRAP, ; }
Let's take an example:
interface TestHttpBin { //The request must return 500 @RequestLine("GET /status/500") Object get(); } static class TestErrorDecoder implements ErrorDecoder { @Override public Exception decode(String methodKey, Response response) { //Get FeignException corresponding to error code FeignException exception = errorStatus(methodKey, response); //Encapsulated as RetryableException return new RetryableException( response.status(), exception.getMessage(), response.request().httpMethod(), exception, new Date(), response.request()); } } public static void main(String[] args) { TestHttpBin httpBin = Feign.builder() .errorDecoder(new TestErrorDecoder()) //If UNWRAP is not specified here, the exception thrown below is RetryableException. Otherwise, it is the cause of RetryableException, that is, FeignException .exceptionPropagationPolicy(ExceptionPropagationPolicy.UNWRAP) .target(TestHttpBin.class, "http://httpbin.org"); httpBin.get(); }
After execution, you can find that feign. Is thrown FeignException$InternalServerError: [500 INTERNAL SERVER ERROR] during [GET] to [ http://httpbin.org/status/500 ][testhttpbin#get()]: [] this exception.
Retryer for RetryableException
When an exception occurs in the call, we may want to retry according to a certain strategy. This abstract retry strategy generally includes:
- Which exceptions will be retried
- When to retry and when to end the retry, for example, after n retries
Those exceptions will be retried, which is determined by ErrorDecoder. If the exception needs to be retried, it is encapsulated as a RetryableException, so Feign will retry using retryer. Retryer needs to consider when to retry and when to end the retry:
public interface Retryer extends Cloneable { /** * Judge whether to continue to retry or throw an exception to end the retry */ void continueOrPropagate(RetryableException e); /** * For each request, this method is called to create a new Retryer object with the same configuration */ Retryer clone(); }
Let's take a look at the default implementation of Retryer:
class Default implements Retryer { //max retries private final int maxAttempts; //Initial retry interval private final long period; //Maximum retry interval private final long maxPeriod; //Current number of retries int attempt; //The retry interval and time currently waiting long sleptForMillis; public Default() { //By default, the initial retry interval is 100ms, the maximum retry interval is 1s, and the maximum number of retries is 5 this(100, SECONDS.toMillis(1), 5); } public Default(long period, long maxPeriod, int maxAttempts) { this.period = period; this.maxPeriod = maxPeriod; this.maxAttempts = maxAttempts; //The current number of retries starts from 1, because the call has occurred before the first entry into continueOrPropagate, but it failed and threw a RetryableException this.attempt = 1; } // visible for testing; protected long currentTimeMillis() { return System.currentTimeMillis(); } public void continueOrPropagate(RetryableException e) { //If the current number of retries is greater than the maximum number of retries, the if (attempt++ >= maxAttempts) { throw e; } long interval; //If retry after is specified, the waiting time is determined based on this header if (e.retryAfter() != null) { interval = e.retryAfter().getTime() - currentTimeMillis(); if (interval > maxPeriod) { interval = maxPeriod; } if (interval < 0) { return; } } else { //Otherwise, it is calculated by nextMaxInterval interval = nextMaxInterval(); } try { Thread.sleep(interval); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); throw e; } //Record the total waiting time sleptForMillis += interval; } //Increase each retry interval by 50% until the maximum retry interval long nextMaxInterval() { long interval = (long) (period * Math.pow(1.5, attempt - 1)); return interval > maxPeriod ? maxPeriod : interval; } @Override public Retryer clone() { //Copy configuration return new Default(period, maxPeriod, maxAttempts); } }
The default Retryer has rich functions. Users can refer to this to implement a Retryer that is more suitable for their business scenarios.
Configuration Options for each HTTP request
The following configurations are required for any HTTP client:
- Connection timeout: This is the TCP connection establishment timeout
- Read timeout: This is the timeout before receiving the HTTP response
- Follow redirection OpenFeign can be configured through Options:
public static class Options { private final long connectTimeout; private final TimeUnit connectTimeoutUnit; private final long readTimeout; private final TimeUnit readTimeoutUnit; private final boolean followRedirects; }
For example, we can configure a connection timeout of 500ms and a read timeout of 6s to follow the redirected Feign:
Feign.builder().options(new Request.Options( 500, TimeUnit.MILLISECONDS, 6, TimeUnit.SECONDS, true ))

In this section, we introduced various components of OpenFeign in detail. With this knowledge, we can actually implement the glue code in spring cloud OpenFeign ourselves. In fact, spring cloud OpenFeign registers these components in the NamedContextFactory in the form of beans for different configurations of different microservices.