一個成熟的微服務集群,內部調用必然依賴一個好的RPC框架,比如:基於http協議的feign,基於私有tcp協議的dubbo。本文內容介紹feign。
一、What?
如果不使用rpc框架,那么調用服務需要走http的話,配置請求head、body,然后才能發起請求。獲得響應體后,還需解析等操作,十分繁瑣。
Feign是一個http請求調用的輕量級框架,可以以Java接口注解的方式調用Http請求。Feign通過處理注解,將請求模板化,當實際調用的時候,傳入參數,根據參數再應用到請求上,進而轉化成真正的請求,封裝了http調用流程。
二、How?
feign底層基於http協議,適應絕大部分內外部API調用的應用場景,並且SpringCloud對feign已經有了比較好的封裝。使用上可以依賴於SpringCloud封裝過的feign:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Feign在默認情況下使用的是JDK原生的URLConnection發送HTTP請求,沒有連接池,但是對每個地址會保持一個長連接,即利用HTTP的
persistence connection。建議替換為Apache HttpClient,作為底層的http client包,從而獲取連接池、超時時間等與性能息息相關的控制能力:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
在配置文件中啟用ApacheHttpClient:
feign.httpclient.enabled=true
FeignClient參數:
public @interface FeignClient {
@AliasFor("name")
String value() default "";
/** @deprecated */
@Deprecated
String serviceId() default "";
String contextId() default "";
// 指定FeignClient的名稱
@AliasFor("value")
String name() default "";
String qualifier() default "";
// 全路徑地址或hostname,http或https可選
String url() default "";
// 當發生http 404錯誤時,如果該字段位true,會調用decoder進行解碼,否則拋出FeignException
boolean decode404() default false;
// Feign配置類,可以自定義Feign的LogLevel
Class<?>[] configuration() default {};
// 容錯的處理類,當調用遠程接口失敗或超時時,會調用對應接口的容錯邏輯
Class<?> fallback() default void.class;
// 工廠類,用於生成fallback類實例,通過這個屬性我們可以實現每個接口通用的容錯邏輯,減少重復的代碼
Class<?> fallbackFactory() default void.class;
// 定義當前FeignClient的統一前綴,類似於controller類上的requestMapping
String path() default "";
boolean primary() default true;
}
三、Why?
- 啟動時,程序會進行包掃描,掃描所有包下所有@FeignClient注解的類,並將這些類注入到spring的IOC容器中。
- 當定義的Feign中的接口被調用時,通過JDK的動態代理來生成RequestTemplate。RequestTemplate中包含請求的所有信息,如請求參數,請求URL等。
- RequestTemplate聲場Request,然后將Request交給client處理,client默認是JDK的HTTPUrlConnection,也可以是OKhttp、Apache的HTTPClient等。
- 最后client封裝成LoadBaLanceClient,結合ribbon負載均衡地發起調用。
使用Feign涉及兩個注解:@EnableFeignClients,用來開啟Feign;@FeignClient,標記要用Feign來攔截的請求接口。
1、啟用
啟動配置上檢查是否有@EnableFeignClients注解,如果有該注解,則開啟包掃描,掃描被@FeignClient注解的接口。掃描出該注解后,
通過beanDefinition注入到IOC容器中,方便后續被調用使用。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] defaultConfiguration() default {};
Class<?>[] clients() default {};
}
@EnableFeignClients 是關於注解掃描的配置,使用了@Import(FeignClientsRegistrar.class)。在spring context處理過程中,這個Import會在解析Configuration的時候當做提供了其他的bean definition的擴展,Spring通過調用其registerBeanDefinitions方法來獲取其提供的bean definition。
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
}
FeignClientsRegistrar里重寫了spring里ImportBeanDefinitionRegistrar接口的registerBeanDefinitions方法。也就是在啟動時,處理了EnableFeignClients注解后,registry里面會多出一些關於Feign的BeanDefinition。
2、發起請求
ReflectiveFeign內部使用了jdk的動態代理為目標接口生成了一個動態代理類,這里會生成一個InvocationHandler統一的方法攔截器,同時為接口的每個方法生成一個SynchronousMethodHandler攔截器,並解析方法上的元數據,生成一個http請求模板RequestTemplate。
public class ReflectiveFeign extends Feign {
@Override
public <T> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
for (Method method : target.type().getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
} else if (Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler);
for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
}
Feign內置了一個重試器,當HTTP請求出現IO異常時,Feign會有一個最大嘗試次數發送請求:
final class SynchronousMethodHandler implements MethodHandler {
@Override
public Object invoke(Object[] argv) throws Throwable {
// 根據輸入參數,構造Http請求
RequestTemplate template = buildTemplateFromArgs.create(argv);
// 克隆出一份重試器
Retryer retryer = this.retryer.clone();
// 嘗試最大次數,如果中間有結果,直接返回
while (true) {
try {
return executeAndDecode(template);
} catch (RetryableException e) {
try {
retryer.continueOrPropagate(e);
} catch (RetryableException th) {
Throwable cause = th.getCause();
if (propagationPolicy == UNWRAP && cause != null) {
throw cause;
} else {
throw th;
}
}
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
}
Feign真正發送HTTP請求是委托給feign.Client來做的:
public interface Client {
Response execute(Request request, Options options) throws IOException;
class Default implements Client {
@Override
public Response execute(Request request, Options options) throws IOException {
HttpURLConnection connection = convertAndSend(request, options);
return convertResponse(connection, request);
}
}
}
默認底層通過JDK的java.net.HttpURLConnection實現了feign.Client接口類。在每次發送請求的時候,都會創建新的HttpURLConnection鏈接,這樣的話默認情況下Feign的性能很差,一般擴展該接口,比如使用Apache的HttpClient或者OkHttp3等基於連接池的高性能Http客戶端。
注意:SynchronousMethodHandler並不是直接完成遠程URL的請求,而是通過負載均衡機制,定位到合適的遠程server服務器,然后再完成真正的遠程URL請求。即:SynchronousMethodHandler實例的client成員,其實際不是feign.Client.Default類型,而是LoadBalancerFeignClient客戶端負載均衡類型。
3、性能分析
Feign框架比較小巧,在處理請求轉換和消息解析的過程中,基本上沒什么時間消耗。真正影響性能的,是處理Http請求的環節。可以從這個方面着手分析系統的性能提升點。