代码改变世界

博思软件实训总结(五)

2020-10-10 13:24  君莫笑哈哈哈  阅读(183)  评论(0编辑  收藏  举报

博思软件实训总结(五)

基于微服务架构的系统,各模块之间的调用是通过openfeign进行远程调用的,下面简单了解一下openfeign的原理

背景

OpenFeign 是 Spring Cloud 家族的一个成员, 它最核心的作用是为 HTTP 形式的 Rest API 提供了非常简洁高效的 RPC 调用方式。 如果说 Spring Cloud 其他成员解决的是系统级别的可用性,扩展性问题, 那么 OpenFeign 解决的则是与开发人员利益最为紧密的开发效率问题。
在这里插入图片描述

使用方式

在介绍 OpenFeign 的工作原理之前, 首先值得说明的是使用了 Open Feign 后, 开发人员的效率是如何得到提升的。 下面展示在使用了 OpenFeign 之后, 一个接口的提供方和消费方是如何快速高效地完成代码开发。

接口提供方

接口提供方的形式为 RestApi, 这个在 spring-web 框架的支持下, 编写起来非常简单

@RestController
@RequestMapping(value = "/api")
public class ApiController {

	@RequestMapping(value = "/demoQuery", method = {RequestMethod.POST}, consumes = MediaType.APPLICATION_JSON_VALUE)
    public ApiBaseMessage<DemoModel> demoQuery(@RequestBody DemoQryRequest request){
        return new ApiBaseMessage(new DemoModel());
    }
}
123456789

如上, 除去请求 DemoQryRequest 和响应 DemoModel 类的定义, 这个接口就已经快速地被完成了。 在这里 Feign 不需要发挥任何作用。

注意该接口的入参是 json 格式, 框架会自动帮我们反序列化到对应的 DemoQryRequest 类型的入参对象里。

返回值 ApiBaseMessage 也会被框架自动序列化为 json 格式

接口使用方

在接口的使用者一端, 首先需要引入 SpringFeign 依赖(为简化篇幅, 只展示 build.gradle 中添加 Feign 的依赖, 没有展示其他的 spring cloud 依赖添加)

    implementation('org.springframework.cloud:spring-cloud-starter-openfeign')
1
@Component
@FeignClient(name = "${feign.demoApp.name}")
@RequestMapping("/api")
public interface DemoService {
    @RequestMapping(value = "/demoQuery", method = RequestMethod.POST,
            consumes = MediaType.APPLICATION_JSON_VALUE)
    ApiBaseMessage<DemoModel> demoQuery(@RequestBody DemoQryRequest request);
}
12345678

再直接利用 spring 的自动注入功能, 就可以使用服务端的接口了

@Component
public class DemoServiceClient
{
    private final DemoService demoService;

    @Autowired
    public DemoServiceClient(DemoService demoService) {
        this.demoService= demoService;
    }

    public void useDemoService(DemoQryRequest request){
        // 直接像调用一个本地方法一样, 调用远端的 Rest API 接口, 完全是 RPC 形式
        ApiBaseMessage<DemoModel> result = demoService.demoQuery(request);
    }
}
123456789101112131415

通过上面的例子可以看到, Feign 正如同其英文含义"假装"一样, 能够让我们装作调用一个本地 java 方法一样去调用基于 HTTP 协议的 Rest API 接口。 省去了我们编写 HTTP 连接,数据解析获取等一系列繁琐的操作

工作原理

在展开讲解工作原理前, 首先捋一下上文中, 我们完成 Feign 调用前所进行的操作:

  1. 添加了 Spring Cloud OpenFeign 的依赖
  2. 在 SpringBoot 启动类上添加了注解 @EnableFeignCleints
  3. 按照 Feign 的规则定义接口 DemoService, 添加@FeignClient 注解
  4. 在需要使用 Feign 接口 DemoService 的地方, 直接利用@Autowire 进行注入
  5. 使用接口完成对服务端的调用

可以根据上面使用 Feign 的步骤大致猜测出整体的工作流程:

  1. SpringBoot 应用启动时, 由针对 @EnableFeignClient 这一注解的处理逻辑触发程序扫描 classPath中所有被@FeignClient 注解的类, 这里以 DemoService 为例, 将这些类解析为 BeanDefinition 注册到 Spring 容器中
  2. Sping 容器在为某些用的 Feign 接口的 Bean 注入 DemoService 时, Spring 会尝试从容器中查找 DemoService 的实现类
  3. 由于我们从来没有编写过 DemoService 的实现类, 上面步骤获取到的 DemoService 的实现类必然是 feign 框架通过扩展 spring 的 Bean 处理逻辑, 为 DemoService 创建一个动态接口代理对象, 这里我们将其称为 DemoServiceProxy 注册到spring 容器中。
  4. Spring 最终在使用到 DemoService 的 Bean 中注入了 DemoServiceProxy 这一实例。
  5. 当业务请求真实发生时, 对于 DemoService 的调用被统一转发到了由 Feign 框架实现的 InvocationHandler 中, InvocationHandler 负责将接口中的入参转换为 HTTP 的形式, 发到服务端, 最后再解析 HTTP 响应, 将结果转换为 Java 对象, 予以返回。

上面整个流程可以进一步简化理解为:

  1. 我们定义的接口 DemoService 由于添加了注解 @FeignClient, 最终产生了一个虚假的实现类代理
  2. 使用这个接口的地方, 最终拿到的都是一个假的代理实现类 DemoServiceProxy
  3. 所有发生在 DemoServiceProxy 上的调用, 都被转交给 Feign 框架, 翻译成 HTTP 的形式发送出去, 并得到返回结果, 再翻译回接口定义的返回值形式。

所以不难发现, Feign 的核心实现原理就是java 原生支持的基于接口的动态代理

工作原理实现细节

FeignClient 的扫描与注册

FeignClient 的扫描与注册是基于 Spring 框架的 Bean 管理机制实现的,不了解原理的同学可以考虑阅读博文那些你应该掌握的 Spring 原理

这里简单叙述 SpringBoot 应用中的扫描触发流程:

SpringApplication.run() -->
SpringApplication.refresh() -->
AbstractApplicationContext.refresh() --> AbstractApplicationContext.invokeBeanFactoryPostProcessors() -->
AbstractApplicationContext.invokeBeanDefinitionRegistryPostProcessors() -->
补充知识点: 上面的 invokeBeanFactoryPostProcessors() 能触发invokeBeanDefinitionRegistryPostProcessors() 是因为 Spring 设计中, BeanDeifinitionRegistryPostProcessor 是 BeanFactoryPostProcessor 的继承
PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors()–>
ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry()–>
ConfigurationClassPostProcessor.processConfigBeanDefinitions()–>
ConfigurationClassBeanDefinitionReader.loadBeanDefinitions()–>
ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromRegistrars -->
FeignClientsRegistrar.registerBeanDefinitions()

到这里, 我们进入了 Feign 框架的逻辑 FeignClientsRegistrar.registerBeanDefinitions()

	@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		// registerDefaultConfiguration 方法内部从 SpringBoot 启动类上检查是否有 @EnableFeignClients, 有该注解的话, 则完成 Feign 框架相关的一些配置内容注册
		registerDefaultConfiguration(metadata, registry);
		// registerFeignClients 方法内部从 classpath 中, 扫描获得 @FeignClient 修饰的类, 将类的内容解析为 BeanDefinition , 最终通过调用 Spring 框架中的 BeanDefinitionReaderUtils.resgisterBeanDefinition 将解析处理过的 FeignClient BeanDeifinition 添加到 spring 容器中
		registerFeignClients(metadata, registry);
	}
