Spring cloud upgrade 2020.0.x version - 29. Analysis of spring cloud openfeign

Keywords: spring-cloud

Code address of this series: https://github.com/JoJoTec/sp...

In many microservices that use cloud native, relatively small-scale ones may directly rely on the load balancer in the cloud service for internal domain name and service mapping, judge the instance health status through the health check interface, and then directly use OpenFeign to generate the Feign Client of the corresponding domain name. In the Spring Cloud ecosystem, OpenFeign is encapsulated, and the components of Feign Client are also customized to achieve integrated service discovery and load balancing in OpenFeign Client. On this basis, we also combined Resilience4J components to realize microservice instance level thread isolation, microservice method level circuit breaker and retry.

Let's first analyze the Spring Cloud OpenFeign

Spring Cloud OpenFeign parsing

HTTP codec, combined with codec in spring boot

Any component in Spring Cloud is implemented based on Spring Boot. Since there is already an HTTP codec in Spring Boot, it is not necessary to implement the HTTP codec separately for OpenFeign, but consider implementing the codec interface of OpenFeign with the HTTP codec of Spring Boot.

In FeignClientsConfiguration, the default implementation is provided:

//Due to the initialization sequence and the Configuration initialization of NamedContextFactory, it is necessary to inject ObjectFactory instead of HttpMessageConverters to prevent the Bean from being found
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;

@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() { 
    return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
}

@Bean
@ConditionalOnMissingBean
//We ignore the existence of Pageable class here
//The compatible implementation of paging wrapper Pageable for Spring Data is also relatively simple, which is ignored here
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
    return springEncoder(formWriterProvider, encoderProperties);
}

private Encoder springEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider,
    FeignEncoderProperties encoderProperties) {
    AbstractFormWriter formWriter = formWriterProvider.getIfAvailable();
    
    if (formWriter != null) {
        return new SpringEncoder(new SpringPojoFormEncoder(formWriter), this.messageConverters, encoderProperties);
    }
    else {
        return new SpringEncoder(new SpringFormEncoder(), this.messageConverters, encoderProperties);
    }
}

Decoder based on spring decoder

It can be seen from the source code that the default Decoder is a Decoder packaged in several layers, including:

  • Optional decoder: decoder used to handle optional encapsulation classes in Java JDK
  • ResponseEntityDecoder: a decoder used to handle the request response encapsulation class HttpEntity in spring web
  • Spring Decoder: Feign Decoder implemented using spring Decoder

The HttpMessageConverters object passed into SpringDecoder is a collection of all HttpMessageConverters of spring web. HttpMessageConverter is a tool for encoding and decoding HTTP request and response bodies in spring web. Its interface structure is:

public interface HttpMessageConverter<T> {
    //Judge whether the clazz type can be read by the current HttpMessageConverter
    boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
    //Judge whether the clazz type can be written by the current HttpMessageConverter
    boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
    //Get all supported mediatypes
    List<MediaType> getSupportedMediaTypes();
    //Obtain the MediaType supported by the HttpMessageConverter through the clazz type
    //The default implementation is to return all supported mediatypes of getsupportedmediatetypes if the type can be read or written by the current HttpMessageConverter
    default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
        return (canRead(clazz, null) || canWrite(clazz, null) ?
                getSupportedMediaTypes() : Collections.emptyList());
    }

    //Read and parse the object of clazz type from the inputMessage. When the requested content type is a supported MediaType
    T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException;

    //Write object t serialization to HttpOutputMessage when the requested accept is a supported MediaType
    void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException;

}

spring boot has built-in many httpmessageconverters. We can also implement our own HttpMessageConverter to implement our custom MediaType. For example, we define one here:

public class CustomizedHttpMessageConverter implements HttpMessageConverter<Student> {
    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        return clazz.equals(Student.class);
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return clazz.equals(Student.class);
    }
    
    public static class StudentMediaType extends MediaType {
        public StudentMediaType() {
            super("application", "student", StandardCharsets.UTF_8);
        }
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return List.of(new StudentMediaType());
    }

    @Override
    public Student read(Class<? extends Student> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        String temp = StreamUtils.copyToString(inputMessage.getBody(), StandardCharsets.UTF_8);
        String[] split = temp.split(",");
        return new Student(
                Long.parseLong(split[0]),
                split[1]);
    }

    @Override
    public void write(Student student, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        outputMessage.getBody().write((student.getId() + "," + student.getName()).getBytes(StandardCharsets.UTF_8));
    }
}

Then, like before, configure it into the spring boot compatible MVC configuration:

@Configuration(proxyBeanMethods = false)
public class TestConfig implements WebMvcConfigurer {
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new CustomizedHttpMessageConverter());
    }
}

Write Controller and test:

@RestController
@RequestMapping("/test")
public class TestController {
    @PostMapping("/post-to-student")
    public Student postToStudent(@RequestBody Student student) {
        return student;
    }
}

Use a tool like postman to specify the HTTP request header:

Content-Type:application/student
Accept:application/student

Body is:

1,zhx

After the request, it will go to the read of the customized httpmessageconverter and resolve it into a student object. Then, the responding student will be written into the response Body by the write of the customized httpmessageconverter

It can be seen that due to the existence of Spring encoder, we can reuse the built-in HttpMessageConverter in Spring. At the same time, we can also extend and customize our own HttpMessageConverter, which is very convenient.

The code of ResponseEntityDecoder is relatively simple. The effect is to ignore HttpEntity, the spring web wrapper class for HTTP response, when decoding:

@Override
public Object decode(final Response response, Type type) throws IOException, FeignException {
    //Is it httpentity with formal parameters <? > 
    if (isParameterizeHttpEntity(type)) {
        //Take out the parameter type
        type = ((ParameterizedType) type).getActualTypeArguments()[0];
        //Use the formal parameter type to resolve the response
        Object decodedObject = this.decoder.decode(response, type);
        //Fill in the returned object, status code and other information of HttpEntity
        return createResponse(decodedObject, response);
    }
    else if (isHttpEntity(type)) {
        //Empty formal parameter, which means there is no body or body is ignored, and only HttpEntity status code and other information are filled in
        return createResponse(null, response);
    }
    else {
        //If it is not HttpEntity, decode it directly
        return this.decoder.decode(response, type);
    }
}

In fact, in order to be compatible with the response of RestTemplate, RestTemplate can return HttpEntity, but the body returned by the underlying HTTP request does not wrap this type.

Similarly, the Optional wrapper class in JDK also needs to do the same thing, which is implemented through OptionalDecoder.

Encoder based on spring encoder

The spring encoder is also very simple and is based on the HttpMessageConverter in spring. We won't repeat it here.

WeChat search "my programming meow" attention to the official account, daily brush, easy to upgrade technology, and capture all kinds of offer:

Posted by tHud on Mon, 08 Nov 2021 18:09:12 -0800