轉載請標明出處:
本文出自方誌朋的部落格
什麼是Feign
Feign是受到Retrofit,JAXRS-2.0和WebSocket的影響,它是一個jav的到http客戶端繫結的開源專案。 Feign的主要目標是將Java Http 客戶端變得簡單。Feign的原始碼地址:github.com/OpenFeign/f…
寫一個Feign
在我之前的博文有寫到如何用Feign去消費服務,文章地址:blog.csdn.net/forezp/arti… 。
現在來簡單的實現一個Feign客戶端,首先通過@FeignClient,客戶端,其中value為呼叫其他服務的名稱,FeignConfig.class為FeignClient的配置檔案,程式碼如下:
@FeignClient(value = "service-hi",configuration = FeignConfig.class)
public interface SchedualServiceHi {
@GetMapping(value = "/hi")
String sayHiFromClientOne(@RequestParam(value = "name") String name);
}複製程式碼
其自定義配置檔案如下,當然也可以不寫配置檔案,用預設的即可:
@Configuration
public class FeignConfig {
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(100, SECONDS.toMillis(1), 5);
}
}複製程式碼
檢視FeignClient註解的原始碼,其程式碼如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
@AliasFor("value")
String name() default "";
String url() default "";
boolean decode404() default false;
Class<?>[] configuration() default {};
Class<?> fallback() default void.class;
Class<?> fallbackFactory() default void.class;
}
String path() default "";
boolean primary() default true;複製程式碼
FeignClient註解被@Target(ElementType.TYPE)修飾,表示FeignClient註解的作用目標在介面上;
@Retention(RetentionPolicy.RUNTIME),註解會在class位元組碼檔案中存在,在執行時可以通過反射獲取到;@Documented表示該註解將被包含在javadoc中。
feign 用於宣告具有該介面的REST客戶端的介面的註釋應該是建立(例如用於自動連線到另一個元件。 如果功能區可用,那將是
用於負載平衡後端請求,並且可以配置負載平衡器
使用與偽裝客戶端相同名稱(即值)@RibbonClient 。
其中value()和name()一樣,是被呼叫的 service的名稱。
url(),直接填寫硬編碼的url,decode404()即404是否被解碼,還是拋異常;configuration(),標明FeignClient的配置類,預設的配置類為FeignClientsConfiguration類,可以覆蓋Decoder、Encoder和Contract等資訊,進行自定義配置。fallback(),填寫熔斷器的資訊類。
FeignClient的配置
預設的配置類為FeignClientsConfiguration,這個類在spring-cloud-netflix-core的jar包下,開啟這個類,可以發現它是一個配置類,注入了很多的相關配置的bean,包括feignRetryer、FeignLoggerFactory、FormattingConversionService等,其中還包括了Decoder、Encoder、Contract,如果這三個bean在沒有注入的情況下,會自動注入預設的配置。
- Decoder feignDecoder: ResponseEntityDecoder(這是對SpringDecoder的封裝)
- Encoder feignEncoder: SpringEncoder
- Logger feignLogger: Slf4jLogger
- Contract feignContract: SpringMvcContract
- Feign.Builder feignBuilder: HystrixFeign.Builder
程式碼如下:
@Configuration
public class FeignClientsConfiguration {
...//省略程式碼
@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() {
return new ResponseEntityDecoder(new SpringDecoder(this.messageConverters));
}
@Bean
@ConditionalOnMissingBean
public Encoder feignEncoder() {
return new SpringEncoder(this.messageConverters);
}
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}
...//省略程式碼
}複製程式碼
重寫配置:
你可以重寫FeignClientsConfiguration中的bean,從而達到自定義配置的目的,比如FeignClientsConfiguration的預設重試次數為Retryer.NEVER_RETRY,即不重試,那麼希望做到重寫,寫個配置檔案,注入feignRetryer的bean,程式碼如下:
@Configuration
public class FeignConfig {
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(100, SECONDS.toMillis(1), 5);
}
}複製程式碼
在上述程式碼更改了該FeignClient的重試次數,重試間隔為100ms,最大重試時間為1s,重試次數為5次。
Feign的工作原理
feign是一個偽客戶端,即它不做任何的請求處理。Feign通過處理註解生成request,從而實現簡化HTTP API開發的目的,即開發人員可以使用註解的方式定製request api模板,在傳送http request請求之前,feign通過處理註解的方式替換掉request模板中的引數,這種實現方式顯得更為直接、可理解。
通過包掃描注入FeignClient的bean,該原始碼在FeignClientsRegistrar類:
首先在啟動配置上檢查是否有@EnableFeignClients註解,如果有該註解,則開啟包掃描,掃描被@FeignClient註解介面。程式碼如下:
private void registerDefaultConfiguration(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
Map<String, Object> defaultAttrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
}
else {
name = "default." + metadata.getClassName();
}
registerClientConfiguration(registry, name,
defaultAttrs.get("defaultConfiguration"));
}
}複製程式碼
程式啟動後通過包掃描,當類有@FeignClient註解,將註解的資訊取出,連同類名一起取出,賦給BeanDefinitionBuilder,然後根據BeanDefinitionBuilder得到beanDefinition,最後beanDefinition式注入到ioc容器中,原始碼如下:
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
scanner.addIncludeFilter(annotationTypeFilter);
basePackages = getBasePackages(metadata);
}
else {
final Set<String> clientClasses = new HashSet<>();
basePackages = new HashSet<>();
for (Class<?> clazz : clients) {
basePackages.add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
}
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
@Override
protected boolean match(ClassMetadata metadata) {
String cleaned = metadata.getClassName().replaceAll("\\$", ".");
return clientClasses.contains(cleaned);
}
};
scanner.addIncludeFilter(
new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
}
for (String basePackage : basePackages) {
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(
FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
definition.addPropertyValue("url", getUrl(attributes));
definition.addPropertyValue("path", getPath(attributes));
String name = getName(attributes);
definition.addPropertyValue("name", name);
definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
String alias = name + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null
beanDefinition.setPrimary(primary);
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}複製程式碼
注入bean之後,通過jdk的代理,當請求Feign Client的方法時會被攔截,程式碼在ReflectiveFeign類,程式碼如下:
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;
}複製程式碼
在SynchronousMethodHandler類進行攔截處理,當被FeignClient的方法被攔截會根據引數生成RequestTemplate物件,該物件就是http請求的模板,程式碼如下:
@Override
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
return executeAndDecode(template);
} catch (RetryableException e) {
retryer.continueOrPropagate(e);
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}複製程式碼
其中有個executeAndDecode()方法,該方法是通RequestTemplate生成Request請求物件,然後根據用client獲取response。
Object executeAndDecode(RequestTemplate template) throws Throwable {
Request request = targetRequest(template);
...//省略程式碼
response = client.execute(request, options);
...//省略程式碼
}複製程式碼
Client元件
其中Client元件是一個非常重要的元件,Feign最終傳送request請求以及接收response響應,都是由Client元件完成的,其中Client的實現類,只要有Client.Default,該類由HttpURLConnnection實現網路請求,另外還支援HttpClient、Okhttp.
首先來看以下在FeignRibbonClient的自動配置類,FeignRibbonClientAutoConfiguration ,主要在工程啟動的時候注入一些bean,其程式碼如下:
@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@Configuration
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignRibbonClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
return new LoadBalancerFeignClient(new Client.Default(null, null),
cachingFactory, clientFactory);
}
}複製程式碼
在缺失配置feignClient的情況下,會自動注入new Client.Default(),跟蹤Client.Default()原始碼,它使用的網路請求框架為HttpURLConnection,程式碼如下:
@Override
public Response execute(Request request, Options options) throws IOException {
HttpURLConnection connection = convertAndSend(request, options);
return convertResponse(connection).toBuilder().request(request).build();
}複製程式碼
怎麼在feign中使用HttpClient,檢視FeignRibbonClientAutoConfiguration的原始碼
@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@Configuration
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignRibbonClientAutoConfiguration {
...//省略程式碼
@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
protected static class HttpClientFeignLoadBalancedConfiguration {
@Autowired(required = false)
private HttpClient httpClient;
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
ApacheHttpClient delegate;
if (this.httpClient != null) {
delegate = new ApacheHttpClient(this.httpClient);
}
else {
delegate = new ApacheHttpClient();
}
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}
}
...//省略程式碼
}複製程式碼
從程式碼@ConditionalOnClass(ApacheHttpClient.class)註解可知道,只需要在pom檔案加上HttpClient的classpath就行了,另外需要在配置檔案上加上feign.httpclient.enabled為true,從 @ConditionalOnProperty註解可知,這個可以不寫,在預設的情況下就為true.
在pom檔案加上:
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>RELEASE</version>
</dependency>複製程式碼
同理,如果想要feign使用Okhttp,則只需要在pom檔案上加上feign-okhttp的依賴:
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>RELEASE</version>
</dependency>複製程式碼
feign的負載均衡是怎麼樣實現的呢?
通過上述的FeignRibbonClientAutoConfiguration類配置Client的型別(httpurlconnection,okhttp和httpclient)時候,可知最終向容器注入的是LoadBalancerFeignClient,即負載均衡客戶端。現在來看下LoadBalancerFeignClient的程式碼:
@Override
public Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName).executeWithLoadBalancer(ribbonRequest,
requestConfig).toResponse();
}
catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}複製程式碼
其中有個executeWithLoadBalancer()方法,即通過負載均衡的方式請求。
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
RequestSpecificRetryHandler handler = getRequestSpecificRetryHandler(request, requestConfig);
LoadBalancerCommand<T> command = LoadBalancerCommand.<T>builder()
.withLoadBalancerContext(this)
.withRetryHandler(handler)
.withLoadBalancerURI(request.getUri())
.build();
try {
return command.submit(
new ServerOperation<T>() {
@Override
public Observable<T> call(Server server) {
URI finalUri = reconstructURIWithServer(server, request.getUri());
S requestForServer = (S) request.replaceUri(finalUri);
try {
return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
}
catch (Exception e) {
return Observable.error(e);
}
}
})
.toBlocking()
.single();
} catch (Exception e) {
Throwable t = e.getCause();
if (t instanceof ClientException) {
throw (ClientException) t;
} else {
throw new ClientException(e);
}
}
}複製程式碼
其中服務在submit()方法上,點選submit進入具體的方法,這個方法是LoadBalancerCommand的方法:
Observable<T> o =
(server == null ? selectServer() : Observable.just(server))
.concatMap(new Func1<Server, Observable<T>>() {
@Override
// Called for each server being selected
public Observable<T> call(Server server) {
context.setServer(server);
}}複製程式碼
上述程式碼中有個selectServe(),該方法是選擇服務的進行負載均衡的方法,程式碼如下:
private Observable<Server> selectServer() {
return Observable.create(new OnSubscribe<Server>() {
@Override
public void call(Subscriber<? super Server> next) {
try {
Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);
next.onNext(server);
next.onCompleted();
} catch (Exception e) {
next.onError(e);
}
}
});
}複製程式碼
最終負載均衡交給loadBalancerContext來處理,即之前講述的Ribbon,在這裡不再重複。
總結
總到來說,Feign的原始碼實現的過程如下:
- 首先通過@EnableFeignCleints註解開啟FeignCleint
- 根據Feign的規則實現介面,並加@FeignCleint註解
- 程式啟動後,會進行包掃描,掃描所有的@ FeignCleint的註解的類,並將這些資訊注入到ioc容器中。
- 當介面的方法被呼叫,通過jdk的代理,來生成具體的RequesTemplate
- RequesTemplate在生成Request
- Request交給Client去處理,其中Client可以是HttpUrlConnection、HttpClient也可以是Okhttp
- 最後Client被封裝到LoadBalanceClient類,這個類結合類Ribbon做到了負載均衡。