SpringCloud Feign的分析
Feign是一個宣告式的Web Service客戶端,它使得編寫Web Serivce客戶端變得更加簡單。我們只需要使用Feign來建立一個介面並用註解來配置它既可完成。
@FeignClient(value = "qrcodepay-dike-service")public interface TestRoute { @RequestMapping(value = "/dike/get", method = RequestMethod.GET) HdResult get(); }
我們只需要在相應的介面上新增@FeignClient註解即可將他宣告為一個web客戶端。這其中的原理我們後續分析。我們首先先關注下feign暴露的幾個配置。
value: 目標服務名,一般都是 application.name
fallback : 服務降級策略
@FeignClient(value = "qrcodepay-dike-service",fallback = TestRoute.TestRouteFaback.class) public interface TestRoute { @RequestMapping(value = "/dike/get", method = RequestMethod.GET) HdResult get(); }
@Component class TestRouteFaback implements TestRoute{ @Override public HdResult get() { return HdResult.makeFail("服務降級"); } }
fallbackFactory :fallback的升級版,可以獲取更加詳細的異常資訊
@FeignClient(value = "qrcodepay-dike-service",fallbackFactory = TestRoute.TestRouteFallbackFactory.class)public interface TestRoute { @RequestMapping(value = "/dike/get", method = RequestMethod.GET) HdResult get(); @Component class TestRouteFallbackFactory implements FallbackFactory{ private static final Logger logger = LoggerFactory.getLogger(TestRouteFallbackFactory.class); @Override public TestRoute create(Throwable throwable) { String msg = throwable == null ? "" : throwable.getMessage(); if (!StringUtils.isEmpty(msg)) { logger.error("異常資訊列印:{}",msg); } return new TestRoute() { @Override public HdResult get() { return HdResult.makeFail(msg); } }; } } }
configuration:重寫feign的配置
具體哪些內容可以配置我們可以看 FeignClientsConfiguration和feign.Feign.Builder。
下面用兩種方式重寫feign的配置
覆蓋原有的配置bean達到重寫目的
@Configurationpublic class FeignBreakerConfiguration { @Bean public ErrorDecoder errorDecoder() { return new UserErrorDecoder(); } /** * 自定義錯誤解碼器 只有返回http status 非200才會進入 */ public class UserErrorDecoder implements ErrorDecoder { private Logger logger = LoggerFactory.getLogger(getClass()); @Override public Exception decode(String methodKey, Response response) { Exception exception = null; try { String json = Util.toString(response.body().asReader()); System.out.println("自定義解碼:"+json); exception = new RuntimeException(json); HdResult result=HdResult.makeFail(json); // 業務異常包裝成 HystrixBadRequestException,不進入熔斷邏輯// if (!result.isSuccess()) {// exception = new HystrixBadRequestException(result.getMessage());// } } catch (IOException ex) { logger.error(ex.getMessage(), ex); } return exception; } } }
自定義客戶端達到重寫的目的
@Import(FeignClientsConfiguration.class) @RestControllerpublic class DefaultController { private FeignClientService feignClientService; public DefaultController(Decoder decoder, Encoder encoder, Client client){ this.feignClientService = Feign.builder().client(client) .encoder(encoder) .decoder(decoder) .requestInterceptor(new BasicAuthRequestInterceptor("user","password")) .target(FeignClientService.class,""); } @RequestMapping(name = "/default",method = RequestMethod.GET) public String getInfo(){ return feignClientService.getValue("hello world!"); } }
feignclient最常用的配置大致如上,接下來介紹下feign實現的原理。
先說結論,feign是透過動態代理的技術將一個interface變為Web Service客戶端。那我們應該從哪裡入手呢。在使用feign的時候,我們應該關注兩個註解,一個就是我們上文所說的feignClient,但是僅僅只用這個註解feign是不會生效的,必須要在啟動類上加上EnableFeignClients,feign才會自動掃描feignClient。所以我們的入口應該是 EnableFeignClients
EnableFeignClients 匯入了FeignClientsRegistrar,這個註解真正的邏輯就在FeignClientsRegistrar中
這個類實現了三個介面,我們先關注 ImportBeanDefinitionRegistrar,這是spring動態註冊bean的介面。所以spring在啟動的時候會呼叫以下方法
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { registerDefaultConfiguration(metadata, registry); registerFeignClients(metadata, registry); }
將配置類納入beandefinationMap管理 ,這一塊更為詳細的內容可以看
private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { MapdefaultAttrs = 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註解,將interface納入beanDefination
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { ClassPathScanningCandidateComponentProvider scanner = getScanner(); scanner.setResourceLoader(this.resourceLoader); SetbasePackages; Map 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 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 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 attributes = annotationMetadata .getAnnotationAttributes( FeignClient.class.getCanonicalName()); String name = getClientName(attributes); registerClientConfiguration(registry, name, attributes.get("configuration")); registerFeignClient(registry, annotationMetadata, attributes); } } } }
接下來,我們需要找到jdk代理的地方
我們在構建feign的地方發現如下方法
public Feign build() { SynchronousMethodHandler.Factory synchronousMethodHandlerFactory = new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger, logLevel, decode404); ParseHandlersByName handlersByName = new ParseHandlersByName(contract, options, encoder, decoder, errorDecoder, synchronousMethodHandlerFactory); return new ReflectiveFeign(handlersByName, invocationHandlerFactory); }
最終我們在SynchronousMethodHandler類中發現了真正攔截的程式碼
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; } } }
真正執行的邏輯如下,這裡也是feign最為關鍵的地方。這裡我們主要關注下真正請求的那一行。如果想對feign做debug或者重寫一些配置,參考這裡會是一個很好的入口。
Object executeAndDecode(RequestTemplate template) throws Throwable { Request request = targetRequest(template); if (logLevel != Logger.Level.NONE) { logger.logRequest(metadata.configKey(), logLevel, request); } Response response; long start = System.nanoTime(); try { response = client.execute(request, options); // ensure the request is set. TODO: remove in Feign 10 response.toBuilder().request(request).build(); } catch (IOException e) { if (logLevel != Logger.Level.NONE) { logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start)); } throw errorExecuting(request, e); } long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); boolean shouldClose = true; try { if (logLevel != Logger.Level.NONE) { response = logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime); // ensure the request is set. TODO: remove in Feign 10 response.toBuilder().request(request).build(); } if (Response.class == metadata.returnType()) { if (response.body() == null) { return response; } if (response.body().length() == null || response.body().length() > MAX_RESPONSE_BUFFER_SIZE) { shouldClose = false; return response; } // Ensure the response body is disconnected byte[] bodyData = Util.toByteArray(response.body().asInputStream()); return response.toBuilder().body(bodyData).build(); } if (response.status() >= 200 && response.status()這裡的client是請求客戶端,feign統一封裝為LoadBalancerFeignClient
@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); } }
預設的Client 是HttpURLConnection,同時 feign也支援httpclient和okhhtp
@Configuration @ConditionalOnClass(ApacheHttpClient.class) @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer") @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true) protected static class HttpClientFeignConfiguration { @Autowired(required = false) private HttpClient httpClient; @Bean @ConditionalOnMissingBean(Client.class) public Client feignClient() { if (this.httpClient != null) { return new ApacheHttpClient(this.httpClient); } return new ApacheHttpClient(); } } @Configuration @ConditionalOnClass(OkHttpClient.class) @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer") @ConditionalOnProperty(value = "feign.okhttp.enabled", matchIfMissing = true) protected static class OkHttpFeignConfiguration { @Autowired(required = false) private okhttp3.OkHttpClient okHttpClient; @Bean @ConditionalOnMissingBean(Client.class) public Client feignClient() { if (this.okHttpClient != null) { return new OkHttpClient(this.okHttpClient); } return new OkHttpClient(); } }只要滿足 配置條件,就可以將httpclient或okhhtp引入,這裡舉例說明怎麼使用httpclient
在pom檔案加上:
com.netflix.feign feign-httpclient RELEASE 在配置檔案上加上feign.httpclient.enabled為true(預設為true,可不寫)
最後,我們再看看feign是怎麼使用ribbon的,上文我們說過feign統一將client封裝為LoadBalancerFeignClient,fein的請求最終都會到以下程式碼
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); LoadBalancerCommandcommand = LoadBalancerCommand. builder() .withLoadBalancerContext(this) .withRetryHandler(handler) .withLoadBalancerURI(request.getUri()) .build(); try { return command.submit( new ServerOperation () { @Override public Observable 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方法裡,發現瞭如下程式碼
// Use the load balancer Observableo = (server == null ? selectServer() : Observable.just(server)) .concatMap(new Func1 >() { }
這裡的selectServer 最終會呼叫 ILoadBalancer 選擇一個server
ILoadBalancer lb = getLoadBalancer(); if (host == null) { // Partial URI or no URI Case // well we have to just get the right instances from lb - or we fall back if (lb != null){ Server svc = lb.chooseServer(loadBalancerKey);關於這方面的具體內容,請參考
以上,就是對feign的具體分析
原文出處:https://www.cnblogs.com/xmzJava/p/9612988.html
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2318/viewspace-2813833/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Springcloud(二) feignSpringGCCloud
- SpringCloud-FeignSpringGCCloud
- SpringCloud 2020.0.4 系列之 FeignSpringGCCloud
- SpringCloud——Feign例項及原理SpringGCCloud
- 分享一個 SpringCloud Feign 中所埋藏的坑SpringGCCloud
- 2018-03-29 SpringCloud Feign DecoderSpringGCCloud
- SpringCloud-使用Feign呼叫服務介面SpringGCCloud
- SpringCloud微服務治理二(Robbin,Hystix,Feign)SpringGCCloud微服務
- (22)SpringCloud-使用Feign呼叫服務介面SpringGCCloud
- springcloud學習筆記(二)Spring Cloud FeignSpringGCCloud筆記
- SpringCloud之服務提供與呼叫(Ribbon,Feign)SpringGCCloud
- springcloud中feign檔案上傳、下載SpringGCCloud
- Feign - 原始碼分析原始碼
- SpringCloud解決feign呼叫token丟失問題SpringGCCloud
- SpringCloud系列之使用Feign進行服務呼叫SpringGCCloud
- SpringCloud之使用Feign跨服務呼叫最佳方式SpringGCCloud
- [jaeger] 四、微服務之呼叫鏈(Feign+SpringCloud)微服務SpringGCCloud
- SpringCloud微服務(基於Eureka+Feign+Hystrix+Zuul)SpringGCCloud微服務Zuul
- SpringCloud學習筆記:宣告式呼叫Feign(4)SpringGCCloud筆記
- SpringCloud 原始碼學習筆記2——Feign宣告式http客戶端原始碼分析SpringGCCloud原始碼筆記HTTP客戶端
- SpringCloud學習之路(四) - 服務消費者(Feign)SpringGCCloud
- 跟我學SpringCloud | 第三篇:服務的提供與Feign呼叫SpringGCCloud
- SpringCloud基礎概念學習筆記(Eureka、Ribbon、Feign、Zuul)SpringGCCloud筆記Zuul
- 微服務實戰SpringCloud之Spring Cloud Feign替代HTTP Client微服務SpringGCCloudHTTPclient
- ②SpringCloud 實戰:引入Feign元件,發起服務間呼叫SpringGCCloud元件
- SpringCloud 通過feign檔案傳輸並打zip包下載SpringGCCloud
- Feign踩坑原始碼分析--@FeignClient注入容器原始碼client
- Feign的使用
- 企業分散式微服務雲SpringCloud SpringBoot mybatis - 服務消費者(Feign)分散式微服務GCCloudSpring BootMyBatis
- 還在用Feign?推薦一款微服務間呼叫神器,跟SpringCloud絕配!微服務SpringGCCloud
- java B2B2C Springcloud電子商城系統-Feign負載均衡JavaSpringGCCloud負載
- SpringCloud學習系列之二 ----- 服務消費者(Feign)和負載均衡(Ribbon)SpringGCCloud負載
- 白話SpringCloud | 第四章:服務消費者(RestTemple+Ribbon+Feign)SpringGCCloudREST
- 業餘草 SpringCloud教程 | 第三篇: 服務消費者(Feign)(Finchley版本)SpringGCCloud
- Feign的工作原理
- feign
- 記一次線上SpringCloud-Feign請求服務超時異常排查SpringGCCloud
- Feign系列