前言
前面用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方法来进行实例化的。