12345678

这里值得进一步关注的是, registerFeignClients 方法内部, 调用了一个 registerFeignClient方法

private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
		String className = annotationMetadata.getClassName();
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientFactoryBean.class);
		.....此处省一部分代码
	    .....此处省一部分代码
		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
				new String[] { alias });
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}

public static BeanDefinitionBuilder genericBeanDefinition(Class<?> beanClass) {
	BeanDefinitionBuilder builder = new BeanDefinitionBuilder(new GenericBeanDefinition());
	builder.beanDefinition.setBeanClass(beanClass);
	return builder;
}
1234567891011121314151617

注意! 该方法的第二行通过调用genericBeanDefinition 方法为 FeignClient 生成了一个 BeanDeifinition, 而该方法的入参是 FeignClientFactoryBean.class

查看 genericBeanDefinition 的逻辑, 发现此处将 FeignClient 的 BeanDefinition 的 beanClass 设置成了FeignClientFactoryBean.class , 也就是说 FeignClient 被注册成了一个工厂 bean(Factory Bean), 不熟悉 “Factory Bean”概念的同学可以阅读 那些你应该掌握的 Spring 原理

这里简单说明下, 工厂 Bean 是一种特殊的 Bean, 对于 Bean 的消费者来说, 他逻辑上是感知不到这个 Bean 是普通的 Bean 还是工厂 Bean, 只是按照正常的获取 Bean 方式去调用, 但工厂bean 最后返回的实例不是工厂Bean 本身, 而是执行工厂 Bean 的 getObject 逻辑返回的示例。

查看一下 FeignClientFactoryBean 的 getObject 方法

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

	<T> T getTarget() {
		FeignContext context = applicationContext.getBean(FeignContext.class);
		Feign.Builder builder = feign(context);
	
		if (!StringUtils.hasText(this.url)) {
			... 省略代码
			return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type,
					this.name, url));
		}
		... 省略代码
		return (T) targeter.target(this, builder, context, new HardCodedTarget<>(
				this.type, this.name, url));
	}
1234567891011121314151617

查看上面两个 return 所调用的方法, 最后发现都会统一使用到 Target.target() 方法, 该方法最终调用到 Feign.target 方法, 并进一步触发 RefleactiveFeign.newInstance 的执行

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

	public <T> T newInstance(Target<T> target) {
	    ... 省略代码
	    InvocationHandler handler = factory.create(target, methodToHandler);
	    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);
	    ... 省略代码
	  }

1234567891011

至此, 我们找到了对于 Java 原生的动态代理的使用, 整个 feign 的核心工作原理就基本清晰了, 后续就只是 handler 如何把基于 Proxy 方法的调用转换为 HTTP 请求发出以及翻译回来的 HTTP 响应了, 属于按部就班的工作, 有兴趣的同学可以查看源码进行学习, 这里不作赘述。

总结

Spring Cloud OpenFeign 的核心工作原理经上文探究可以非常简单的总结为:

  1. 通过 @EnableFeignCleints 触发 Spring 应用程序对 classpath 中 @FeignClient 修饰类的扫描
  2. 解析到 @FeignClient 修饰类后, Feign 框架通过扩展 Spring Bean Deifinition 的注册逻辑, 最终注册一个 FeignClientFacotoryBean 进入 Spring 容器
  3. Spring 容器在初始化其他用到 @FeignClient 接口的类时, 获得的是 FeignClientFacotryBean 产生的一个代理对象 Proxy.
  4. 基于 java 原生的动态代理机制, 针对 Proxy 的调用, 都会被统一转发给 Feign 框架所定义的一个 InvocationHandler , 由该 Handler 完成后续的 HTTP 转换, 发送, 接收, 翻译HTTP响应的工作
    在这里插入图片描述