OpenFeign核心知识小结

sharkchili 微服务基础 9073 字 👁 10 2023-01-04

什么是OpenFeign,它是解决什么问题呢?

Spring服务之间的调用若基于WebClient实现调用则会出现很多很业务无关的代码如下所示:

        String result = webClientBuilder.build()
                .get()
                //http://file 会被nacos等价替换成file的ip+端口号
               .uri("http://file/file/test/hello")
                .retrieve()
                .bodyToMono(String.class)
                .block();

所以OpenFeign就诞生了,我们只需一些简单的配置,即可实现服务之间的远程调用,改造完成后的代码如下所示,相对简洁了许多。

 return fileService.hello();

能不能给我演示一下OpenFeign的使用?

首先需要进行远程调用的服务必须引入下面这个依赖

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

然后启动类添加这个注解,注意basePackages 为包扫描路径

@EnableFeignClients(basePackages = {"com.course"})

完成后创建远程服务调用的接口,如下所示FeignClientvalue即远程调用服务注册到nacos上的服务名,path为请求通用路径,后续GetMapping这些则是具体请求地址了

@FeignClient(value = "file", path = "/file")
public interface FileService {


	//GetMapping指向调用地址
    @GetMapping("/test/hello")
    String hello();
}

完成上述配置后,我们将接口注入就行了。

  @Autowired
    private FileService fileService;

    @GetMapping("/test")
    public String hello() {

        return fileService.hello();
    }

OpenFeign包扫描原理了解嘛?

回答这个问题我们首先要从启动类上的注解说起EnableFeignClients这个注解@Import(FeignClientsRegistrar.class),由此可知这个注解会将FeignClientsRegistrar导入,所以我们不妨查看一下FeignClientsRegistrar

可以看到它有一个registerFeignClients方法,我们不妨步入看看它的核心流程:

@Override
	public void FeignClientsRegistrar(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		registerDefaultConfiguration(metadata, registry);
		//注册 FeignClient
		registerFeignClients(metadata, registry);
	}

源代码如下,可以看到在获取包路径之后就会拿到关于FeignClient注解的bean注入到Spring容器中,进行后续的IOC操作。

public void registerFeignClients(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		ClassPathScanningCandidateComponentProvider scanner = getScanner();
		scanner.setResourceLoader(this.resourceLoader);

		Set<String> basePackages;

		....略
		if (clients == null || clients.length == 0) {
			scanner.addIncludeFilter(annotationTypeFilter);
			//获取扫描的包路径
			basePackages = getBasePackages(metadata);
		}
		.....
			
		//遍历包路径
		for (String basePackage : basePackages) {
			//找到带有FeignClient注解的的BeanDefinition
			Set<BeanDefinition> candidateComponents = scanner
					.findCandidateComponents(basePackage);
			for (BeanDefinition candidateComponent : candidateComponents) {
				....
				
					registerClientConfiguration(registry, name,
							attributes.get("configuration"));
			//完成后将bean注册到Spring容器中
					registerFeignClient(registry, annotationMetadata, attributes);
				}
			}
		}
	}

Spring如何完成OpenFeign注册的?

上一个问题中源码包扫描获取到带有FeignClientbean之后,执行了一个registerFeignClient方法,这就是完成注册的关键,源码如下,可以看到完成上一步包扫描到FeignClient后会不断遍历将这些bean注册到容器中。

private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
		String className = annotationMetadata.getClassName();
		//创建FeignClient动态代理构造器
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientFactoryBean.class);
		validate(attributes);
		//给这个构建器设置当前bean的信息
		.....
		definition.addPropertyValue("type", className);


		//生成beanDefinition 
		AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

		//生成BeanDefinitionHolder 
		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
				new String[] { alias });
		//会将这些bean注入到Spring容器的beanDefinitionMap中
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}

OpenFeign是如何实现动态代理的

上一个问题的registerFeignClient方法,可以看到这样一个代码段,可以看到genericBeanDefinition时用到了FeignClientFactoryBean来创建一个BeanDefinition构建器。然后通过这个构建器在Spring后续IOC的逻辑完成依赖注入。

BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientFactoryBean.class);

所以我们不妨看看FeignClientFactoryBean源码,查看它是如何完成对象创建的:

可以看到它继承FactoryBean<Object>,并重写了bean工厂的getObject方法,我们不妨步入看看核心逻辑

@Override
	public Object getObject() throws Exception {
		return getTarget();
	}

可以看到getTarget通过feign方法获取一个构建器,然后通过loadBalance完成代理对象的创建。

