1. 概述
本文,我们来分享 Spring Cloud Alibaba Dubbo 项目的源码解析,看看 Dubbo 是如何集成到 Spring Cloud 中的。
😈 Spring Cloud 和 Dubbo 一直不是竞争的关系,胖友要好好理解哟。
目前 Spring Cloud Alibaba Dubbo 暂时没有文档,不过不用担心,艿艿会先教你搭建一个示例,同时它也是我们后面用来调试的示例。
2. 调试环境搭建
在读源码之前,我们当然是先把调试环境搭建起来。
2.1 依赖工具
- JDK :1.8+
- Maven
- IntelliJ IDEA
2.2 源码拉取
从官方仓库 https://github.com/spring-cloud-incubator/spring-cloud-alibaba Fork
出属于自己的仓库。为什么要 Fork
?既然开始阅读、调试源码,我们可能会写一些注释,有了自己的仓库,可以进行自由的提交。😈
使用 IntelliJ IDEA 从 Fork 出来的仓库拉取代码。拉取完成后,Maven 会下载依赖包,可能会花费一些时间,耐心等待下。
考虑到方便,我们直接使用 spring-cloud-alibaba-dubbo
项目提供的示例,就在它的 test
测试目录下,如下图所示:示例项目
😈 另外,本文使用的 spring-cloud-alibaba
版本是 0.2.2.BUILD-SNAPSHOT
。
2.3 启动 Nacos
因为后面我们会使用 Nacos 作为注册中心和配置中心,所以需要启动它。具体的,参考艿艿在 《Nacos 实现原理与源码解析系统 —— 精品合集》 的 「2. 快速开始」 小节。
2.4 启动示例
右键 DubboSpringCloudBootstrap 类的 #main(String[] args)
方法,直接运行即可。🙂 如果你没有看到任何异常输出,说明就已经成功了。
另外,这个示例,即做了服务消费者,又做了服务提供者。
下面,让我们让我们逐步解释示例中的每个类和配置文件。
2.4.1 bootstrap.yaml
spring: |
- Eureka 相关的配置,可以无视,因为我们使用的是 Nacos 作为注册中心。
spring.application.name
,配置了应用名。spring.cloud.nacos.discovery.server-addr
,配置了 Nacos 作为注册中心。spring.cloud.nacos.config.server-addr
,配置了 Nacos 作为配置中心。
2.4.2 application.yaml
dubbo: |
- 每个配置,看看其后的配置文件。
2.4.3 EchoService
org.springframework.cloud.alibaba.dubbo.service.EchoService
,EchoService 接口。代码如下:
// EchoService.java |
- 熟悉不能在熟悉的 Dubbo Service 接口的代码~
2.4.4 DefaultEchoService
org.springframework.cloud.alibaba.dubbo.service.DefaultEchoService
,实现 EchoService 接口,默认的 EchoService 实现者,服务提供者。代码如下:
// DefaultEchoService.java |
@Service(version = "1.0.0", protocol = {"dubbo", "rest"})
注解,提供 Dubbo 和 Rest 两种协议的服务。
2.4.5 DubboSpringCloudBootstrap
org.springframework.cloud.alibaba.dubbo.bootstrap.DubboSpringCloudBootstrap
,示例的 Spring Boot 启动器。代码如下:
// DubboSpringCloudBootstrap.java |
@EnableDiscoveryClient
注解,用于开启注册发现 Client 的功能。@EnableAutoConfiguration
注解,用于开启自动配置。@EnableFeignClients
注解,开启 Feign Client 。在spring-cloud-alibaba-dubbo
项目中,使用 Feign 作为服务消费者。@RestController
注解,后续我们会看到这个类中会提供基于 Spring MVC 的 HTTP API 接口。echoService
属性,使用@Reference(version = "1.0.0")
注解,引入 Dubbo 服务。这个方式,就是我们原先在 Dubbo 中就使用的。feignEchoService
属性,使用标准的 Feign Client 作为服务消费者,它使用 RestTemplate 调用的是 Dubbo 提供的 Rest 接口。代码如下:// DubboSpringCloudBootstrap.java
"spring-cloud-alibaba-dubbo") (
public interface FeignEchoService {
"/echo") (value =
String echo(@RequestParam("message") String message);
}dubboFeignEchoService
属性,也使用标准的 Feign Client 作为服务消费者,它调用 Dubbo 调用的是 Dubbo 提供的 Dubbo 接口。代码如下:// DubboSpringCloudBootstrap.java
"spring-cloud-alibaba-dubbo") (
public interface DubboFeignEchoService {
"/echo") (value =
String echo(@RequestParam("message") String message);
}- 相比
echoService
属性,它在方法上,增加了org.springframework.cloud.alibaba.dubbo.annotation.@DubboTransported
注解。 - 可能会有胖友好奇,具体是如何实现的呢?本文我们一起来揭晓~
- 相比
// DubboSpringCloudBootstrap.java |
- 声明了 ApplicationRunner Bean 对象,在 Spring Boot 启动完成,直接发起相应的调用。它的目的是,看看三种调用方式,是否都正常。如果没有报错,说明都是 OK 的。
// DubboSpringCloudBootstrap.java |
- 声明了三个 Spring MVC HTTP API ,分别调用三个不同的服务消费者。
- 这就是为什么 DubboSpringCloudBootstrap 有
@RestController
注解,且配置文件中配置了server.port=8080
。
😈 至此,我们已经完整看过了 Spring Cloud Alibaba Dubbo 的示例,可以愉快的开始调试了。
3. 项目结构一览
本文主要分享 spring-cloud-alibaba-dubbo
的 项目结构。
希望通过本文能让胖友对 spring-cloud-alibaba-dubbo
的整体项目有个简单的了解。
3.1 代码统计
这里先分享一个小技巧。笔者在开始源码学习时,会首先了解项目的代码量。
第一种方式,使用 IDEA Statistic 插件,统计整体代码量。
- 我们可以粗略的看到,整个 Spring Cloud Alibaba 的代码量在 16000 行。这其中还包括单元测试,示例等等代码。
- /(ㄒoㄒ)/~~ 显然,目前这个插件没办法很方便的统计出,我们想要看到的
spring-cloud-alibaba-dubbo
的代码量。
第二种方式,使用 Shell 脚本命令逐个 Maven 模块统计 。
一般情况下,笔者使用 find . -name "*.java"|xargs cat|grep -v -e ^$ -e ^\s*\/\/.*$|wc -l
。这个命令只过滤了部分注释,所以相比 IDEA Statistic 会偏多。
当然,考虑到准确性,胖友需要手动 cd
到每个 Maven 项目的 src/main/java
目录下,以达到排除单元测试的代码量。
统计完后,发现代码量是不多的。
3.2 annotation
包
annotation
包,62 行代码,提供 @EnableFeignClients
注解。
3.3 autoconfigure
包
autoconfigure
包,172 行代码,提供 Spring Boot 自动配置 Spring Cloud Alibaba Dubbo 。
3.4 context
包
context
包,35 行代码,目前仅有 DubboServiceRegistrationApplicationContextInitializer 类,先不解释哈~
3.5 metadata
包
metadata
包,904 行代码,实现将 Spring Cloud Alibaba Dubbo 的服务的元数据,存储到配置中心。这样,后续使用 @DubboTransported
注解的 Dubbo 调用时,因为会使用到 Dubbo的泛化调用 ,有了服务的元数据,就可以愉快的调用了。
3.6 openfeign
包
openfeign
包,305 行代码,实现将 Dubbo 集成到 OpenFeign 中。
3.7 registry
包
registry
包,478 行代码,实现 Dubbo 使用 Spring Cloud Service 注册中心体系。可能这么说有点抽象,我们可以回过头看看 「2.4.2 application.yaml」 中看到,有个神奇的 dubbo.protocols.registry.address=spring-cloud://nacos
配置。
emm~哈哈哈,等会看具体代码,会更加明白。
4. annotation
包
4.1 @DubboTransported
org.springframework.cloud.alibaba.dubbo.annotation.@DubboTransported
注解,表名调用时,使用 Dubbo 作为底层 RPC 调用。代码如下:
// DubboTransported.java |
protocol
属性,使用的 Dubbo 协议,默认为"dubbo"
。cluster
属性,使用 使用的集群容错方式,默认为"failover"
。- 可标记在类或者方法上。
5. autoconfigure
包
在 META-INF/spring.factories
文件中,声明了三个自动配置类。代码如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
- 下面,我们逐个来瞅瞅。
5.1 DubboMetadataAutoConfiguration
org.springframework.cloud.alibaba.dubbo.autoconfigure.DubboMetadataAutoConfiguration
,Dubbo 元数据(Metadata)相关 Bean 的自动配置类。代码如下:
// DubboMetadataAutoConfiguration.java |
- 通过
@Import(DubboServiceMetadataRepository.class)
,创建了 DubboServiceMetadataRepository 对象。详细解析,见 「7.5 DubboServiceMetadataRepository」 。 #metadataConfigService()
方法,创建了 NacosMetadataConfigService Bean 对象。详细解析,见 「7.4.1 NacosMetadataConfigService」 。
5.2 DubboOpenFeignAutoConfiguration
org.springframework.cloud.alibaba.dubbo.autoconfigure.DubboOpenFeignAutoConfiguration
,Dubbo OpenFeign 相关 Bean 的自动配置类。代码如下:
// DubboOpenFeignAutoConfiguration.java |
#metadataJsonResolver(...)
方法,创建 DubboServiceBeanMetadataResolver Bean 对象。详细解析,见 「7.2 MetadataResolver」 。#targeterBeanPostProcessor(...)
方法,创建 TargeterBeanPostProcessor Bean 对象。详细解析,见 「8.1 TargeterBeanPostProcessor」 。
5.3 DubboRestMetadataRegistrationAutoConfiguration
org.springframework.cloud.alibaba.dubbo.autoconfigure.DubboRestMetadataRegistrationAutoConfiguration
,自动配置两个 Spring 事件监听器,将 Dubbo Rest 元数据(Metadata)注册到配置中心。代码如下:
// DubboRestMetadataRegistrationAutoConfiguration.java |
serviceRestMetadata
属性,Dubbo Rest Service 方法的元数据(Metadata)集合。#recordRestMetadata(ServiceBeanExportedEvent)
方法,监听 ServiceBeanExportedEvent 事件。- 在每个 Dubbo 服务暴露后,会发布 ServiceBeanExportedEvent 事件。在 《精尽 Dubbo 源码分析 —— 注解配置》 的 「5.3.3 onApplicationEvent」 中,我们有过详细解析。
- 在接收到 ServiceBeanExportedEvent 事件,该方法会调用
MetadataResolver#resolveServiceRestMetadata(ServiceBean serviceBean)
方法,获得该 ServiceBean 的 ServiceRestMetadata 集合,添加到serviceRestMetadata
中。详细解析,见 「7.5 DubboServiceBeanMetadataResolver」 。
#registerRestMetadata(InstancePreRegisteredEvent)
方法,监听 InstancePreRegisteredEvent 事件。- InstancePreRegisteredEvent 事件,在 Spring Cloud 应用注册到注册中心之前,会触发该事件。详细的,可以看看 《spring-cloud-commons reference》 文章的 「2.2.1 ServiceRegistry Auto-Registration」 小节。现在,可以不看~
- 在接收到 InstancePreRegisteredEvent 事件,该方法会调用
MetadataConfigService#publishServiceRestMetadata(String serviceName, Set<ServiceRestMetadata> serviceRestMetadata)
方法,将每个 Dubbo 服务的 ServiceRestMetadata 元数据集合,发布(存储)到配置中心。
- 这样,后续的 Dubbo 泛化调用,就有 Dubbo 服务的元数据咧。
因为本小节讲的都是自动配置类,胖友可能会略有懵逼。不要慌,我们继续往下撸。
对咧,最好一边调试,一边看。
6. context
包
在 META-INF/spring.factories
文件中,声明了三个自动配置类。代码如下:
org.springframework.context.ApplicationContextInitializer=\ |
6.1 DubboServiceRegistrationApplicationContextInitializer
org.springframework.cloud.alibaba.dubbo.context.DubboServiceRegistrationApplicationContextInitializer
,实现 ApplicationContextInitializer 接口,将 applicationContext
设置到 SpringCloudRegistryFactory.applicationContext
静态属性。代码如下:
// SpringCloudRegistryFactory.java |
7. metadata
包
在看具体代码之前,我们先在 Nacos 的配置中心界面,看看什么是 Dubbo Metadata 。如下图所示:Dubbo Metadata
可能这样还是不够清理,我们来看一个 Dubbo Service Metadata 更具体的示例,如下 JSON 串:
{ |
*
7.1 Metadata 类
在 metadata
包的根目录,一共有 6 个 Metadata 类。如下:
- ServiceRestMetadata
- RestMethodMetadata
- MethodMetadata
- MethodParameterMetadata
- RequestMetadata
- DubboTransportedMethodMetadata
7.1.1 ServiceRestMetadata
org.springframework.cloud.alibaba.dubbo.metadata.ServiceRestMetadata
,Service Rest Metadata 。代码如下:
// ServiceRestMetadata.java |
7.1.2 RestMethodMetadata
org.springframework.cloud.alibaba.dubbo.metadata.RestMethodMetadata
,Rest Method Metadata 。代码如下:
// RestMethodMetadata.java |
7.1.2.1 MethodMetadata
org.springframework.cloud.alibaba.dubbo.metadata.MethodMetadata
,Method Metadata 。代码如下:
// ServiceRestMetadata.java |
7.1.2.1.1 MethodParameterMetadata
org.springframework.cloud.alibaba.dubbo.metadata.MethodParameterMetadata
,Method Parameter Metadata 。代码如下:
// MethodParameterMetadata.java |
7.1.2.2 RequestMetadata
org.springframework.cloud.alibaba.dubbo.metadata.RequestMetadata
,Request Metadata 。代码如下:
// RequestMetadata.java |
7.1.2.3 DubboTransportedMethodMetadata
org.springframework.cloud.alibaba.dubbo.metadata.DubboTransportedMethodMetadata
,继承 MethodMetadata 类, @DubboTransported
注解对应的 MethodMetadata 对象。代码如下:
// DubboTransportedMethodMetadata.java |
7.2 MetadataResolver
org.springframework.cloud.alibaba.dubbo.metadata.resolver.MetadataResolver
,Metadata Resolver 元数据解析器接口。代码如下:
// MetadataResolver.java |
7.2.1 DubboServiceBeanMetadataResolver
DubboServiceBeanMetadataResolver Bean 对象,在 「5.2 DubboOpenFeignAutoConfiguration」 中被创建。
org.springframework.cloud.alibaba.dubbo.metadata.resolver.DubboServiceBeanMetadataResolver
,实现 MetadataResolver、BeanClassLoaderAware、SmartInitializingSingleton 接口,基于 Dubbo ServiceBean 的 MetadataResolver 实现类。
// DubboServiceBeanMetadataResolver.java |
7.2.1.1 构造方法
// DubboServiceBeanMetadataResolver.java |
- 具体的每个属性,我们下面一个一个来看。
7.2.1.2 afterSingletonsInstantiated
实现 #afterSingletonsInstantiated()
方法,初始化 contracts
属性。代码如下:
// DubboServiceBeanMetadataResolver.java |
<1>
处,创建 Feign Contract 数组contracts
。<2.1>
处,如果contract
存在,则添加到contracts
中。一般情况下,contract
不存在,所以可以暂时无视。<2.2>
处,遍历CONTRACT_CLASS_NAMES
数组,创建对应的 Contract 对象,添加到contracts
中。一般来说,都会存在。涉及代码如下:// DubboServiceBeanMetadataResolver.java
// 判断指定 className 类是否存在
private boolean isClassPresent(String className) {
return ClassUtils.isPresent(className, classLoader);
}
// 加载 contractClassName 对应的 Contract 实现类
private Class<?> loadContractClass(String contractClassName) {
return ClassUtils.resolveClassName(contractClassName, classLoader);
}
// 创建 Contract 对象
private Contract createContract(Class<?> contractClassName) {
return (Contract) BeanUtils.instantiateClass(contractClassName);
}<3>
处,赋值给this.contracts
中。
7.2.1.3 resolveMethodRestMetadata
实现 #resolveMethodRestMetadata(Class<?> targetType)
方法,解析指定类的 ServiceRestMetadata 集合。代码如下:
// DubboServiceBeanMetadataResolver.java |
<1>
处,调用#selectFeignContractMethods(Class<?> targetType)
方法,获得 Method 集合。代码如下:// DubboServiceBeanMetadataResolver.java
/**
* Select feign contract methods
* <p>
* extract some code from {@link Contract.BaseContract#parseAndValidatateMetadata(java.lang.Class)}
*
* @param targetType
* @return non-null
*/
private List<Method> selectFeignContractMethods(Class<?> targetType) {
List<Method> methods = new LinkedList<>();
// 遍历目标的方法
for (Method method : targetType.getMethods()) {
// 忽略
if (method.getDeclaringClass() == Object.class || // Object 声明的方法,例如说 equals 方法
(method.getModifiers() & Modifier.STATIC) != 0 || // 静态方法
Util.isDefault(method)) { // Feign 默认方法
continue;
}
methods.add(method);
}
return methods;
}<2>
处,转换成 RestMethodMetadata 集合。<2.1>
处,调用Contract#parseAndValidatateMetadata()
方法,返回目标类型的 Feign MethodMetadata 数组。这块代码属于 Feign 的,我们先不细调,看一个结果的示例。如下图:结果
这里有一点要注意,因为
CONTRACT_CLASS_NAMES
中,即有 JAXRS2Contract 类,又有 SpringMvcContract 类,所以要求添加 JSR311 的注解,也要添加 Spring MVC 的注解。举个例子:// DefaultEchoService.java
"1.0.0", protocol = {"dubbo", "rest"}) (version =
// Spring MVC 注解
"/") // JSR311 注解 (
public class DefaultEchoService implements EchoService {
"/echo" (value =
// consumes = MediaType.APPLICATION_JSON_VALUE,
// produces = MediaType.APPLICATION_JSON_UTF8_VALUE
) // Spring MVC 注解
"/echo") // JSR311 注解 (
// JSR311 注解
// @Consumes("application/json")
// @Produces("application/json;charset=UTF-8")
public String echo(@RequestParam // Spring MVC 注解
@QueryParam("message") String message) { // JSR311 注解
System.out.println(message);
return RpcContext.getContext().getUrl() + " [echo] : " + message;
}
}- 不过艿艿暂时不太理解,为什么这么设计。有知道的胖友,麻烦教育下,嘻嘻~
<2.2>
处,调用#resolveMethodRestMetadata(MethodMetadata methodMetadata, Class<?> targetType, List<Method> feignContractMethods)
方法,将 Feign MethodMetadata 转换成 RestMethodMetadata 对象。代码如下:// DubboServiceBeanMetadataResolver.java
protected RestMethodMetadata resolveMethodRestMetadata(MethodMetadata methodMetadata, // Feign MethodMetadata
Class<?> targetType,
List<Method> feignContractMethods) {
// 获得 configKey 。例如说:DefaultEchoService#echo(String)
String configKey = methodMetadata.configKey();
// 获得匹配的 Method
Method feignContractMethod = getMatchedFeignContractMethod(targetType, feignContractMethods, configKey);
// 创建 RestMethodMetadata 对象,并设置其属性
RestMethodMetadata metadata = new RestMethodMetadata();
metadata.setRequest(new RequestMetadata(methodMetadata.template()));
metadata.setMethod(new org.springframework.cloud.alibaba.dubbo.metadata.MethodMetadata(feignContractMethod));
metadata.setIndexToName(methodMetadata.indexToName());
return metadata;
}
private Method getMatchedFeignContractMethod(Class<?> targetType, List<Method> methods, String expectedConfigKey) {
Method matchedMethod = null;
// 遍历 Method 集合
for (Method method : methods) {
// 获得该方法的 configKey
String configKey = Feign.configKey(targetType, method);
// 如果相等,则进行返回。
if (expectedConfigKey.equals(configKey)) {
matchedMethod = method;
break;
}
}
return matchedMethod;
}- 对象转换的代码,简单瞅瞅即可明白。
7.2.1.4 resolveServiceRestMetadata
实现 #resolveServiceRestMetadata(ServiceBean serviceBean)
方法,解析指定 ServiceBean 的 ServiceRestMetadata 集合。代码如下:
// DubboServiceBeanMetadataResolver.java |
<1.3>
处,调用#resolveMethodRestMetadata(Class<?> targetType)
方法,解析 Bean 类型对应的 RestMethodMetadata 集合。即,我们在 「7.2.1.3 resolveMethodRestMetadata」 解析的方法。<2.1>
处,获得 ServiceBean 暴露的 URL 集合。例如说,本文的 DefaultEchoService 示例,就暴露了dubbo://
和rest://
两种协议的 URL 。<2.3>
处,遍历 URL 集合,将 RestMethodMetadata 集合,封装成 ServiceRestMetadata 对象,然后添加到 serviceRestMetadata 中,最后返回。此处的SpringCloudRegistry::getServiceName
代码段,就是调用SpringCloudRegistry#getServiceName(URL url)
方法,获得 Dubbo 服务名。例如说:providers:dubbo:org.springframework.cloud.alibaba.dubbo.service.EchoService:1.0.0
。
至此,Dubbo Service 元数据的解析,就已经完成了。后续,我们会看到两个方面的内容:
- 1、将元数据存储到配置中心。这样,服务消费者才能共享到该部分的数据。
- 2、服务消费者基于 Dubbo Service 元数据,可以使用 Dubbo 发起泛化调用。
当然,如果不使用 Dubbo 发起泛化调用,是不需要这部分 Dubbo Service 元数据的。为什么呢?胖友好好思考一波~
7.3 DubboTransportedMethodMetadataResolver
org.springframework.cloud.alibaba.dubbo.metadata.resolver.DubboTransportedMethodMetadataResolver
,解析 @DubboTransported
注解的 MethodMetadata 数据。代码如下:
// DubboTransportedMethodMetadataResolver.java |
<1>
处,调用#resolveDubboTransportedMethodMetadataSet(Class<?> targetType)
方法,获得指定类的 DubboTransportedMethodMetadata 集合。代码如下:// DubboTransportedMethodMetadataResolver.java
protected Set<DubboTransportedMethodMetadata> resolveDubboTransportedMethodMetadataSet(Class<?> targetType) {
// The public methods of target interface
Method[] methods = targetType.getMethods();
// 创建 DubboTransportedMethodMetadata 数组
Set<DubboTransportedMethodMetadata> methodMetadataSet = new LinkedHashSet<>();
// 遍历方法
for (Method method : methods) {
// 如果有 @DubboTransported 注解
DubboTransported dubboTransported = resolveDubboTransported(method); // ①
// 如果有,则创建成 DubboTransportedMethodMetadata 对象,并添加到 methodMetadataSet 中
if (dubboTransported != null) {
// 创建 ②
DubboTransportedMethodMetadata methodMetadata = createDubboTransportedMethodMetadata(method, dubboTransported);
// 添加
methodMetadataSet.add(methodMetadata);
}
}
return methodMetadataSet;
}
// ①
private DubboTransported resolveDubboTransported(Method method) {
// 先从方法上,获得 @DubboTransported 注解
DubboTransported dubboTransported = AnnotationUtils.findAnnotation(method, DUBBO_TRANSPORTED_CLASS);
// 如果获得不到,则从类上,获得 @DubboTransported 注解
if (dubboTransported == null) { // Attempt to find @DubboTransported in the declaring class
Class<?> declaringClass = method.getDeclaringClass();
dubboTransported = AnnotationUtils.findAnnotation(declaringClass, DUBBO_TRANSPORTED_CLASS);
}
return dubboTransported;
}
// ②
private DubboTransportedMethodMetadata createDubboTransportedMethodMetadata(Method method, DubboTransported dubboTransported) {
// 创建 DubboTransportedMethodMetadata 对象
DubboTransportedMethodMetadata methodMetadata = new DubboTransportedMethodMetadata(method);
// 解析属性,并设置到 methodMetadata 中
String protocol = propertyResolver.resolvePlaceholders(dubboTransported.protocol());
String cluster = propertyResolver.resolvePlaceholders(dubboTransported.cluster());
methodMetadata.setProtocol(protocol);
methodMetadata.setCluster(cluster);
return methodMetadata;
}<2>
处,调用#resolveRequestMetadataMap(Class<?> targetType)
方法,获得指定类的 RequestMetadata 映射。其中,KEY 为configKey
。代码如下:// DubboTransportedMethodMetadataResolver.java
private Map<String, RequestMetadata> resolveRequestMetadataMap(Class<?> targetType) {
return contract.parseAndValidatateMetadata(targetType) // 获得指定类的 Feign MethodMetadata 集合
.stream().collect(Collectors.toMap(feign.MethodMetadata::configKey, this::requestMetadata)); // 创建 RequestMetadata 对象
}
private RequestMetadata requestMetadata(feign.MethodMetadata methodMetadata) {
return new RequestMetadata(methodMetadata.template());
}<3>
处,转换成 DubboTransportedMethodMetadata 和 RequestMetadata 的映射。
后续,这个类会被 「8.1 TargeterInvocationHandler」 所调用 。
7.4 MetadataConfigService
给服务提供者使用。
org.springframework.cloud.alibaba.dubbo.metadata.service.MetadataConfigService
,元数据配置服务接口,即可以从配置中心,读取和写入元数据。代码如下:
// MetadataConfigService.java |
7.4.1 NacosMetadataConfigService
org.springframework.cloud.alibaba.dubbo.metadata.service.NacosMetadataConfigService
,实现 MetadataConfigService 接口,基于 Nacos 作为配置中心的实现类。
7.4.1.1 构造方法
// NacosMetadataConfigService.java |
7.4.1.2 publishServiceRestMetadata
实现 #publishServiceRestMetadata(String serviceName, Set<ServiceRestMetadata> serviceRestMetadata)
方法,代码如下:
// NacosMetadataConfigService.java |
7.4.1.3 getServiceRestMetadata
实现 #getServiceRestMetadata(String serviceName)
方法,代码如下:
// NacosMetadataConfigService.java |
7.5 DubboServiceMetadataRepository
给服务消费者使用。
org.springframework.cloud.alibaba.dubbo.metadata.repository.DubboServiceMetadataRepository
,Dubbo Service Metadata 仓库。通过该类,服务消费者就可以获得每个对应 Dubbo 服务的 ReferenceBean 对象~
7.5.1 构造方法
// DubboServiceMetadataRepository.java |
- 看看每个属性上的注释哟。
- 有一点要注意,
Key is application name
,表示首个 KEY 是 Spring Cloud(Spring Boot)应用名。- 但是,一个 Dubbo 应用中,可以有多个 Dubbo Service ,所以,就有了第二个 Map ,例如
Map<RequestMetadata, ReferenceBean<GenericService>
。 - 当然,此处是按照 RequestMetadata 为维度,即每个 Dubbo Service 方法对应的请求信息。因为,每个请求路径的 URL 是唯一的,所以这么做是 OK 的。相当于说,把每个 Dubbo Service 的方法,平铺开。
- 但是,一个 Dubbo 应用中,可以有多个 Dubbo Service ,所以,就有了第二个 Map ,例如
7.5.2 updateMetadata
#updateMetadata(String serviceName)
方法,初始化指定 serviceName
的元数据。代码如下:
// DubboServiceMetadataRepository.java |
<1.3>
处,调用MetadataConfigService#getServiceRestMetadata(String serviceName)
方法,获得指定serviceName
的 ServiceRestMetadata 集合。此处,我们就使用上了 「7.4 MetadataConfigService」。<2>
处,遍历 ServiceRestMetadata 集合,创建对应的 ReferenceBean ,获得对应的 MethodMetadata 对象。<2.1>
处,调用#adaptReferenceBean(ServiceRestMetadata serviceRestMetadata)
方法,创建 ReferenceBean 对象。代码如下:// DubboServiceMetadataRepository.java
private ReferenceBean<GenericService> adaptReferenceBean(ServiceRestMetadata serviceRestMetadata) {
// 获得相应的属性
String dubboServiceName = serviceRestMetadata.getName();
String[] segments = SpringCloudRegistry.getServiceSegments(dubboServiceName);
String interfaceName = SpringCloudRegistry.getServiceInterface(segments);
String version = SpringCloudRegistry.getServiceVersion(segments);
String group = SpringCloudRegistry.getServiceGroup(segments);
// 创建 ReferenceBean 对象,并设置相关属性
ReferenceBean<GenericService> referenceBean = new ReferenceBean<GenericService>();
referenceBean.setGeneric(true);
referenceBean.setInterface(interfaceName);
referenceBean.setVersion(version);
referenceBean.setGroup(group);
return referenceBean;
}- 注意哦,此时创建的 ReferenceBean 是 Dubbo 的泛化引用。
<2.2>
处,遍历 RestMethodMetadata 集合,添加到genericServicesMap
和methodMetadataMap
中,进行缓存。
7.5.3 getReferenceBean
#getReferenceBean(String serviceName, RequestMetadata requestMetadata)
方法,获得指定应用的指定 RequestMetadata 对应的 ReferenceBean 对象。代码如下:
public ReferenceBean<GenericService> getReferenceBean(String serviceName, RequestMetadata requestMetadata) { |
7.5.4 getMethodMetadata
#getMethodMetadata(String serviceName, RequestMetadata requestMetadata)
方法,获得指定应用的指定 RequestMetadata 对应的 MethodMetadata 对象。代码如下:
// DubboServiceMetadataRepository.java |
7.6 小结
至此,我们看完了 metadata
包下的所有代码。因为本小节更多是元数据的解析、存储、读取,不涉及到具体的使用,所以会略微有点懵逼。但是,到下一节 openfeign
后,Feign 通过泛化调用对应的 Dubbo 服务,就会使用上元数据了。此时,我们就可以把整个流程打通。
8. openfeign
包
本小节,我们来瞅瞅,Dubbo 是如何和 Feign 进行集成的。
8.1 TargeterBeanPostProcessor
org.springframework.cloud.alibaba.dubbo.openfeign.TargeterBeanPostProcessor
,实现 BeanPostProcessor、BeanClassLoaderAware 接口,处理类型为 openfeign Targeter 的 Bean ,创建其动态代理,从而能够将 Dubbo 集成到 Openfeign 中。代码如下:
// TargeterBeanPostProcessor.java |
- 在分析具体代码之前,胖友先看下 《Spring Cloud Alibaba Sentinel 整合 Feign 的设计实现》 的 「Feign 的执行过程」 小节。因为艿艿看的时候也不是很了解 Feign 的内部运转机制,也是参考这个小节读懂这部分代码的。
<1>
处,获得 Bean 的类。根据上面推荐的文章,我们可以知道,此时返回的是 HystrixTargeter 或 DefaultTargeter 类。<2>
处,获得 openfeign Targeter 接口。<3>
处,如果实现 openfeign Targeter 接口,则创建动态代理。其中,传入的处理器是 TargeterInvocationHandler 对象。详细解析,见 「8.2 TargeterInvocationHandler」 。
8.2 TargeterInvocationHandler
org.springframework.cloud.alibaba.dubbo.openfeign.TargeterInvocationHandler
,会拦截 Targeter 的 #target(FeignClientFactoryBean factory, Builder feign, FeignContext context, HardCodedTarget<T> target)
方法,根据条件,创建不同的代理对象。代码如下:
如果不理解 Targeter 的话,请再看下 《Spring Cloud Alibaba Sentinel 整合 Feign 的设计实现》 的 「Feign 的执行过程」 小节。
// TargeterInvocationHandler.java |
- 为了让胖友更加好理解,我们来看下这个方法被调用时的截图:
调用图
<1>
处,先调用原有target
方法,返回默认的代理对象。<2>
处,调用#createDubboProxyIfRequired(FeignContext feignContext, Target target, Object defaultProxy)
方法,如果符合创建 Dubbo 代理对象,则创建 Dubbo 代理对象。否则,使用默认的 defaultProxy 代理。那么问题就来了,条件是什么呢?有@DubboTransported
注解,且从配置中心拉取不到服务提供者的元数据。😈 因为,没有服务提供者的元数据,我们也无法使用 Dubbo 的泛化调用呀。- so ,我们继续往下看。
8.2.1 createDubboProxyIfRequired
#createDubboProxyIfRequiredcreateDubboProxyIfRequired(FeignContext feignContext, Target target, Object defaultProxy)
方法,根据条件,创建对应的代理对象。代码如下:
// TargeterInvocationHandler.java |
<1>
处,调用#createDubboInvocationHandler(FeignContext feignContext, Target target, Object defaultFeignClientProxy)
方法,尝试创建 DubboInvocationHandler。<2.1>
处,如果未创建成功,说明不符合条件,则返回默认的defaultProxy
代理。<2.2>
处,如果创建成功,说明符合条件,则创建使用dubboInvocationHandler
的动态代理。
8.2.2 createDubboInvocationHandler
#createDubboInvocationHandler(FeignContext feignContext, Target target, Object defaultFeignClientProxy)
方法,创建 DubboInvocationHandler 对象。代码如下:
未来这块的逻辑,会被抽取到
org.springframework.cloud.alibaba.dubbo.openfeign.DubboInvocationHandlerFactory
类中。
// TargeterInvocationHandler.java |
<1.1>
处,获得 Feign Contract 。<1.2>
处,创建 DubboTransportedMethodMetadataResolver 对象。<1.3>
处,调用DubboTransportedMethodMetadataResolver#resolve(Class<?> targetType)
解析指定类,获得其 DubboTransportedMethodMetadata 和 RequestMetadata 的映射。此处,我们就把 「7.3 DubboTransportedMethodMetadata」 给串起来了。<1.4>
处,如果为空,则返回,说明不符合条件。此处,我们来抛一个问题,如果一个类里,多个方法中,有部分方法没有@DubboTransported
注解,那么会不会创建 DubboInvocationHandler 呢?答案是肯定的。那么此时,就会有@DubboTransported
注解的方法,使用 Dubbo 进行调用,没有@DubboTransported
注解的方法,还是选择原有 Feign 提供的方式(例如说 RestTemplate)进行调用。<2>
处,调用DubboServiceMetadataRepository#updateMetadata(String serviceName)
方法,初始化指定serviceName
的元数据(此时,会从配置中心,获得元数据)。此处,我们就把 「7.5 DubboServiceMetadataRepository」 给串起来了。<3>
处,遍历methodRequestMetadataMap
集合,初始化其 GenericService 。<3.1.1>
处,获得 ReferenceBean 对象,并初始化其属性。<3.1.2>
处,添加到genericServicesMap
中。<3.2.1>
处,获得 MethodMetadata 对象。<3.2.2>
处,添加到methodMetadataMap
中。
<4.1>
处,获得默认的defaultFeignClientProxy
中,默认的 InvocationHandler 对象。为什么需要它呢?因为,一个类中,可能有没有@DubboTransported
注解的方法。<4.2>
处,创建 DubboInvocationHandler 对象。
8.3 DubboInvocationHandler
org.springframework.cloud.alibaba.dubbo.openfeign.DubboInvocationHandler
,实现 InvocationHandler 接口,Dubbo InvocationHandler 实现类。代码如下:
// DubboInvocationHandler.java |
<1>
处,如果任一不存在,使用默认的defaultInvocationHandler
,即无@DubboTransported
注解的方法。<2>
处,执行泛化调用,即有@DubboTransported
注解的方法。
8.4 小结
至此,整个服务消费者调用的流程,我们已经串联起来了。因为本文是按照 package
包分层来写,所以连贯性会相对比较差。因此,需要胖友自己在调试一下哈。
9. registry
包
本小节,我们来瞅瞅,Dubbo 是如何和 Spring Cloud 注册中心进行集成的。
在 2.4.2 application.yaml 中,我们可以看到,注册中心使用的是 dubbo.registry.address: spring-cloud://nacos
。
9.1 Registration
org.springframework.cloud.alibaba.dubbo.registry.DubboRegistration
,实现 Spring Cloud Registration 接口,Dubbo Registration 实现类。代码如下:
// DubboRegistration.java |
9.2 SpringCloudRegistryFactory
在 com.alibaba.dubbo.registry.RegistryFactory
中,声明了一个 SpringCloudRegistryFactory 拓展。如下:
spring-cloud=org.springframework.cloud.alibaba.dubbo.registry.SpringCloudRegistryFactory |
- 前缀为
"spring-cloud"
。即,和我们配置的dubbo.registry.address: spring-cloud://nacos
能够对应上。
org.springframework.cloud.alibaba.dubbo.registry.SpringCloudRegistryFactory
,实现 Dubbo RegistryFactory 接口,创建对 SpringCloudRegistry 注册中心。代码如下:
// SpringCloudRegistryFactory.java |
<1>
处,获得 ServiceRegistry 对象。此处,如果我们使用 Nacos 作为注册中心,那么返回的就是org.springframework.cloud.alibaba.nacos.registry.NacosServiceRegistry
对象。<2>
处,获得 DiscoveryClient 对象。此处,如果我们使用 Nacos 作为注册中心,那么返回的 Composite DiscoveryClient 对象,包含org.springframework.cloud.alibaba.nacos.NacosDiscoveryClient
对象。- 上述两个变量,如下图所示:
变量
<3>
处,创建 SpringCloudRegistry 对象。详细解析,见 「9.3 SpringCloudRegistry」 。
9.3 SpringCloudRegistry
本小节,建立在胖友看过 《精尽 Dubbo 源码分析 —— 注册中心(一)之抽象 API》 文章。
org.springframework.cloud.alibaba.dubbo.registry.SpringCloudRegistry
,继承 Dubbo FailbackRegistry 抽象类,基于 Spring Cloud DiscoveryClient、SpringCloudRegistry 的 API ,封装出 Spring Cloud Registry 实现类。
9.3.1 构造方法
// SpringCloudRegistry.java |
9.3.2 doRegister
实现 #doRegister(URL ur)
方法,执行注册。代码如下:
// SpringCloudRegistry.java |
<1>
处,获得serviceName
。例如:providers:dubbo:org.springframework.cloud.alibaba.dubbo.service.EchoService:1.0.0
。代码如下:// SpringCloudRegistry.java
public static String getServiceName(URL url) {
// 获得分类
String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
// 获得 ServiceName
return getServiceName(url, category);
}
private static void appendIfPresent(StringBuilder target, URL url, String parameterName) {
String parameterValue = url.getParameter(parameterName);
appendIfPresent(target, parameterValue);
}
private static final String SERVICE_NAME_SEPARATOR = ":";
private static void appendIfPresent(StringBuilder target, String parameterValue) {
if (StringUtils.hasText(parameterValue)) {
target.append(SERVICE_NAME_SEPARATOR).append(parameterValue);
}
}<2>
处,创建 DubboRegistration 对象。代码如下:// SpringCloudRegistry.java
private Registration createRegistration(String serviceName, URL url) {
return new DubboRegistration(createServiceInstance(serviceName, url));
}
private ServiceInstance createServiceInstance(String serviceName, URL url) {
// Append default category if absent
// 获得属性
String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
URL newURL = url.addParameter(Constants.CATEGORY_KEY, category);
newURL = newURL.addParameter(Constants.PROTOCOL_KEY, url.getProtocol());
String ip = NetUtils.getLocalHost(); // IP
int port = newURL.getParameter(Constants.BIND_PORT_KEY, url.getPort()); // 端口
// 创建 DefaultServiceInstance 对象
DefaultServiceInstance serviceInstance = new DefaultServiceInstance(serviceName, ip, port, false);
serviceInstance.getMetadata().putAll(new LinkedHashMap<>(newURL.getParameters()));
return serviceInstance;
}<3>
处,调用ServiceRegistry#register(R registration)
方法,注册到serviceRegistry
中。这样,Dubbo 就注册到 Spring Cloud Registry 中。
9.3.3 doUnregister
实现 #doUnregister(URL url)
方法,取消注册。代码如下:
// SpringCloudRegistry.java |
9.3.4 doSubscribe
实现 #doSubscribe(URL url, NotifyListener listener)
方法,执行订阅。代码如下:
// SpringCloudRegistry.java |
<1>
处,获得 serviceName 数组。代码如下:// SpringCloudRegistry.java
private List<String> getServiceNames(URL url, NotifyListener listener) {
// 管理端,暂时无视
if (isAdminProtocol(url)) {
scheduleServiceNamesLookup(url, listener);
return getServiceNamesForOps(url);
} else {
return doGetServiceNames(url);
}
}
private List<String> doGetServiceNames(URL url) {
// 获得 category 数组
String[] categories = getCategories(url);
// 创建 serviceName 数组,并进行获得
List<String> serviceNames = new ArrayList<String>(categories.length);
for (String category : categories) {
final String serviceName = getServiceName(url, category);
serviceNames.add(serviceName);
}
return serviceNames;
}<2>
处,调用#doSubscribe(final URL url, final NotifyListener listener, final List<String> serviceNames)
方法,执行订阅。代码如下:// SpringCloudRegistry.java
private void doSubscribe(final URL url, final NotifyListener listener, final List<String> serviceNames) {
for (String serviceName : serviceNames) {
// <2.1> 获得 ServiceInstance 数组
List<ServiceInstance> serviceInstances = discoveryClient.getInstances(serviceName);
// <2.2> 通知订阅者
notifySubscriber(url, listener, serviceInstances);
// TODO Support Update notification event
}
}- 遍历
serviceNames
数组,逐个处理。 <2.1>
处,调用DiscoveryClient#getInstances(String serviceId)
方法,获得 ServiceInstance 数组。<2.2>
处,调用#notifySubscriber(URL url, NotifyListener listener, List<ServiceInstance> serviceInstances)
方法,通知订阅者。详细解析,见 「notifySubscriber」 。
- 遍历
9.3.4.1 notifySubscriber
#notifySubscriber(URL url, NotifyListener listener, List<ServiceInstance> serviceInstances)
方法,通知订阅者。代码如下:
// SpringCloudRegistry.java |
<1>
处,调用#filterHealthyInstances(Collection<ServiceInstance> instances)
方法,过滤掉非健康的 Dubbo 服务。代码如下:// SpringCloudRegistry.java
private void filterHealthyInstances(Collection<ServiceInstance> instances) {
filter(instances, new Filter<ServiceInstance>() {
public boolean accept(ServiceInstance data) {
// TODO check the details of status
// return serviceRegistry.getStatus(new DubboRegistration(data)) != null;
return true;
}
});
}
private <T> void filter(Collection<T> collection, Filter<T> filter) {
// remove if not accept
collection.removeIf(data -> !filter.accept(data));
}
private interface Filter<T> {
/**
* Tests whether or not the specified data should be accepted.
*
* @param data The data to be tested
* @return <code>true</code> if and only if <code>data</code>
* should be accepted
*/
boolean accept(T data);
}- 从
TODO check the details of status
可以看出,目前暂时未实现。后续,可能会根据状态进行过滤。
- 从
<2>
处,调用#buildURLs(URL consumerURL, Collection<ServiceInstance> serviceInstances)
方法,将 ServiceInstance 数组,转换成 URL 数组。代码如下:// SpringCloudRegistry.java
private List<URL> buildURLs(URL consumerURL, Collection<ServiceInstance> serviceInstances) {
// serviceInstances 为空,返回空数组
if (serviceInstances.isEmpty()) {
return Collections.emptyList();
}
// serviceInstances 非空,则逐个构建对应的 URL 对象,添加到 urls 中进行返回
List<URL> urls = new LinkedList<>();
for (ServiceInstance serviceInstance : serviceInstances) {
// 构建 URL 对象
URL url = buildURL(serviceInstance);
if (UrlUtils.isMatch(consumerURL, url)) {
urls.add(url);
}
}
return urls;
}
private URL buildURL(ServiceInstance serviceInstance) {
return new URL(serviceInstance.getMetadata().get(Constants.PROTOCOL_KEY),
serviceInstance.getHost(),
serviceInstance.getPort(),
serviceInstance.getMetadata());
}<3>
处,调用父#notify(URL url, NotifyListener listener, List<URL> urls)
方法,通知订阅者。
9.3.5 doUnsubscribe
实现 #doUnsubscribe(URL url, NotifyListener listener)
方法,取消订阅。代码如下:
// SpringCloudRegistry.java |
666. 彩蛋
😈 大体是这样,如果有些细节没写到位,欢迎知识星球留言。
比较有趣的,还是泛化调用那一块。