声明式服务调用feign解析

本文详细解析了Feign实现声明式服务调用的流程,从@EnableFeignClients注解开始,通过动态代理创建目标接口的代理实例,结合SpringMvcContract处理请求信息,最终利用 Ribbon 进行负载均衡,实现高效的服务间调用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

之前的文章已经分析过了ribbon的流程,如果我们只使用ribbon来进行服务调用的话,就需要依赖RestTemplate,看起来可能是这样的

@RequestMapping(value = "/hello/{name}", method = RequestMethod.GET)
public String greeting(@PathVariable("name") String name) {
   RestTemplate restTemplate = getRestTemplate();
   return restTemplate.getForObject("http://ServiceName/sayHello/" + name, String.class);
}

这就跟我们平时使用Spring MVC的方式有点不太一样,感觉是不是有些别扭,你说我就想自动注入一个Service,然后在接口调用的时候直接使用 服务名.方法名() 的方式来调用,借助feign,就可以实现声明式的服务调用

具体实现起来也很简单,我相信大家应该也不需要我从feign的使用来一点点讲了,简单来说就是我们的启动类上需要加上@EnableFeignClients注解,然后添加一个接口,接口需要使用@FeignClient注解,然后直接调用这个接口里面的方法就可以实现我们所希望的声明式服务调用了

接下来就是具体的流程分析了,会贴出一部分的源码,如果不习惯看大段的源码可以直接跳到最后看总结

首先我们的入口是@EnableFeignClients注解,这个注解标注在启动类上,顾名思义就是启用@FeignClient

@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

}

不重要的代码我都已经省略了,重点就是上面@Import中的FeignClientsRegistrar类,里面有一个方法叫做registerBeanDefinitions

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
		BeanDefinitionRegistry registry) {
	registerDefaultConfiguration(metadata, registry);
	registerFeignClients(metadata, registry);
}

上面那个registerDefaultConfiguration方法看方法名猜测可能是注册默认的配置信息,不是我们所关注的,所以直接跳过,接下来是registerFeignClients这个方法,代码有点多,我这边就不贴出来了,主要是扫描@EnableFeignClients注解中的属性(一般这里面也不怎么配置)和@FeignClient注解(这里面有很多属性可以配置,一般来说name属性是会配置的,也就是标记所要调用接口的服务名)

registerFeignClients方法走完之后会调用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);

	// 省略代码
}

通过上面的代码可以发现创建了一个BeanDefinitionBuilder,使用构造器模式创建了一个definition,这个definition就是@FeignClient注解标注的类的动态代理,里面加入了各种@FeignClient加入的属性,那么到底是如何构造出一个动态代理的呢?

关键就在FeignClientFactoryBean这个类中,我们直接来看FeignClientFactoryBean类的getObject方法

@Override
public Object getObject() throws Exception {
	FeignContext context = applicationContext.getBean(FeignContext.class);
	// 省略代码
	Targeter targeter = get(context, Targeter.class);
	return targeter.target(this, builder, context, new HardCodedTarget<>(
			this.type, this.name, url));
}

省略了中间的大段逻辑(顺便一提,中间省略代码是对url的处理,如果你配置了@FeignClient注解的url属性,那么就会按你配置的url请求,否则的话会通过ribbon来进行负载均衡选择url去发送请求)

重点就是最后两句,使用FeignContext创建了一个动态代理,跟进targeter.target的方法里面,最后会进入ReflectiveFeign的newInstance方法,里面就会根据jdk的动态代理生成一个我们需要调用接口的一个动态代理FeignInvocationHandler,之后的请求都是由这个动态代理来处理

@Override
public <T> T newInstance(Target<T> target) {
  Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
  Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
  List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

  for (Method method : target.type().getMethods()) {
    if (method.getDeclaringClass() == Object.class) {
      continue;
    } else if(Util.isDefault(method)) {
      DefaultMethodHandler handler = new DefaultMethodHandler(method);
      defaultMethodHandlers.add(handler);
      methodToHandler.put(method, handler);
    } else {
      methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
    }
  }
  InvocationHandler handler = factory.create(target, methodToHandler);
  T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);

  for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
    defaultMethodHandler.bindTo(proxy);
  }
  return proxy;
}

在ReflectiveFeign的newInstance方法中有一行非常重要的方法

Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);

这个apply方法中会解析出我们的请求信息,那么他是如何做到的呢?

 public Map<String, MethodHandler> apply(Target key) {
     List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type());
     // 省略代码
 }

这里的contract是SpringMvcContract,这个组件负责扫描我们在接口上定义的各种Spring MVC注解,打个比方来说,假如我们的请求方法如下

@RequestMapping(value = "/hello/{name}", method = RequestMethod.GET)
String hello(@PathVariable("name") String name);

他会解析出来三块内容

  1. 方法的定义:InterfaceName# hello(String),也就是接口名称#方法名(参数类型)
  2. 方法的返回类型:class java.lang.String
  3. 发送HTTP请求的模板:GET /hello/{name} HTTP/1.1

通过这些信息,我们就可以构造出真正的url来请求了

当我们获得了url信息之后,再回过头到我们得到的动态代理,FeignInvocationHandler的invoke方法就是动态代理的处理逻辑

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  if ("equals".equals(method.getName())) {
    try {
      Object
          otherHandler =
          args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
      return equals(otherHandler);
    } catch (IllegalArgumentException e) {
      return false;
    }
  } else if ("hashCode".equals(method.getName())) {
    return hashCode();
  } else if ("toString".equals(method.getName())) {
    return toString();
  }
  return dispatch.get(method).invoke(args);
}

dispatch.get(method).invoke(args)的实现交给了SynchronousMethodHandler类的invoke方法

@Override
public Object invoke(Object[] argv) throws Throwable {
  // 将方法的入参绑定到我们获得的url信息中,此时才是一个完整的url
  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(template)这个方法就是实际的执行请求的方法

Object executeAndDecode(RequestTemplate template) throws Throwable {
  // 拦截请求并生成一个Request用来进行请求
  Request request = targetRequest(template);

  Response response;
  long start = System.nanoTime();
  try {
    // 基于LoadBalancerFeignClient完成请求的发送
    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);

  // 返回响应的处理逻辑
}

到这里为止,请求就被真正发送到了接口上去了,并且也返回了响应,响应会使用feign的Decoder组件进行反序列化之类的工作,这里就不展开了

最后来画图总结一下feign的整个流程
feign的工作流程
简单来说,feign的工作流程其实就是通过扫描@FeignClient注解,找到所有的需要发送请求的接口,然后生成相应接口的动态代理,之后通过SpringMvcContract组件扫描Spring MVC注解得到url的基础信息;
我们的动态代理,在真正的请求发起的时候,得到url中的请求参数,拼成一个完整的url,最后通过ribbon或者url属性将生成的Request请求通过http请求发送到相应的服务上,并且接收返回结果,这就完成了一次完整的声明式服务调用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值