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

Posted by newyear498 on Sun, 07 Nov 2021 07:01:04 +0100

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

Start with NamedContextFactory

github address of Spring Cloud OpenFeign: https://github.com/spring-clo...

First, according to our previous analysis of the spring cloud loadbalancer process, we start with the class that inherits NamedContextFactory. Here is FeignContext. Through its constructor, we get the default configuration class:

FeignContext.java

public FeignContext() {
    super(FeignClientsConfiguration.class, "feign", "feign.client.name");
}

As can be seen from the construction method, the default configuration class is FeignClientsConfiguration. Next, we analyze the elements in this configuration class in detail and combine them with the OpenFeign components we analyzed earlier.

The Contract responsible for parsing class metadata is combined with the HTTP annotation of spring web

In order for developers to use and understand better, it is best to use spring web HTTP annotations (such as @ RequestMapping, @ GetMapping, etc.) to define FeignClient interface. This is done in FeignClientsConfiguration:

FeignClientsConfiguration.java

@Autowired(required = false)
private FeignClientProperties feignClientProperties;

@Autowired(required = false)
private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();

@Autowired(required = false)
private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>();

@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
    boolean decodeSlash = feignClientProperties == null || feignClientProperties.isDecodeSlash();
    return new SpringMvcContract(this.parameterProcessors, feignConversionService, decodeSlash);
}

@Bean
public FormattingConversionService feignConversionService() {
    FormattingConversionService conversionService = new DefaultFormattingConversionService();
    for (FeignFormatterRegistrar feignFormatterRegistrar : this.feignFormatterRegistrars) {
        feignFormatterRegistrar.registerFormatters(conversionService);
    }
    return conversionService;
}

The Feign Contract provided by its core is spring mvccontract, which mainly contains two parts of core logic:

  • Define Formatter and Converter registration for Feign Client
  • Use AnnotatedParameterProcessor to parse spring MVC annotations and our custom annotations

Define Formatter and Converter registration for Feign Client

First, Spring provides a type conversion mechanism, in which one-way type conversion implements the Converter interface; In web applications, we often need to convert the string type data passed in from the front end into a specified format or a specified data type to meet our call requirements. Similarly, the back-end development also needs to adjust the returned data to a specified format or a specified type to return to the front-end page (in Spring Boot, we have already done the conversion from json parsing and return objects to json, but in some special cases, such as compatibility with the old project interface, we may also use it). This is achieved by implementing the Formatter interface. Take a simple example:

Define a type:

@Data
@AllArgsConstructor
public class Student {
    private final Long id;
    private final String name;
}

We define a Converter that can parse the object of this class through a string. For example, "1,zhx" means id = 1 and name = zhx:

public class StringToStudentConverter implements Converter<String, Student> {
    @Override
    public Student convert(String from) {
        String[] split = from.split(",");
        return new Student(
                Long.parseLong(split[0]),
                split[1]);
    }
}

Then register the Converter:

@Configuration(proxyBeanMethods = false)
public class TestConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToStudentConverter());
    }
}

Write a test interface:

@RestController
@RequestMapping("/test")
public class TestController {
    @GetMapping("/string-to-student")
    public Student stringToStudent(@RequestParam("student") Student student) {
        return student;
    }
}

Call / test / string to student? Student = 1, ZHX, and you can see the return:

{
    "id": 1,
    "name": "zhx"
}

Similarly, we can also use Formatter:

public class StudentFormatter implements Formatter<Student> {
    @Override
    public Student parse(String text, Locale locale) throws ParseException {
        String[] split = text.split(",");
        return new Student(
                Long.parseLong(split[0]),
                split[1]);
    }

    @Override
    public String print(Student object, Locale locale) {
        return object.getId() + "," + object.getName();
    }
}

Then register this Formatter:

@Configuration(proxyBeanMethods = false)
public class TestConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new StudentFormatter());
    }
}

Feign also provides this registration mechanism. In order to distinguish it from the registration mechanism of spring webmvc, FeignFormatterRegistrar is used to inherit the FormatterRegistrar interface. Then, the Bean FormattingConversionService is defined to realize the registration of Formatter and Converter. For example:

Suppose we have another micro service that needs to call the above interface through FeignClient, then we need to define a FeignFormatterRegistrar to register the Formatter:

@Bean
public FeignFormatterRegistrar getFeignFormatterRegistrar() {
    return registry -> {
        registry.addFormatter(new StudentFormatter());
    };
}

Then we define FeignClient:

@FeignClient(name = "test-server", contextId = "test-server")
public interface TestClient {
    @GetMapping("/test/string-to-student")
    Student get(@RequestParam("student") Student student);
}

When the get method is called, the print of StudentFormatter will be called to output the Student object as a formatted string. For example, {"id": 1,"name": "zhx"} will become 1,zhx.

AnnotatedParameterProcessor to parse spring MVC annotations and our custom annotations

AnnotatedParameterProcessor is a Bean used to parse annotations into AnnotatedParameterContext. AnnotatedParameterContext contains the request definition of Feign, including, for example, the MethodMetadata of Feign mentioned earlier, that is, method metadata. The default AnnotatedParameterProcessor includes the corresponding parsing of all spring MVC annotations for HTTP method definitions, such as @ re Requestparameterprocessor corresponding to the requestparameter annotation:

RequestParamParameterProcessor.java

public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
    //Gets the number of times the current parameter belongs to the method
    int parameterIndex = context.getParameterIndex();
    //Get parameter type
    Class<?> parameterType = method.getParameterTypes()[parameterIndex];
    //Resolved method metadata to save MethodMetadata
    MethodMetadata data = context.getMethodMetadata();

    //If it is a Map, specify the queryMap subscript and return directly
    //This means that once the Map is used as the RequestParam, other requestparams will be ignored and the parameters in the Map will be directly parsed as the RequestParam
    if (Map.class.isAssignableFrom(parameterType)) {
        checkState(data.queryMapIndex() == null, "Query map can only be present once.");
        data.queryMapIndex(parameterIndex);
        //Return resolution success
        return true;
    }
    RequestParam requestParam = ANNOTATION.cast(annotation);
    String name = requestParam.value();
    //The name of RequestParam cannot be empty
    checkState(emptyToNull(name) != null, "RequestParam.value() was empty on parameter %s", parameterIndex);
    context.setParameterName(name);
    
    Collection<String> query = context.setTemplateParameter(name, data.template().queries().get(name));
    //Put RequestParam into MethodMetadata
    data.template().query(name, query);
    //Return resolution success
    return true;
}

We can also implement AnnotatedParameterProcessor to define our annotations and use them together with spring MVC annotations to define FeignClient

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

Topics: spring-cloud