欢迎关注公众号【sharedCode】致力于主流中间件的源码分析, 可以直接与我联系
博主个人网站:www.shared-code.com
前言
上文中介绍了dubbo的服务暴露过程,服务暴露之后,接下来就是消费者如何来引用服务,本文主要是解读一下dubbo消费者引用服务的原理
RefenenceBean
在消费者项目中,引用一个外部的服务,就会生成一个ReferenceBean实例,由于ReferenceBean实现了FactoryBean,所以直接看一下getObject方法,
看看这个Bean是如何生成的
@Override
public Object getObject() throws Exception {
return get();
}
调用了父类ReferenceConfig的get方法
//ReferenceConfig.java
public synchronized T get() {
if (destroyed) {
// 如果destroyed == true , 那么说明这个类已经准备销毁了。
throw new IllegalStateException("Already destroyed!");
}
if (ref == null) {
// 如果引用为空,那么需要初始化,调用init方法。
init();
}
return ref;
}
从上面可以看出,大部分的逻辑都是在init里面 , 在这里,同时也说明了一个问题,消费者引用服务,这个逻辑是发生在ReferenceBean创建的时候。
ReferenceConfig.init()
private void init() {
if (initialized) {
return;
}
// ...
// 省略代码200行
// ....
// 重点在这个方法,创建一个代理类,也就是我们的服务引用类
ref = createProxy(map);
// 建立ConsumerModel,放入全局的MAP中。
ConsumerModel consumerModel = new ConsumerModel(getUniqueServiceName(), this, ref, interfaceClass.getMethods());
ApplicationModel.initConsumerModel(getUniqueServiceName(), consumerModel);
}
ReferenceConfig.createProxy()
private T createProxy(Map<String, String> map) {
URL tmpUrl = new URL("temp", "localhost", 0, map);
final boolean isJvmRefer; // 是否需要JVM级别的调用
if (isInjvm() == null) {
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();
}
// 上文我们说过,dubbo是本地暴露和远程暴露都会进行的,如果服务的消费者和服务提供者跑在同一个JVM里面,如:同一个Tomcat中,
// 那么,dubbo会优先使用本地暴露,因为这个就是直接JVM级别的交互,速度会更加快速
if (isJvmRefer) {
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 {
if (url != null && url.length() > 0) { // 是否是直接指定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())) {
urls.add(url.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
} else {
urls.add(ClusterUtils.mergeUrl(url, map));
}
}
}
} else {
// 到了这个地方,就是说是需要从注册中心获取服务提供者的调用地址了,
// 获取注册中心地址
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()));
}
// 构建注册中心的URL ,如:registry://xxxxx这种。
urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
}
}
if (urls == null || 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) {
// 一般来说,就是一个注册中心的地址,走这里,调用refer方法
invoker = refprotocol.refer(interfaceClass, urls.get(0));
} else {
// 多个注册中心的,目前暂未去了解
List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
URL registryURL = null;
for (URL url : urls) {
invokers.add(refprotocol.refer(interfaceClass, url));
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
registryURL = url; // use last registry url
}
}
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);
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; // 如果没有设置,那么就会启动启动时检查
}
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());
}
// 获取服务引用的代理类、
return (T) proxyFactory.getProxy(invoker);
}
说明:
上面的核心代码主要是在下面这一行代码,连接注册中心,集群容错模式,连接dubbo服务提供者,建立长连接,生成服务引用对象。
invoker = refprotocol.refer(interfaceClass, urls.get(0));
refprotocol的定义如下
private static final Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
如果已经看了SPI拓展机制的原理的,就会知道,上面的这个代码会返回一个Protocol$Adaptive实现类,在执行该类的方法的时候,还要看传入的参数URL的协议是什么,虽然默认Protocol的协议是dubbo,但是如果url里面是其他的协议,那么就会以URL的为准
url的样例如下:
registry://localhost:2181/com.alibaba.dubbo.registry.RegistryService?application=dubbo-consumer&dubbo=2.6.2&pid=10768&refer=application%3Ddubbo-consumer%26check%3Dfalse%26dubbo%3D2.6.2%26interface%3Dcom.dubbo.service.user.UserFacadeService%26methods%3DgetUser%26pid%3D10768%26register.ip%3D192.168.59.3%26side%3Dconsumer%26timestamp%3D1540205102302®istry=zookeeper×tamp=1540205144720
相关链接:
dubbo系列之内核SPI-Protocol扩展类生成(九)
调用refer方法,他的执行链如下, 前面是哪个类的执行顺序是不确定的,但是最终都会执行到RegistryProtocol 这个类里面去。
ProtocolListenerWrapper > ProtocolFilterWrapper > QosProtocolWrapper > RegistryProtocol
RegistryProtocol
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
// 构建zookeeper连接地址
url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY);
// 连接zk
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);
}
}
// 创建服务引用。
return doRefer(cluster, registry, type, url);
}
url的样例如下:
zookeeper://localhost:2181/com.alibaba.dubbo.registry.RegistryService?application=dubbo-consumer&dubbo=2.6.2&pid=10768&refer=application%3Ddubbo-consumer%26check%3Dfalse%26dubbo%3D2.6.2%26interface%3Dcom.dubbo.service.user.UserFacadeService%26methods%3DgetUser%26pid%3D10768%26register.ip%3D192.168.59.3%26side%3Dconsumer%26timestamp%3D1540205102302×tamp=1540205144720
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
// 构建注册目录管理类,用来管理和zk的连接,长连接断开后,进行重试
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
directory.setRegistry(registry);
directory.setProtocol(protocol);
// 请求参数。
Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
// 需要订阅的url
URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, parameters.remove(Constants.REGISTER_IP_KEY), 0, type.getName(), parameters);
// consumer://192.168.59.3/com.dubbo.service.user.UserFacadeService?application=dubbo-consumer&category=
//providers,configurators,routers&check=false&dubbo=2.6.2&interface=com.dubbo.service.user.UserFaca
// deService&methods=getUser&pid=10768&side=consumer×tamp=1540205102302
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)));
}
// 订阅
directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY,
Constants.PROVIDERS_CATEGORY
+ "," + Constants.CONFIGURATORS_CATEGORY
+ "," + Constants.ROUTERS_CATEGORY));
// 集群容错功能加强,根据集群容错策略,生成新的Invoker,方便执行容错策略,这个后面会详细讲解。
Invoker invoker = cluster.join(directory);
// 注册消费者,将Invoker添加到consumerInvokers里面去。
ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
return invoker;
}
每个消费者注册到zk上的节点名称就是下面这个,也就是subscribeUrl , consumer://192.168.59.3/com.dubbo.service.user.UserFacadeService
下面这行代码,主要就是为了当和zk连接断开时,由于订阅了consumer://192.168.59.3/com.dubbo.service.user.UserFacadeService这个节点,所以可以监听到连接状态,可以进行重试,同时监听和服务提供者的连接状态,当和服务提供者的长连接断开时,也会进行重新连接。
directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY,
Constants.PROVIDERS_CATEGORY
+ "," + Constants.CONFIGURATORS_CATEGORY
+ "," + Constants.ROUTERS_CATEGORY));
执行完上面的代码之后,在zk上可以看到如下目录:
总结:
本文其实主要就是介绍了一个流程,具体的细节上没有过多谈及, 总体上的流程如下:
1.初始化ReferenceBan,执行init方法
2.对消费者这边的一些参数做解析
3.判断是否需要进行本地调用,也就是JVM级别的调用。
4.生成注册中心的注册地址,执行Protocol扩展实现类的refer方法
5.在refer方法里面,最终调用到RegistryProtocol这个类里面,然后建立和zk的连接,订阅消费者的zk节点
6.生成RegistryDirectory注册管理类,监听zk的节点变化,刷新Invoker都是这个类来做的,后面会详细介绍
7.根据选择的集群容错策略,对Invoker进行代码加强
消费者引用服务这个功能还会再写一篇,下文中,主要讲解消费者和服务提供者建立长连接,和zk的连接,异步通知等。
欢迎关注公众号【sharedCode】致力于主流中间件的源码分析, 可以直接与我联系
博主个人网站:www.shared-code.com