在使用雲原生的很多微服務中,比較小規模的可能直接依靠雲服務中的負載均衡器進行內部域名與服務對映,通過健康檢查介面判斷例項健康狀態,然後直接使用 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: