参考资料:
总结
主线:参数检查,配置Map -> 构建URL对象,按照协议生成Invoker -> 动态代理封装Invoker
具体过程:
- 检查各种配置,把参数添加到Map里面
- 把map转成参数字符串拼接到URL对象上。根据URL上的协议,利用dubbo spi机制调用对应Protocol#refer创建Invoker
RegistryProtocol#refer
,获取注册中心实例,注册消费者信息,以zk为例,在协议/服务接口/consumers目录下创建consumer节点。接着,订阅provider、configurators和routers目录,订阅完成会触发DubboProtocol#referDubboProtocol#refer
,初始化客户端连接,默认是NettyClient,返回DubboInvoker- 如果多个URL对象,先遍历生成Invoker对象,然后经过StaticDirectory封装一下,使用Cluster#join合并,对外暴露一个Invoker,方便调用
- 利用动态代理,封装Invoker,屏蔽调用细节,返回代理对象
一些思考
这段内容源自Dubbo系列之服务订阅(2),从宏观的角度讲述了调用远程服务的过程和细节,看完这个会对服务订阅有了初步认识。
-
如果要实现远程服务的调用,那么就必须建立网络连接。因为我们需要与特定的目标通信,所以使用TCP连接,除此以外,还要打开并监听端口,我们封装一个类实现这些操作,叫做
ExchangeClient
。 -
拿到ExchangeClient后,我们就可以远程连接。但是,我们更加方便沟通交流,还需要定义一个协议,比如dubbo。于是,搞了个
DubboInvoker
-
可以通信了,但是,我还想在本地增加一些过滤器。那就通过责任链模式,把Filter与DubboInvoker连接起来,返回一个
ProtocolFilterWrapper
-
同理,如果还想加上一些监听器,同样再进行一次封装,返回
ListenerInvokerWrapper
-
考虑到一个服务有多个提供者,我给每个提供者都创建一个ListenerInvokerWrapper,如下所示
Demoservice::196.254.324.1 ListenerInvokerWrapper1 Demoservice::196.254.324.2 ListenerInvokerWrapper2 Demoservice::196.254.324.3 ListenerInvokerWrapper3
-
这样本地创建太费事,我们引入注册中心。直接把服务注册到注册中心,然后消费者从注册中心拉取服务对象。拉取时,这些服务都在某个目录下,我们称为
RegistryDirectory
,这个服务目录包含所有远程服务调用对象ListenerInvokerWrapper。我们在RegistryDirectory弄一个Map,叫做urlInvokerMap
,保存所有的远程服务调用对象,key是Demoservice::ip -
我们可以在本地通过调用RegistryDirectoy对象,实现远程通信。但是每次调用都需要手动选择调用哪个,非常麻烦,而且造成流量不均。想到这里,我们封装一个负载均衡类来解决,叫做LoadBalance,定义一个select方法,
LoadBalance.select(registryDirectory)
自动选择一个服务调用。负载均衡策略包括:一致性Hash、随机、轮询、最少活跃数、最短响应时间 -
虽然可以自动调用,但是调用失败能不能重试,或者直接失败。那我们把具备这种能力的服务调用,称为集群(Cluster),定义个join方法,
Cluster.join(registry)
,返回一个可以快速失败或者重试的服务调用对象 -
目前,我们已经拥有一个XXXclusterInvoker对象,它具有快速失败或者重试等功能,且具有负载均衡算法的远程服务调用对象。但是,我还想在远程服务调用失败后,可以在本地模拟调用,返回一个mock对象,那么我们对XXXclusterInvoker重新封装,命名为MockClusterInvoker,具有Mock功能,持有服务目录RegistryDirectory和XXXclusterInvoker对象
-
至此,我们的MockClusterInvoker已经比较完善。不过,为了屏蔽远程调用的细节,使得用户可以像本地调用般无感知,我们通过jdk动态代理返回代理对象,在拦截方法invoke中调用MockClusterInvoker的方法,完成远程调用。
最终在消费者端呈现出一个远程服务代理对象如下图:
服务订阅入口
经过上篇文章的分析,无论是xml方式,还是注解方式,服务订阅入口都是ReferenceConfig#get()
。我们发现,这个方法核心是内部调用init方法。
public synchronized T get() {
if (destroyed) {
throw new IllegalStateException("Already destroyed!");
}
if (ref == null) {
init();
}
return ref;
}
由于init方法实在太长,大部分都是检查配置,然后往Map里面填充各种属性,下图是部分代码,以及构建完的map内容
private void init() {
// .... 省略代码
StaticContext.getSystemContext().putAll(attributes);
// 重点:创建代理对象
ref = createProxy(map);
ConsumerModel consumerModel = new ConsumerModel(getUniqueServiceName(), this, ref, interfaceClass.getMethods());
ApplicationModel.initConsumerModel(getUniqueServiceName(), consumerModel);
}
createProxy
接下来是重点方法com.alibaba.dubbo.config.ReferenceConfig#createProxy
,由于方法同样比较长,我会删除掉部分非重点代码。
/**
* 核心方法,通过配置参数的元信息,创建一个代理对象
*
**/
private T createProxy(Map<String, String> map) {
// 创建临时URL
// temp://localhost?application=bm-officeAuto-asset&check=false&dubbo=2.8.4.BM-SNAPSHOT...
URL tmpUrl = new URL("temp", "localhost", 0, map);
final boolean isJvmRefer;
// 这一段都是用来判断是否引用本地服务
if (isInjvm() == null) {
// 指定URL的情况下,不做本地引用
if (url != null && url.length() > 0) {
isJvmRefer = false;
} else if (InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl)) {
// 默认情况下如果本地有服务暴露,则引用本地服务.
isJvmRefer = true;
} else {
isJvmRefer = false;
}
} else {
isJvmRefer = isInjvm().booleanValue();
}
if (isJvmRefer) {
// 构建本地协议的URL
URL url = new URL(Constants.LOCAL_PROTOCOL, NetUtils.LOCALHOST, 0, interfaceClass.getName()).addParameters(map);
// 本地服务引入,就是去之前服务暴露的exported
invoker = refprotocol.refer(interfaceClass, url);
} else {
// 用户指定url,有可能是点对点直连,也有可能是注册中心地址
if (url != null && url.length() > 0) {
// 获取到Reference配置的url,多个url用分号分隔(;)
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,将map转成查询字符串,然后使用refer作为参数的key拼接到url上。即xx?refer=map查询字符串
urls.add(url.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
} else {
// 如果是点对点,合并url,移除服务提供者的一些配置
urls.add(ClusterUtils.mergeUrl(url, map));
}
}
}
} else {
// 不满足上面两种情况,获取注册中心地址。与服务暴露不同,参数是false,表示不是provider
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()));
}
// 将map转成查询字符串,然后使用refer作为参数的key拼接到url上。即xx?refer=map查询字符串
urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
}
}
....
}
/******** 往上都是在构建URL对象,把map作为参数 *************/
/******** 往下就是根据URL的协议,调用refer方法,创建Invoker *************/
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) {
// 多个url,各自转成invoker后保存到集合
invokers.add(refprotocol.refer(interfaceClass, url));
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
registryURL = url; // use last registry url
}
}
// 创建StaticDirectory实例,并由cluster对多个invoker进行合并,只暴露出一个invoker,便于使用
if (registryURL != null) { // registry url is available
// use AvailableCluster only when register's cluster is available
URL u = registryURL.addParameterIfAbsent(Constants.CLUSTER_KEY, AvailableCluster.NAME);
invoker = cluster.join(new StaticDirectory(u, invokers));
} else { // not a registry url
invoker = cluster.join(new StaticDirectory(invokers));
}
}
}
......
// 通过动态代理封装Invoker,返回代理对象
// create service proxy
return (T) proxyFactory.getProxy(invoker);
}
小结:
createProxy()
方法主要做的事情:1.先获取一个或多个注册中心的URL,然后把map转成字符串作为url的参数,key是refer;(这一步和服务暴露类似,构建URL对象,注册自身信息)
2.根据URL对象上的协议,调用对应的
protocol.refer
方法得到invoker3.把invoker添加到集合中,使用StaticDirectory封装,通过Cluster进行合并,对外暴露出一个invoker
4.使用动态代理,封装invoker调用细节,返回代理对象
protocol#refer
我们已经知道大致的服务引入流程,不过,还有些细节还需要继续发掘,比如生成的Invoker到底是什么,消费者又是怎么获取提供者的数据。要解决这些疑惑,都离不开protocol#refer
,根据协议不同,可以分为Registry类型的Protocol,和非Registry类型的Protocol。
RegistryProtocol#refer
我们先来看下com.alibaba.dubbo.registry.integration.RegistryProtocol#refer
@Override
@SuppressWarnings("unchecked")
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
// 获取registry的参数值,设置为URL协议头
// 未替换前:registry://ip:port/path?registry=zookeeper&key=value
// 替换后:zookeeper://ip:port/path?key=value
url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY))
.removeParameter(Constants.REGISTRY_KEY);
// 获取注册中心实例,一般是ZookeeperRegistry,包括zkClient、registryUrl
Registry registry = registryFactory.getRegistry(url);
if (RegistryService.class.equals(type)) {
return proxyFactory.getInvoker((T) registry, type, url);
}
// .... 省略部分代码
// 真正的refer
return doRefer(cluster, registry, type, url);
}
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
// 创建RegistryDirectory,就是我们之前提到的服务目录
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
// 设置注册中心实例
directory.setRegistry(registry);
// 设置动态生成的Protocol$Adaptive
directory.setProtocol(protocol);
// all attributes of REFER_KEY
// 获取url上的参数,封装成map
Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
// 构建订阅URL,以consumer//开头
// url:consumer://10.0.0.113/cn.com.bluemoon.service.portal.service.PortalAppService?
// application=bm-officeAuto-asset&check=false
// &interface=cn.com.bluemoon.service.portal.service.PortalAppService&methods=insertUserRoleTable,queryAPIList
// &pid=7252&revision=0.0.8&sendmsg=false&side=consumer&timeout=60000×tamp=1608728137056
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)) {
URL registeredConsumerUrl = getRegisteredConsumerUrl(subscribeUrl, url);
// 向注册中心注册消费者,实际上就是在consumers目录下创建新节点
registry.register(registeredConsumerUrl);
directory.setRegisteredConsumerUrl(registeredConsumerUrl);
}
// 订阅服务,并且会拉取远程服务Invoker到directory对象的urlInvokerMap
// 监听providers目录、configurators目录和routers目录。订阅完,会触发DubboProtocol#refer方法
directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY,
Constants.PROVIDERS_CATEGORY
+ "," + Constants.CONFIGURATORS_CATEGORY
+ "," + Constants.ROUTERS_CATEGORY));
// 封装多个Invoker,对外暴露一个,方便使用。得到是MockClusterInvoker
Invoker invoker = cluster.join(directory);
// 在提供者消费者服务表中记录这个信息
ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
return invoker;
}
小结:
RegistryProtocol#refer
方法主要是获取注册中心实例,接着调用deRefer
方法,生成一个服务目录RegistryDirectory,然后向注册中心注册自身信息,以zk为例,在服务接口的目录下创建consumer节点。接着,监听providers、configurators和routers节点,然后RegistryDirectory会拉取节点的最新数据,触发DubboProtocol#refer方法,生成DubboInvoker,消费者与提供者建立TCP连接。具体订阅过程参考:Dubbo系列之服务订阅(3)
DubboProtocol#refer
com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol#refer
@Override
public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {
optimizeSerialization(url);
// create rpc invoker.
// 根据url获取远程客户端,返回一个DubboInvoker
// 关键是getClients()
DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
invokers.add(invoker);
return invoker;
}
private ExchangeClient[] getClients(URL url) {
// whether to share connection
boolean service_share_connect = false;
int connections = url.getParameter(Constants.CONNECTIONS_KEY, 0);
// if not configured, connection is shared, otherwise, one connection for one service
if (connections == 0) {
service_share_connect = true;
connections = 1;
}
ExchangeClient[] clients = new ExchangeClient[connections];
for (int i = 0; i < clients.length; i++) {
if (service_share_connect) {
// 获取共享的客户端。过程是通过远程地址找到client,如果远程地址还没有客户端就执行initClients()
clients[i] = getSharedClient(url);
} else {
// 得到初始化的客户端
clients[i] = initClient(url);
}
}
return clients;
}
/**
* Create new connection
*/
private ExchangeClient initClient(URL url) {
// client type setting.
// 获取客户端类型,默认是netty
String str = url.getParameter(Constants.CLIENT_KEY,
url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_CLIENT));
url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME);
// enable heartbeat by default
url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));
// BIO is not allowed since it has severe performance issue.
.......
ExchangeClient client;
try {
// connection should be lazy
if (url.getParameter(Constants.LAZY_CONNECT_KEY, false)) {
// 懒连接
client = new LazyConnectExchangeClient(url, requestHandler);
} else {
// 连接远程
client = Exchangers.connect(url, requestHandler);
}
} catch (RemotingException e) {
throw new RpcException("Fail to create remoting client for service(" + url + "): " + e.getMessage(), e);
}
return client;
}
看下获取到的clients:
小结:
DubboProtocol#refer
主要是获取到远程客户端,然后封装返回一个DubboInvoker。