spring cloud之消费方使用Feign接口服务及其原理-4

前言

前面用ribbon实现服务调用,代码比较麻烦,需要先用loadBalancerClient获取一个serviceInstance,然后组装Url,再用restTemplate调用,那么用Feign可以直接定义接口进行调用

一、 Feign基本使用方法

1、引入依赖

<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2、定义一个接口,里面定义所有需要调用的服务接口

//服务名:PROVIDER-PRODUCT
@FeignClient(name = "PROVIDER-PRODUCT",configuration = FeignClientConfig.class)
public interface IProductClientService {
    @RequestMapping("/prodcut/get/{id}")
    public Product getProduct(@PathVariable("id")long id);

    @RequestMapping("/prodcut/add")
    public boolean addPorduct(Product product) ;

}

3、调用

@RestController
@RequestMapping("/consumer")
public class ConsumerProductController {
    @Resource
    private IProductClientService iProductClientService;

    @RequestMapping("/product/get")
    public Object getProduct(long id) {
        return  iProductClientService.getProduct(id);
    }
}

4、启动类上加上注解

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients("cn.clokc.service")
public class ConsumerFeignApp {
}

二、 Feign原理解析

1、引入spring-cloud-starter-openfeign组件

这个pom中引入,

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-openfeign-core</artifactId>
</dependency>

在这里插入图片描述
在这里插入图片描述
1.1、FeignAutoConfiguration对象
这个对象是一个注解类,实例化了FeignContext对象

@Bean
public FeignContext feignContext() {
	FeignContext context = new FeignContext();
	context.setConfigurations(this.configurations);
	return context;
}
//FeignContext类
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
    //构造方法
	public FeignContext() {
		super(FeignClientsConfiguration.class, "feign", "feign.client.name");
	}

}

1.2、FeignContext的父类NamedContextFactory
NamedContextFactory会创建出AnnotationConfigApplicationContext实例,并以name作为唯一标识,然后每个AnnotationConfigApplicationContext实例都会注册部分配置类,从而可以给出一系列的基于配置类生成的组件实例,这样就可以基于name来管理一系列的组件实例,为不同的 FeignClient准备不同配置组件实例

2、@EnableFeignClients注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})
public @interface EnableFeignClients {
}

2.1、Import导入了FeignClientsRegistrar 对象

,FeignClientsRegistrar这个对象实现了ImportBeanDefinitionRegistrar接口

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
		ResourceLoaderAware, EnvironmentAware {
	@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		//从EnableFeignClients的属性值来构建Feign的Configuration
		registerDefaultConfiguration(metadata, registry);
		//扫描package,注册被@FeignClient修饰的接口类Bean的信息
		registerFeignClients(metadata, registry);
	}
}

2.2、初始化配置方法registerDefaultConfiguration

这个方法主要注入了一个FeignClientSpecification对象,
FeignClientSpecification实现了NamedContextFactory.Specification接口,它是Feign实例化的重要一环,在上面的方法中,它持有自定义配置的组件实例,Springcloud使用NamedContextFactory创建一些列的运行上下文(ApplicationContext)来让对应的Specification在这些上下文中创建实例对象

NamedContextFactory有3个功能
1.创建AnnotationConfigApplicationContext上下文
2.在上下文中创建并获取bean实例
3.当上下文销毁时清除其中的feign实例

2.3、registerFeignClients方法

扫描@FeignClient 注解的接口下定义的接口
registerFeignClients方法:

//对单独的某个FeignClient的configuration进行配置
registerClientConfiguration(registry, name,attributes.get("configuration"));
//注册
registerFeignClient(registry, annotationMetadata, attributes);

2.4、registerFeignClient方法

这个方法注入了FeignClientFactoryBean这个对象

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

2.5、FeignClientFactoryBean这个对象

这是一个工厂类,实现了FactoryBean这个接口,那么通过getObject方法会注入实际的bean

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
		ApplicationContextAware {
}

2.6、FeignClientFactoryBean.getObject方法

