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<TestRoute>{ 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達到重寫目的
@Configuration public 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) @RestController public 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,"http://eureka-client"); } @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管理 ,這一塊更為詳細的內容可以看 SpringIoc分析
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注解,將interface納入beanDefination
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); } } } }
接下來,我們需要找到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() < 300) { if (void.class == metadata.returnType()) { return null; } else { return decode(response); } } else if (decode404 && response.status() == 404) { return decoder.decode(response, metadata.returnType()); } else { throw errorDecoder.decode(metadata.configKey(), response); } } catch (IOException e) { if (logLevel != Logger.Level.NONE) { logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime); } throw errorReading(request, response, e); } finally { if (shouldClose) { ensureClosed(response.body()); } } }
這里的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文件加上:
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>RELEASE</version>
</dependency>
在配置文件上加上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); 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方法里,發現了如下代碼
// Use the load balancer Observable<T> o = (server == null ? selectServer() : Observable.just(server)) .concatMap(new Func1<Server, Observable<T>>() { }
這里的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);
關於這方面的具體內容,請參考 SpringCloud Ribbon的分析
以上,就是對feign的具體分析