<T> T getTarget() {
		FeignContext context = this.applicationContext.getBean(FeignContext.class);
		Feign.Builder builder = feign(context);

		if (!StringUtils.hasText(this.url)) {
			if (!this.name.startsWith("http")) {
				this.url = "http://" + this.name;
			}
			else {
				this.url = this.name;
			}
			this.url += cleanPath();
			return (T) loadBalance(builder, context,
					new HardCodedTarget<>(this.type, this.name, this.url));
		}
		.......略
	}

我们继续看看loadBalance逻辑,可以看到调用一个target生成代理对象,我们不妨步入看看

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);
			//调用target方法动态生成对象
			return targeter.target(this, builder, context, target);
		}

		.....略
	}

不断步入即可看到ReflectiveFeignnewInstance,实现对FeignClient中的每一个方法进行拦截,然后生成一个InvocationHandler ,从而完成代理对象的创建。

@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>();

	//遍历每个方法生成methodHandler然后存放到methodToHandler中
    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)));
      }
    }
    //拿着methodToHandler和目标类生成代理对象
    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;
  }

OpenFeign是如何如何解析MVC注解的知道吗?

我们上文提到了实现对FeignClientBeanDefinitionFeignClientFactoryBean构建器,在Spring IOC阶段,这个构建器的逻辑就开始工作了。

如下源码所示,IOC会在doGetBean时走到FeignClientFactoryBeangetTarget,

<T> T getTarget() {
		FeignContext context = this.applicationContext.getBean(FeignContext.class);
		Feign.Builder builder = feign(context);

		.......
			return (T) loadBalance(builder, context,
					new HardCodedTarget<>(this.type, this.name, this.url));
		}


}

然后不断步进,走到ReflectiveFeignnewInstance,有一个代码段targetToHandlersByName.apply(target),这就是解析Spring MVC注解的关键。我们不妨步入看看

public <T> T newInstance(Target<T> target) {
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);

}

可以看到这个方法内部会执行一个parseAndValidateMetadata

 public Map<String, MethodHandler> apply(Target target) {
      List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());

}

最终代码会走到SpringMvcContractparseAndValidateMetadata,它会完成对MVC注解的解析从而生成元数据

@Override
	public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
		this.processedMethods.put(Feign.configKey(targetType, method), method);
		MethodMetadata md = super.parseAndValidateMetadata(targetType, method);

}

完成元数据解析之后,ReflectiveFeign就会以FeignClient作为key,元数据作为value放到一个map

在这里插入图片描述

这个map就是后续创建动态代理的原料,参见ReflectiveFeignnewInstance

@Override
  public <T> T newInstance(Target<T> target) {
  //上文解析MVC注解的入口
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();

......

	//将MVC解析后的生成的nameToHandler 存到methodToHandler中
	methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
    //methodToHandler生成代理的handler 
    InvocationHandler handler = factory.create(target, methodToHandler);
    //创建动态代理对象
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
        new Class<?>[] {target.type()}, handler);

OpenFeign发送请求的过程了解嘛?

这个问题,我们不妨从调用处插个debug看看,以笔者为例fileService就是笔者创建的一个FeignClient,我们在fileService.hello()插个断点

 @Autowired
    private FileService fileService;

    @GetMapping("/test")
    public String hello() {
		//调用远程服务fileService
        return fileService.hello();
    }

执行调用时,代码走到的ReflectiveFeigninvoke方法,从而走到下面这段代码中。

 @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      .....

      return dispatch.get(method).invoke(args);
    }

从断点我们就可以看到dispatch中存档的就是存放的就是以类名+方法名为key,元数据各种元数据信息(上文解析MVC生成的数据metadata)组装而成的SynchronousMethodHandlervaluemap

通过dispatch.get(method)获取到对应的SynchronousMethodHandler,执行invoke方法。对此我们不妨步进看看invoke方法。

可以看到dispatch中存档的就是存放的就是以类名+方法名为key,元数据各种信息组装而成的SynchronousMethodHandlervaluemap

在这里插入图片描述

最终来到了SynchronousMethodHandlerexecuteAndDecode,完成请求组装,发起请求,返回响应数据

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
	//基于元数据等参数构建而成的RequestTemplate 生成request,包含请求地址等信息
    Request request = targetRequest(template);

 	//发起请求并得到响应结果
      response = client.execute(request, options);

	..... 
	return response;

OpenFeign如何处理响应的?

在上文发出请求后得到result,最后会执行下面这段逻辑,他就会将response的结果解析成feign调用要求的返回值的结果,它会根据代理要求的returnType找到对应的HttpMessageConverter解析出真正的结果返回给用户

 Object result = decode(response);

参考文献

深入理解 OpenFeign 远程调用的架构原理