@Override
public Object getObject() throws Exception {
	FeignContext context = applicationContext.getBean(FeignContext.class);
	Feign.Builder builder = feign(context);

	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));
	}
	if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
		this.url = "http://" + this.url;
	}
	String url = this.url + cleanPath();
	Client client = getOptional(context, Client.class);
	if (client != null) {
		if (client instanceof LoadBalancerFeignClient) {
			// not lod balancing because we have a url,
			// but ribbon is on the classpath, so unwrap
			client = ((LoadBalancerFeignClient)client).getDelegate();
		}
		builder.client(client);
	}
	Targeter targeter = get(context, Targeter.class);
	return targeter.target(this, builder, context, new HardCodedTarget<>(
			this.type, this.name, url));
}

2.7、argeter.target方法: DefaultTargeter实现

class DefaultTargeter implements Targeter {

	@Override
	public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
						Target.HardCodedTarget<T> target) {
		return feign.target(target);
	}
}

public <T> T target(Target<T> target) {
      return build().newInstance(target);
    }

//Feign.Builder负责生成被@FeignClient修饰的FeignClient接口类实例,它通过JAVA反射机制构建
    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);
    }
  }

feign.ReflectiveFeign#newInstance
这方法主要做两件事、
1.扫描FeignClient接口类的所有函数,生成对应的Handler
2.使用Proxy生成FeignClient的实例对象

3、网络请求

使用FeignClient接口类的实例,调用它的函数来发送网络请求,

发送网络请求可以分为3个阶段
1.是将函数实际参数添加到RequestTemplate中
2.调用Target生成具体的Request对象
3.调用Client来发送网络请求,将Response转化为对象返回
feign.SynchronousMethodHandler#invoke

@Override
  public Object invoke(Object[] argv) throws Throwable {
 //根据函数参数创建RequestTemplate 
    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;
      }
    }
  }

3.1、feign添加拦截器,在用feign调用接口接口前,可实现统一拦截

Object executeAndDecode(RequestTemplate template) throws Throwable {
    //这里可以添加fegin的拦截,比如调feign接口前 需要构造token,那么可以统一在这个拦截器里实现,这里会将所有拦截器添加到要调用restTemplate请求中
    Request request = targetRequest(template);
    //调用LoadBalancerFeignClient#execute
     response = client.execute(request, options);
      // ensure the request is set. TODO: remove in Feign 10
      response.toBuilder().request(request).build();
}

LoadBalancerFeignClient#execute方法:

URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
//feign整合了ribbon,用ribbon实现客户端负载
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
		this.delegate, request, uriWithoutHost);
//获取客户端默认超时配置,ConnectTimeout -> 1000,ReadTimeout -> 1000
IClientConfig requestConfig = getClientConfig(options, clientName);
//executeWithLoadBalancer这个方法中会根据服务名选择一个服务返回ip
return 
lbClient(clientName).executeWithLoadBalancer(ribbonRequest,
		requestConfig).toResponse();

3.2、feign的超时配置会被ribbon超时配置覆盖

最后会执行这个方法,中间方法省略FeignLoadBalancer#execute

@Override
public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
		throws IOException {
	Request.Options options;
	//是否有配置覆盖,这里读的是ribbon客户端的超时配置,如果有那么就覆盖了feign的超时配置
	//ribbon.ReadTimeout = 10000
//ribbon.ConnectTimeout = 10000
	if (configOverride != null) {
		RibbonProperties override = RibbonProperties.from(configOverride);
		options = new Request.Options(
				override.connectTimeout(this.connectTimeout),
				override.readTimeout(this.readTimeout));
	}
	else {
	    //this.connectTimeout,this.readTimeout是feign配置的超时时间,通过:前缀feign.client配置
	    //feign.client.config.default.connectTimeout = 5000
//feign.client.config.default.readTimeout = 5000
		options = new Request.Options(this.connectTimeout, this.readTimeout);
	}
	//发送http请求Default#execute#convertAndSend
	Response response = request.client().execute(request.toRequest(), options);
	return new RibbonResponse(request.getUri(), response);
}

3.3、ribbon在哪里根据服务列表选择一个服务返回?

在这里插入图片描述
在这里插入图片描述

 Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);  

总结:
总结FeignClientsRegistrar方法会依据@EnableFeignClients的属性获取要扫描的包路径信息,然后获取这些包下被@FeignClient注解修饰的接口的BeanDefinition

FeignClientFactoryBean 是工厂类,Spring容器通过调用它的getObject方法来获取对应的bean实例,被@FeignClient修饰的接口都是通过FeignClientFactoryBean 的getObject方法来进行实例化的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值