服务引用的总体逻辑
Dubbo服务引用的总体逻辑,可以用一张图来表示
从整体上看,Dubbo框架做服务消费也分为两大部分
1. 通过持有远程服务实例生成Invoker
2. 把Invoker通过动态代理转换成实现用户接口的动态代理引用
这里的Invoker承载了网络连接、服务调用和重试等功能,在客户端,它可能是一个远程的实现,也可能是一个集群实现
服务引用的入口请参考文章:Dubbo是如何搭上Spring的车的?
从源码出发
服务引用的初始化
从服务引用的入口进来,就是其初始化逻辑了。直接看ReferenceConfig
的init方法
private void init() {
// $-- 只允许init一次
if (initialized) {
return;
}
initialized = true;
if (interfaceName == null || interfaceName.length() == 0) {
throw new IllegalStateException("<dubbo:reference interface=\"\" /> interface not allow null!");
}
// $-- ConsumerConfig对象注入
// get consumer's global configuration
checkDefault();
appendProperties(this);
// $-- generic属性的处理
if (getGeneric() == null && getConsumer() != null) {
setGeneric(getConsumer().getGeneric());
}
if (ProtocolUtils.isGeneric(getGeneric())) {
// $-- 如果是泛化调用
interfaceClass = GenericService.class;
} else {
try {
// $-- 非泛化调用,通过反射实例化接口
interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
.getContextClassLoader());
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e.getMessage(), e);
}
// $-- 校验接口与方法
checkInterfaceAndMethods(interfaceClass, methods);
}
// $-- 文件服务调用配置的处理(是否配置有直连提供者)
String resolve = System.getProperty(interfaceName);
String resolveFile = null;
if (resolve == null || resolve.length() == 0) {
resolveFile = System.getProperty("dubbo.resolve.file");
if (resolveFile == null || resolveFile.length() == 0) {
File userResolveFile = new File(new File(System.getProperty("user.home")), "dubbo-resolve.properties");
if (userResolveFile.exists()) {
resolveFile = userResolveFile.getAbsolutePath();
}
}
if (resolveFile != null && resolveFile.length() > 0) {
Properties properties = new Properties();
FileInputStream fis = null;
try {
fis = new FileInputStream(new File(resolveFile));
properties.load(fis);
} catch (IOException e) {
throw new IllegalStateException("Unload " + resolveFile + ", cause: " + e.getMessage(), e);
} finally {
try {
if (null != fis) fis.close();
} catch (IOException e) {
logger.warn(e.getMessage(), e);
}
}
resolve = properties.getProperty(interfaceName);
}
}
if (resolve != null && resolve.length() > 0) {
url = resolve;
if (logger.isWarnEnabled()) {
if (resolveFile != null) {
logger.warn("Using default dubbo resolve file " + resolveFile + " replace " + interfaceName + "" + resolve + " to p2p invoke remote service.");
} else {
logger.warn("Using -D" + interfaceName + "=" + resolve + " to p2p invoke remote service.");
}
}
}
if (consumer != null) {
if (application == null) {
application = consumer.getApplication();
}
if (module == null) {
module = consumer.getModule();
}
if (registries == null) {
registries = consumer.getRegistries();
}
if (monitor == null) {
monitor = consumer.getMonitor();
}
}
if (module != null) {
if (registries == null) {
registries = module.getRegistries();
}
if (monitor == null) {
monitor = module.getMonitor();
}
}
if (application != null) {
if (registries == null) {
registries = application.getRegistries();
}
if (monitor == null) {
monitor = application.getMonitor();
}
}
// $-- 校验application
checkApplication();
// $-- 校验stub与mock
checkStubAndMock(interfaceClass);
// $-- 下面进行构建配置map,用于后续构建url
Map<String, String> map = new HashMap<String, String>();
Map<Object, Object> attributes = new HashMap<Object, Object>();
map.put(Constants.SIDE_KEY, Constants.CONSUMER_SIDE);
map.put(Constants.DUBBO_VERSION_KEY, Version.getProtocolVersion());
map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis()));
if (ConfigUtils.getPid() > 0) {
map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid()));
}
if (!isGeneric()) {
// $-- 非泛化调用
String revision = Version.getVersion(interfaceClass, version);
if (revision != null && revision.length() > 0) {
map.put("revision", revision);
}
String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
if (methods.length == 0) {
logger.warn("NO method found in service interface " + interfaceClass.getName());
map.put("methods", Constants.ANY_VALUE);
} else {
map.put("methods", StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
}
}
map.put(Constants.INTERFACE_KEY, interfaceName);
appendParameters(map, application);
appendParameters(map, module);
appendParameters(map, consumer, Constants.DEFAULT_KEY);
appendParameters(map, this);
String prefix = StringUtils.getServiceKey(map);
if (methods != null && !methods.isEmpty()) {
for (MethodConfig method : methods) {
appendParameters(map, method, method.getName());
String retryKey = method.getName() + ".retry";
if (map.containsKey(retryKey)) {
String retryValue = map.remove(retryKey);
if ("false".equals(retryValue)) {
map.put(method.getName() + ".retries", "0");
}
}
appendAttributes(attributes, method, prefix + "." + method.getName());
checkAndConvertImplicitConfig(method, map, attributes);
}
}
// $-- ip地址获取
String hostToRegistry = ConfigUtils.getSystemProperty(Constants.DUBBO_IP_TO_REGISTRY);
if (hostToRegistry == null || hostToRegistry.length() == 0) {
hostToRegistry = NetUtils.getLocalHost();
} else if (isInvalidLocalHost(hostToRegistry)) {
throw new IllegalArgumentException("Specified invalid registry ip from property:" + Constants.DUBBO_IP_TO_REGISTRY + ", value:" + hostToRegistry);
}
map.put(Constants.REGISTER_IP_KEY, hostToRegistry);
//attributes are stored by system context.
StaticContext.getSystemContext().putAll(attributes);
// $-- 创建ref代理
ref = createProxy(map);
ConsumerModel consumerModel = new ConsumerModel(getUniqueServiceName(), this, ref, interfaceClass.getMethods());
ApplicationModel.initConsumerModel(getUniqueServiceName(), consumerModel);
}
init方法的代码比较长,归结起来,主要做了以下几件事情:
- init次数控制,只允许init一次
- 服务参数、属性的读取,以及泛化调用、ip地址等的处理
- 本地文件url配置的处理
- 创建代理
init方法的大部分笔墨都在处理配置,其重点的远程服务引用功能,还是要看创建代理这一步,即createProxy
方法
private T createProxy(Map<String, String> map) {
// $-- temp://localhost?application=demo-consumer&check=false&dubbo=2.0.2&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello&pid=3376&qos.port=33333®ister.ip=192.168.0.105&side=consumer×tamp=1588948356581
URL tmpUrl = new URL("temp", "localhost", 0, map);
// $-- 判断是否是同一个JVM内部引用,默认非injvm引用
final boolean isJvmRefer;
if (isInjvm() == null) {
// $-- 如果指定了url,则非injvm
if (url != null && url.length() > 0) {
// if a url is specified, don't do local reference
isJvmRefer = false;
} else if (InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl)) {
// by default, reference local service if there is
isJvmRefer = true;
} else {
isJvmRefer = false;
}
} else {
isJvmRefer = isInjvm().booleanValue();
}
if (isJvmRefer) {
// $-- 使用injvm协议从内存中获取实例
URL url = new URL(Constants.LOCAL_PROTOCOL, NetUtils.LOCALHOST, 0, interfaceClass.getName()).addParameters(map);
invoker = refprotocol.refer(interfaceClass, url);
if (logger.isInfoEnabled()) {
logger.info("Using injvm service " + interfaceClass.getName());
}
} else {
// $-- 获取到了url,此url可能是点对点直连的url,也有可能是注册中心的url
if (url != null && url.length() > 0) {
// $-- 支持使用分号隔开指定的多个直连机器
String[] us = Constants.SEMICOLON_SPLIT_PATTERN.split(url);
if (us != null && us.length > 0) {
for (String u : us) {
URL url = URL.valueOf(u);
if (url.getPath() == null || url.getPath().length() == 0) {
url = url.setPath(interfaceName);
}
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
// $-- 是注册中心url(允许直连地址写成注册中心)
urls.add(url.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
} else {
// $-- 是点对点直连的url(直连某一台服务提供者)
urls.add(ClusterUtils.mergeUrl(url, map));
}
}
}
} else {
// $-- 未指定url,则从registry中获取注册中心url
List<URL> us = loadRegistries(false);
if (us != null && !us.isEmpty()) {
for (URL u : us) {
URL monitorUrl = loadMonitor(u);
if (monitorUrl != null) {
map.put(Constants.MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
}
// $-- 注册中心地址后添加refer存储服务消费元数据信息
urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
}
}
if (urls.isEmpty()) {
throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
}
}
if (urls.size() == 1) {
// $-- 单url,直接获取invoker
invoker = refprotocol.refer(interfaceClass, urls.get(0));
} else {
List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
URL registryURL = null;
for (URL url : urls) {
// $-- 逐个获取invoker,并添加到invokers列表
invokers.add(refprotocol.refer(interfaceClass, url));
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
registryURL = url; // use last registry url
}
}
// $-- 通过Cluster将多个Invoker转换成一个Invoker
if (registryURL != null) {
// registry url is available
// use AvailableCluster only when register's cluster is available
URL u = registryURL.addParameter(Constants.CLUSTER_KEY, AvailableCluster.NAME);
// $-- StaticDirectory只是将invokers外面包了一层皮
invoker = cluster.join(new StaticDirectory(u, invokers));
} else {
// not a registry url
invoker = cluster.join(new StaticDirectory(invokers));
}
}
}
Boolean c = check;
if (c == null && consumer != null) {
c = consumer.isCheck();
}
if (c == null) {
c = true; // default true
}
if (c && !invoker.isAvailable()) {
throw new IllegalStateException("Failed to check the status of the service " + interfaceName + ". No provider available for the service " + (group == null ? "" : group + "/") + interfaceName + (version == null ? "" : ":" + version) + " from the url " + invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());
}
if (logger.isInfoEnabled()) {
logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());
}
// create service proxy
return (T) proxyFactory.getProxy(invoker);
}
createProxy方法统筹了多种方式的服务引用,主要逻辑可以总结如下:
-
判断是否为injvm协议引用
- 如果是injvm协议,即本机内部调用,直接从内存中获取实例
- 如果为非injvm协议,则可能为点对点直连,也可能为注册中心url,分别按照对应的方式获取地址,然后进行引用
-
随后会判断是否开启了启动时校验(check参数),如果开启了,会校验服务是否可用
-
最后会创建服务代理
其中injvm的引用逻辑比较简单,这里就不浪费笔墨了。下面我们直接来看一下远程服务的引用。
远程服务的引用
远程服务引用的获取,分为两种情况。
- 如果只有一个url地址,则直接通过
refer
方法获取Invoker
即可 - 如果有多个url地址,则除了要逐个获取invoker,还要通过
Cluster
将多个Invoker
合并转换成一个Invoker
无论最终如何处理,都要首先通过refer
获得Invoker
对象,即代码
refprotocol.refer(interfaceClass, url);
我们使用Dubbo,一般都是使用注册中心的,因此这里我们主要看存在注册中心场景下的引用。
在存在注册中心的场景下,通过Dubbo SPI机制,最终会调用RegistryProtocol
的refer
方法(调用过程中的Filter和Wrapper不是必要逻辑,为了节省篇幅,这里省去不谈)。
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
// $--- 设置具体注册中心协议,如将registry://... 转换为了zookeeper://...形式
url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY);
// $-- 获取具体注册中心实例,如 ZookeeperRegistry(这里的registryFactory是通过Adaptive扩展生成的)
Registry registry = registryFactory.getRegistry(url);
if (RegistryService.class.equals(type)) {
return proxyFactory.getInvoker((T) registry, type, url);
}
// $-- 根据配置处理多分组聚合(支持对注册中心的服务进行分组调用)
// group="a,b" or group="*"
Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(Constants.REFER_KEY));
String group = qs.get(Constants.GROUP_KEY);
if (group != null && group.length() > 0) {
if ((Constants.COMMA_SPLIT_PATTERN.split(group)).length > 1
|| "*".equals(group)) {
return doRefer(getMergeableCluster(), registry, type, url);
}
}
// $-- 处理订阅数据并通过Cluster合并多个Invoker
return doRefer(cluster, registry, type, url);
}
refer
方法的处理,主要是先获取了具体的注册中心实例,然后判断是否配置了group分组,根据此配置,进行后续的doRefer
。
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
// $-- 将url包装为directory,directory是服务消费集群容错的核心
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
directory.setRegistry(registry);
directory.setProtocol(protocol);
// all attributes of REFER_KEY
Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
// $-- 组装消费的协议地址,形如 consumer://192.168.1.1/...
URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, parameters.remove(Constants.REGISTER_IP_KEY), 0, type.getName(), parameters);
if (!Constants.ANY_VALUE.equals(url.getServiceInterface())
&& url.getParameter(Constants.REGISTER_KEY, true)) {
// $-- 注册消费信息到注册中心
registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY, Constants.CONSUMERS_CATEGORY,
Constants.CHECK_KEY, String.valueOf(false)));
}
// $-- 订阅服务提供者(providers)、路由(routers)和动态配置(configurators)
directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY,
Constants.PROVIDERS_CATEGORY
+ "," + Constants.CONFIGURATORS_CATEGORY
+ "," + Constants.ROUTERS_CATEGORY));
// $-- 通过Cluster合并invokers
Invoker invoker = cluster.join(directory);
// $-- 将consumer invoker记录到内存map中
ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
return invoker;
}
doRefer
方法中,先创建了一个Directory
对象(Directory是Dubbo服务消费中集群容错的核心),随后填充组装Directory,创建出Invoker
。
这里为什么不直接创建Invoker,而要用到Directory这个中间对象呢?
原因是为了服务调用时的集群容错机制。这个我们在后续的文章中再详细介绍。
Invoker
是Dubbo服务消费中承上启下的一个关键点。这里进行的创建组装,涉及到集群相关知识,可以先Mark一下,后续再聊。
Invoker
创建时,有几个重要的逻辑,如下:
- 注册消费者信息到注册中心中
- 订阅服务提供者、路由和配置的信息
- 通过Cluster,将Directory包装成Invoker
关于注册消费者到注册中心,订阅注册中心上的服务提供者、路由和配置等信息,这些源码级别的分析,我们已经在服务暴露文章中介绍过了,这里就不再赘述了。
现在,我们主要关注一下Cluster
的join
方法,看看其主要功能。
这里的Cluster
也是Dubbo SPI生成的自适应扩展类,默认实现为 FailoverCluster
,并且被MockClusterWrapper
包装了一下
public class MockClusterWrapper implements Cluster {
private Cluster cluster;
public MockClusterWrapper(Cluster cluster) {
this.cluster = cluster;
}
@Override
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
return new MockClusterInvoker<T>(directory,
this.cluster.join(directory));
}
}
MockClusterWrapper
的join方法,是直接将内部的FailoverCluster进行join之后,包装成MockClusterInvoker
后返回
public class FailoverCluster implements Cluster {
public final static String NAME = "failover";
@Override
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
return new FailoverClusterInvoker<T>(directory);
}
}
FailoverCluster
方法的join也很简单,直接将directory利用FailoverClusterInvoker
进行包装返回。
而FailoverClusterInvoker
的构造函数中也无复杂逻辑,只是变量的赋值罢了。
至此,一个Invoker
的实例就生成了。接下来,就只剩下代理的创建了。
生成远程服务代理
生成远程服务代理,对应代码:
(T) proxyFactory.getProxy(invoker);
这里同样使用了Dubbo SPI机制。其中,proxyFactory被StubProxyFactoryWrapper
包装了一层,内部实际上调用的是AbstractProxyFactory
的getProxy方法。而AbstractProxyFactory
的getProxy方法实现,依赖于SPI配置,可以为Javassist
或JDK Proxy
的方式。
代理生成实现的逻辑并不复杂,这里就不赘述了。
总结
总结一下,Dubbo服务的引用,主要是将一些URL相关的信息,组装成一个Directory
对象,然后通过Cluster
连接成一个Invoker
,最后生成此Invoker
的代理给应用层调用。