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<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的具體分析

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM