声明式服务调用:Spring Cloud Feign

1.使用方法

   1.消费方基于spring mvc 注解自定义访问接口,这样相当于消费方和服务提供方各自维护一套接口。

  2.基于继承的方法,服务提供方将对外接口打成单独的jar包提供出去,消费方依赖这个jar就可以了。缺点是服务提供方和消费方在构建期间就建立起依赖,修改接口的时候需要注意兼容性。

  例:服务提供方提供接口

public interface FeignController {
    @RequestMapping("/hello")
    String hello();

    @RequestMapping(value = "/hello1", method = RequestMethod.GET)
    String hello(@RequestParam("name") String name);

    @RequestMapping(value = "/hello2", method = RequestMethod.PUT)
    String hello(@RequestHeader("name") String name, @RequestHeader("age") Integer age);

    @RequestMapping(value = "/hello3", method = RequestMethod.POST)
    User hello(@RequestBody User user);
}
    消费方继承接口,添加注解

@FeignClient(name = "feign-service", fallback = TestServiceFallback.class)
public interface TestService extends FeignController {

}

上面还设置了fallback,其定义如下:

@Component
public class TestServiceFallback implements TestService {
    @Override
    public String hello() {
        return "error";
    }

    @Override
    public String hello(String s) {
        return "error";
    }

    @Override
    public String hello(String s, Integer integer) {
        return "未知";
    }

    @Override
    public User hello(User user) {
        return new User("未知", 0);
    }
}

2.配置

  Feign是基于ribbon的,所以其配置和ribbon一致。
  全局配置:ribbon.<key>=<value>    
  指定服务配置:<client>.ribbon.<key>=<value>
 
#全局配置
ribbon.ConnectTimeout=1000
ribbon.ReadTimeout=2000
#断路器相关配置
feign.hystrix.enabled=true
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=100000
#hystrix.command.default.execution.timeout.enabled=false 是否开启断路器的超时设置
#如果要单独设置某个方法,则将default改为对应的方法名即可 如hystrix.command.hello.execution..
#注意:相同方法名的Hystrix配置会公用

#设置对应服务的配置
feign-service.ribbon.ConnectTimeout=2
feign-service.ribbon.ReadTimeout=10

#错误重试相关配置
spring.cloud.loadbalancer.retry.enabled=true
feign-service.ribbon.MaxAutoRetries=5
feign-service.ribbon.OkToRetryOnAllOperations=true
feign-service.ribbon.maxAutoRetriesNextServer=1
  

3.日志

格式:logging.level.<FeignClient>=
logging.level.com.feiniu.feign.TestService=debug
只添加上面的配置还是无法实现debug日志的输出。这是由于Fegin客户端默认的Logger.Level 对象为NONE级别,该级别不会记录任何Feign调用过程信息,我们需要调整级别。
全局定义:
@Bean
Logger.Level feignLoggerLevel() {
    return Logger.Level.BASIC;
}
局部定义:
@Configuration
public class FeignFullLogConfiguration {

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}
指定配置
@FeignClient(name = "feign-service", fallback = TestServiceFallback.class,
        configuration = FeignFullLogConfiguration.class
)
public interface TestService extends FeignController {

}


NONE:不记录任何信息
BASIC:仅记录请求方法、URL以及响应状态码和执行时间
HEADERS:BASIC信息加上请求和响应的头信息
FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据等。

4.原理

@EnableFeignClients 注解导入了FeignClientsRegistrar,看到registerFeignClients方法有如下代码

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);
      }
   }
}
可以看到此方法找到有FeignClient 注解的接口,然后进行注册
继续看registerFeignClient方法

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);
}
我们看这句代码
BeanDefinitionBuilder definition = BeanDefinitionBuilder
      .genericBeanDefinition(FeignClientFactoryBean.class);
FeignClientFactoryBean 很明显是一个FactoryBean实现,我们看它的 getObject()实现
if (!StringUtils.hasText(this.url)) {
   String url;
   if (!this.name.startsWith("http")) {
      url = "http://" + this.name;
   }
   else {
      url = this.name;
   }
   url += cleanPath();
   return loadBalance(builder, context, new HardCodedTarget<>(this.type,
         this.name, url));
}

继续看loadBalance方法

protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
      HardCodedTarget<T> target) {
   Client client = getOptional(context, Client.class);
   if (client != null) {
      builder.client(client);
      Targeter targeter = get(context, Targeter.class);
      return targeter.target(this, builder, context, target);
   }

   throw new IllegalStateException(
         "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-ribbon?");
}

打个断点看到这里targeter是HystrixTargeter,我们继续看HystrixTargeter的target方法

@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
               Target.HardCodedTarget<T> target) {
   if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
      return feign.target(target);
   }
   feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
   SetterFactory setterFactory = getOptional(factory.getName(), context,
      SetterFactory.class);
   if (setterFactory != null) {
      builder.setterFactory(setterFactory);
   }
   Class<?> fallback = factory.getFallback();
   if (fallback != void.class) {
      return targetWithFallback(factory.getName(), context, target, builder, fallback);
   }
   Class<?> fallbackFactory = factory.getFallbackFactory();
   if (fallbackFactory != void.class) {
      return targetWithFallbackFactory(factory.getName(), context, target, builder, fallbackFactory);
   }

   return feign.target(target);
}


看其中任意一个分支会看到InvocationHandler的一个实现最终都会调用

return dispatch.get(method).invoke(args);
打开断点,会看到使用的是SynchronousMethodHandler 我们看invoke方法

@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方法

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);
}

其中client是LoadBalancerFeignClient,继续看execute方法

@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方法

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);
    }
}
reconstructURIWithServer将服务名换成真实的服务地址。最终我们看到调用的是这个方法
AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig)
实际调用的是RetryableFeignLoadBalancer的execute方法
return retryTemplate.execute(new RetryCallback<RibbonResponse, IOException>() {
   @Override
   public RibbonResponse doWithRetry(RetryContext retryContext) throws IOException {
      Request feignRequest = null;
      //on retries the policy will choose the server and set it in the context
      //extract the server and update the request being made
      if(retryContext instanceof LoadBalancedRetryContext) {
         ServiceInstance service = ((LoadBalancedRetryContext)retryContext).getServiceInstance();
         if(service != null) {
            feignRequest = ((RibbonRequest)request.replaceUri(reconstructURIWithServer(new Server(service.getHost(), service.getPort()), request.getUri()))).toRequest();
         }
      }
      if(feignRequest == null) {
         feignRequest = request.toRequest();
      }
      Response response = request.client().execute(feignRequest, options);
      if(retryPolicy.retryableStatusCode(response.status())) {
         response.close();
         throw new RetryableStatusCodeException(RetryableFeignLoadBalancer.this.getClientName(), response.status());
      }
      return new RibbonResponse(request.getUri(), response);
   }
});
我们看此段代码
 Response response = request.client().execute(feignRequest, options);
request是RibbonRequest,它的client是什么呢,我们看默认的配置类
@Configuration
class DefaultFeignLoadBalancedConfiguration {
   @Bean
   @ConditionalOnMissingBean
   public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
                       SpringClientFactory clientFactory) {
      return new LoadBalancerFeignClient(new Client.Default(null, null),
            cachingFactory, clientFactory);
   }
}

new Client.Default(null, null)

所以最终是通过Client.Default的execute方法进行请求的

@Override
public Response execute(Request request, Options options) throws IOException {
  HttpURLConnection connection = convertAndSend(request, options);
  return convertResponse(connection).toBuilder().request(request).build();
}

最后我们还看到几个Client的扩展。

基于ApacheHttpClient

@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
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);
   }
}
基于OkHttpClient

@Configuration
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnProperty(value = "feign.okhttp.enabled", matchIfMissing = true)
class OkHttpFeignLoadBalancedConfiguration {

   @Autowired(required = false)
   private okhttp3.OkHttpClient okHttpClient;

   @Bean
   @ConditionalOnMissingBean(Client.class)
   public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
                       SpringClientFactory clientFactory) {
      OkHttpClient delegate;
      if (this.okHttpClient != null) {
         delegate = new OkHttpClient(this.okHttpClient);
      } else {
         delegate = new OkHttpClient();
      }
      return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
   }
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值