天天看點

SpringCloud更新之路2020.0.x版-29.SC OpenFeign 的解析(1)

SpringCloud更新之路2020.0.x版-29.SC OpenFeign 的解析(1)
本系列代碼位址:https://github.com/JoJoTec/spring-cloud-parent

在使用雲原生的很多微服務中,比較小規模的可能直接依靠雲服務中的負載均衡器進行内部域名與服務映射,通過健康檢查接口判斷執行個體健康狀态,然後直接使用 OpenFeign 生成對應域名的 Feign Client。Spring Cloud 生态中,對 OpenFeign 進行了封裝,其中的 Feign Client 的各個元件,也是做了一定的定制化,可以實作在 OpenFeign Client 中內建服務發現與負載均衡。在此基礎上,我們還結合了 Resilience4J 元件,實作了微服務執行個體級别的線程隔離,微服務方法級别的斷路器以及重試。

我們先來分析下 Spring Cloud OpenFeign

Spring Cloud OpenFeign 解析

從 NamedContextFactory 入手

Spring Cloud OpenFeign 的 github 位址:https://github.com/spring-cloud/spring-cloud-openfeign

首先,根據我們之前分析 spring-cloud-loadbalancer 的流程,我們先從繼承

NamedContextFactory

的類入手,這裡是

FeignContext

,通過其構造函數,得到其中的預設配置類:

FeignContext.java

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

從構造方法可以看出,預設的配置類是:

FeignClientsConfiguration

。我們接下來詳細分析這個配置類中的元素,并與我們之前分析的 OpenFeign 的元件結合起來。

負責解析類中繼資料的 Contract,與 spring-web 的 HTTP 注解相結合

為了開發人員更好上手使用和了解,最好能實作使用 spring-web 的 HTTP 注解(例如

@RequestMapping

@GetMapping

等等)去定義 FeignClient 接口。在

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

其核心提供的 Feign 的 Contract 就是

SpringMvcContract

SpringMvcContract

主要包含兩部分核心邏輯:

  • 定義 Feign Client 專用的 Formatter 與 Converter 注冊
  • 使用 AnnotatedParameterProcessor 來解析 SpringMVC 注解以及我們自定義的注解

首先,Spring 提供了類型轉換機制,其中單向的類型轉換為實作 Converter 接口;在 web 應用中,我們經常需要将前端傳入的字元串類型的資料轉換成指定格式或者指定資料類型來滿足我們調用需求,同樣的,後端開發也需要将傳回資料調整成指定格式或者指定類型傳回到前端頁面(在 Spring Boot 中已經幫我們做了從 json 解析和傳回對象轉化為 json,但是某些特殊情況下,比如相容老項目接口,我們還可能使用到),這個是通過實作 Formatter 接口實作。舉一個簡單的例子:

定義一個類型:

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

我們定義可以通過字元串解析出這個類的對象的 Converter,例如 "1,zhx" 就代表 id = 1 并且 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]);
    }
}           

然後将這個 Converter 注冊:

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

編寫一個測試接口:

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

調用

/test/string-to-student?student=1,zhx

,可以看到傳回:

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

同樣的,我們也可以通過 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();
    }
}           

然後将這個 Formatter 注冊:

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

Feign 也提供了這個注冊機制,為了和 spring-webmvc 的注冊機制區分開,使用了 FeignFormatterRegistrar 繼承了 FormatterRegistrar 接口。然後通過定義

FormattingConversionService

這個 Bean 實作 Formatter 和 Converter 的注冊。例如:

假設我們有另一個微服務需要通過 FeignClient 調用上面這個接口,那麼就需要定義一個 FeignFormatterRegistrar 将 Formatter 注冊進去:

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

之後我們定義 FeignClient:

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

在調用 get 方法時,會調用 StudentFormatter 的 print 将 Student 對象輸出為格式化的字元串,例如

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

會變成

1,zhx

AnnotatedParameterProcessor 來解析 SpringMVC 注解以及我們自定義的注解

AnnotatedParameterProcessor

是用來将注解解析成

AnnotatedParameterContext

的 Bean,

AnnotatedParameterContext

包含了 Feign 的請求定義,包括例如前面提到的 Feign 的

MethodMetadata

即方法中繼資料。預設的

AnnotatedParameterProcessor

包括所有 SpringMVC 對于 HTTP 方法定義的注解對應的解析,例如

@RequestParam

注解對應的

RequestParamParameterProcessor

RequestParamParameterProcessor.java

public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
    //擷取目前參數屬于方法的第幾個
    int parameterIndex = context.getParameterIndex();
    //擷取參數類型
    Class<?> parameterType = method.getParameterTypes()[parameterIndex];
    //要儲存的解析的方法中繼資料 MethodMetadata
    MethodMetadata data = context.getMethodMetadata();

    //如果是 Map,則指定 queryMap 下标,直接傳回
    //這代表一旦使用 Map 作為 RequestParam,則其他的 RequestParam 就會被忽略,直接解析 Map 中的參數作為 RequestParam
    if (Map.class.isAssignableFrom(parameterType)) {
        checkState(data.queryMapIndex() == null, "Query map can only be present once.");
        data.queryMapIndex(parameterIndex);
        //傳回解析成功
        return true;
    }
    RequestParam requestParam = ANNOTATION.cast(annotation);
    String name = requestParam.value();
    //RequestParam 的名字不能是空
    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));
    //将 RequestParam 放入 方法中繼資料 MethodMetadata
    data.template().query(name, query);
    //傳回解析成功
    return true;
}