在使用雲原生的很多微服務中,比較小規模的可能直接依靠雲服務中的負載均衡器進行內部域名與服務映射,通過健康檢查接口判斷實例健康狀態,然后直接使用 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
,通過其構造函數,得到其中的默認配置類:
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 注解以及我們自定義的注解
定義 Feign Client 專用的 Formatter 與 Converter 注冊
首先,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;
}
我們也可以實現 AnnotatedParameterProcessor
來自定義我們的注解,配合 SpringMVC 的注解一起使用去定義 FeignClient
微信搜索“我的編程喵”關注公眾號,每日一刷,輕松提升技術,斬獲各種offer: