This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.
This series code address: github.com/JoJoTec/spr…
First, we present the component structure diagram in the official documentation:
The components in the official documents are based on the implementation function dimension, while here we are based on the source code implementation dimension (because we need to customize these components according to needs when we use them later, so we need to split and analyze them from the source code perspective), there may be some minor differences.
Contract, which is responsible for parsing class metadata
OpenFeign automatically generates HTTP APIS by proxy class metadata, so which class metadata is parsed and which class metadata is valid is achieved by specifying a Contract. We can implement this Contract to define the parsing of some class metadata, for example, We define a custom annotation:
/ / in the way that can only be used for @ Java lang. The annotation. Target (METHOD) to maintain / / the specified annotation to the RUNTIME @ Retention (RUNTIME) @ interface Get {/ / request uri String uri (); }Copy the code
The annotation is simple, and the annotated method is automatically encapsulated as a GET request that returns uri().
We then define a custom Contract to handle the annotation. Since MethodMetadata is final and package private, we have to inherit contract. BaseContract to customize annotation parsing:
// External customizations must inherit BaseContract, The constructor for the generated MethodMetadata is package Private's static class CustomizedContract extends Contract.basecontract {@override protected void processAnnotationOnClass(MethodMetadata data, Class<? > CLZ) {// Handle the annotation above the class, Useless to} here @ Override protected void processAnnotationOnMethod (MethodMetadata data, the Annotation Annotation, Method Method) {Get Get = method.getannotation (get.class); // If the Get annotation exists, specify the method HTTP request as Get, and specify the uri as the annotation uri() return if (Get! = null) { data.template().method(Request.HttpMethod.GET); data.template().uri(get.uri()); } } @Override protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, Int paramIndex) {// Handle the annotation above the parameter, return false is not used here; }}Copy the code
Then, we 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"); / / is actually called http://www.httpbin.org/get String s = httpBin. The get (); }Copy the code
In general, we don’t use this Contract because we don’t customise annotations in our business. This is what the underlying framework needs to do. For example, in a Spring-MVC environment, we need spring-MVC-compliant 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; }Copy the code
OpenFeign can customize codecs. Here we use FastJson to customize a set of codecs and codecs to see how this works.
/** * static class implements Decoder {@override public Object Decoder (Response) response, Type type) throws IOException, DecodeException, [] body = response.body().asinputStream ().readallBytes (); FeignException {// Read body byte[] body = response.body().asinputStream ().readallBytes (); return JSON.parseObject(body, type); }} /** * static class implements Encoder {@override public void encode(Object) object, Type bodyType, RequestTemplate template) throws EncodeException { if (object ! Header (CONTENT_TYPE, contentType.application_json.getMimeType ()); template.body(JSON.toJSONBytes(object), StandardCharsets.UTF_8); }}}Copy the code
Then, we test through http://httpbin.org/anything, this link will return everything we send request element.
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"));
}
Copy the code
Looking at the response, you can see that the JSON body we sent was received correctly.
Currently, the encoder and decoder implementations in the OpenFeign project include:
serialization | Additional dependencies need to be added | The implementation class |
---|---|---|
Convert directly to string, default codec | There is no | feign.codec.Encoder.Default 和 feign.codec.Decoder.Default |
gson | feign-gson | feign.gson.GsonEncoder 和 feign.gson.GsonDecoder |
xml | feign-jaxb | feign.jaxb.JAXBEncoder 和 feign.jaxb.JAXBDecoder |
json (jackson) | feign-jackson | feign.jackson.JacksonEncoder 和 feign.jackson.JacksonDecoder |
When we use it in the Spring Cloud environment, there is a unified encoder and decoder in The Spring MVC, namely HttpMessageConverters, and it is compatible with the glue project. So we’ll stick with HttpMessageConverters to specify custom codecs.
RequestInterceptor
RequestInterceptor interface definition:
public interface RequestInterceptor {
void apply(RequestTemplate template);
}
Copy the code
As you can see from the interface, the RequestInterceptor actually does additional operations on the RequestTemplate. Each request is handled by all the RequestInterceptor interceptors.
For example, we can add a specific Header to each 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(); }Copy the code
Execute the program and see in the response the header we added to the request we sent.
Http request Client Client
The underlying Http request client of OpenFeign can be customized. OpenFeign has a wrapper for different Http clients, and the default is through Java’s built-in Http request API. Let’s look at the Client interface definition source:
Public interface Client {/** * Perform request * @param request HTTP request * @param options Configuration options * @return * @throws IOException */ Response execute(Request request, Options options) throws IOException; }Copy the code
Request is the definition of Http Request in Feign. The Client implementation needs to convert the Request into the Request of the corresponding underlying Http Client and call the appropriate method to make the Request. Options are common configurations for requests, including:
Public static class Options {private final Long connectTimeout; // TCP connection timeout unit private final TimeUnit connectTimeoutUnit; Private final Long readTimeout; // Request read response timeout unit private final TimeUnit readTimeoutUnit; Private final Boolean uploader; }Copy the code
Currently, Client implementations include the following:
The underlying HTTP client | Dependencies that need to be added | The implementation class |
---|---|---|
Java HttpURLConnection | There is no | 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
Can specify the wrong decoder ErrorDecoder, at the same time can also specify ExceptionPropagationPolicy exception thrown strategy.
ErrorDecoder is used to read the HTTP response to see if there is an error that needs to be thrown:
public interface ErrorDecoder {
public Exception decode(String methodKey, Response response);
}
Copy the code
The decode method of the configured ErrorDecoder is 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 retry-after from the HTTP response header, encapsulating the exception as RetryableException if it exists 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; }}Copy the code
It can be seen that ErrorDecoder is possible to encapsulate a layer of anomaly, it sometimes for us in order to capture the impact, so can be opened by designated ExceptionPropagationPolicy this layer encapsulation. ExceptionPropagationPolicy is an enumeration type:
Public enum ExceptionPropagationPolicy {/ / do nothing NONE, // The original exception of RetryableException will be extracted and thrown as an exception. // The original exception of RetryableException will be thrown as an exception. Return cause if it is not empty, otherwise return original exception UNWRAP,; }Copy the code
Here’s 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, FeignException FeignException exception = errorStatus(methodKey, Response); 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, the following exception is RetryableException, Otherwise, it is a kind of cause is RetryableException FeignException. ExceptionPropagationPolicy (exceptionPropagationPolicy. UNWRAP) .target(TestHttpBin.class, "http://httpbin.org"); httpBin.get(); }Copy the code
Feign.feignexception $InternalServerError: [500 INTERNAL SERVER ERROR] during the [GET] to [http://httpbin.org/status/500] [TestHttpBin# the GET ()] : [] this exception.
Retryer, the retries for RetryableException
When an exception occurs in the call, we may want to retry according to a policy, which generally includes:
- Which exceptions are retried
- When should I retry and when should I end the retry, for example, after n retries
For those exceptions retries are determined by ErrorDecoder. If the exception needs to be retried, it is encapsulated as a RetryableException so that Feign will retry using Retryer. These are the things Retryer needs to consider as to when to retry and when to stop retry:
Public interface Retryer extends Cloneable {/** * continueOrPropagate(RetryableException e); public interface Retryer extends Cloneable {/** * continueOrPropagate(RetryableException e); /** * For each request, this method is called to create a new Retryer object with the same configuration */ Retryer clone(); }Copy the code
Let’s look at Retryer’s default implementation:
Class Default implements Retryer {// Maximum number of attempts private final int maxAttempts; // Initial retry interval private final Long Period; Private final Long maxPeriod; Int attempt; // The currently waited retry interval and long sleptForMillis; This (100, seconds.tomillis (1), 5); this(100, seconds.tomillis (1), 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 at 1 because the first continueOrPropagate call has already occurred but failed and RetryableException this.attempt = 1; } // visible for testing; protected long currentTimeMillis() { return System.currentTimeMillis(); } public void continueOrPropagate(RetryableException e) {// if (attempt++ >= maxAttempts) {throw e; } long interval; // If (e.retry-after ()! = null) { interval = e.retryAfter().getTime() - currentTimeMillis(); if (interval > maxPeriod) { interval = maxPeriod; } if (interval < 0) { return; NextMaxInterval = nextMaxInterval(); nextMaxInterval = nextMaxInterval(); } try { Thread.sleep(interval); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); throw e; SleptForMillis += interval; } // Each retry interval increases 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() {return new Default(period, maxPeriod, maxAttempts); }}Copy the code
The default Retryer function is also rich, and users can refer to the retries that are more suitable for their own business scenarios.
Configuration Options for each HTTP request
Regardless of the HTTP client, the following configuration is required:
- Connection timeout: This is the TCP connection timeout
- Read timeout: This is the timeout before the HTTP response is received
- Whether to follow redirection
OpenFeign can be configured using 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;
}
Copy the code
For example, we could configure a connection timeout of 500ms, read timeout of 6s, following the redirected Feign:
Feign.builder().options(new Request.Options(
500, TimeUnit.MILLISECONDS, 6, TimeUnit.SECONDS, true
))
Copy the code
This section describes the various components of OpenFeign in detail. With this knowledge, we can actually implement the glue code in Spring-Cloud-OpenFeign ourselves. Spring-cloud-openfeign registers these components as beans to the NamedContextFactory, which can be configured differently by different microservices.
Wechat search “my programming meow” public account, a daily brush, easy to improve skills, won a variety of offers