以Zookeeper作为Dubbo服务的注册中心为例, 先来看看如何使用:
到webapps/ROOT/WEB-INF下,有一个dubbo.properties文件,里面指向Zookeeper ,使用的是Zookeeper 的注册中心
服务端配置
客户端配置:
从配置上看, 可以以ApplicationConfig,RegistryConfig,ServiceConfig,ReferenceConfig这几个类为入口来分析.
这几个类主要存放配置信息, 需要关注:
1, dubbo是如何将配置类转变为spring上下文中的bean,
2, 如何暴露服务,
3, 在暴露服务的时候,
4, 是如何在zookeeper上注册的,
5, 客户端是如何发现服务的,
6, 如何发起远程服务调用的,
7, 服务端在收到请求之后, 是如何找到对应的服务的?
1,spring 配置读取,解析, 再到生成bean, 放到spring上下文的过程.
dubbo自定义了名称空间"dubbo",
spring支持自定义名称空间, 需要以下几步操作
registerBeanDefinitionParser("service",new DubboBeanDefinitionParser(ServiceBean.class,true));
说明 dubbo:service, 最终会生成ServiceBean, 解析转换的细节是spring的源码范畴, 不再深究.
其中, dubbo.xsd 是xsd定义文件, spring.handlers, 指定了dubbo名称空间节点解析器,spring.schemas配置告诉名称空xsd文件在哪里.从com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler中可以看到配置节点对应生成的bean实例:
registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
我们着重来看ServiceBean和ReferenceBean, 这两个分别涉及到服务的暴露及引用.
从ServiceBean的类继承关系及实现接口来看:
public class ServiceBean
继承自ServiceConfig将得到配置属性及暴露服务等相关方法.
实现InitializingBean接口, spring在实例化完成之后, 将自动调用afterPropertiesSet方法做初始化
实现DisposableBean接口, spring在容器销毁的时候, 会调用destroy方法.
实现ApplicationContextAware接口, spring会给这个bean注入ApplicationContext, serviceBean中通过applicationContext抓了很多bean注进来.
实现了ApplicationListener接口, 会监听spring的特有的应用生命周期事件 onApplicationEvent, ServiceBean监听ContextRefreshedEvent事件, 再上下文初始化完成之后, 如果服务未暴露(export)再暴露一下.
实现了BeanNameAware 接口, 将beanName设置为beanid.
从serviceBean的afterPropertiesSet逻辑可以看出, 在读取配置到ServiceConfig后, 在上下文中, 根据ServiceConfig配置属性找到对应的bean注入, 完了调用ServiceConfig的export() 方法暴露服务.
export() 方法做了很多初始化属性(找相关bean来注入), 某些属性如果未配置, 使用默认值注入, 还有就是一些校验逻辑.
继续跟踪到export 跟踪到doExportUrls():
private void doExportUrls() {
List
for (ProtocolConfig protocolConfig : protocols) {
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
先获取所有注册中心地址, 可能配置了多个, 就是下面这个配置
断点调试查到registryURLs 为:
registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=hello-world-app&check=false&dubbo=2.0.0&pid=47708®istry=zookeeper&subscribe=false×tamp=1467342589223
然后根据配置的协议(protocols), 来暴露服务, 如果未配置协议, 默认的是:dubbo
配置在dubbo-default.properties dubbo.provider.protocol=dubbo.
protocols 会从ProviderConfig里取,在ServiceConfig判空写默认值的时候, 实例化了ProviderConfig, 完了会给这个实例写入默认值, 其中的protocols就是从默认配置文件里取的.
private void checkDefault() {
if (provider == null) {
provider = new ProviderConfig();
}
appendProperties(provider);
}
再回到doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List
1, 方法一开始先从protocolConfig上取协议名称, 如果取不到仍然默认协议为"dubbo"
================2~5步为了得到需要暴露的服务的主机IP=======================
2, 从protocolConfig上取host, 取不到, 从provider上取host. (对应dubbo:protocol的host属性, 官方手册解释为
服务主机名,多网卡选择或指定VIP及域名时使用,为空则自动查找本机IP,-建议不要配置,让Dubbo自动获取本机IP)
3, 如果host是无效的本地地址(isInvalidLocalHost):
host == null
|| host.length() == 0
|| host.equalsIgnoreCase("localhost")
|| host.equals("0.0.0.0")
|| (LOCAL_IP_PATTERN.matcher(host).matches());
通过InetAddress.自动获取本机IP.
4, 如果仍然是无效的或者是本地地址, 遍历注册中心的url, 发起socket连接, 然后通过socket.getLocalAddress().getHostAddress()得到主机地址
5, 还是得不到无效的或者是本地地址的话, 通过NetUtils.getLocalHost()得到主机ip, 这个方法通过遍历本地网卡,返回第一个合理的IP。
================2~5步为了得到需要暴露的服务的主机IP=======================
================6~7 为了得到端口========================
6, 从protocolConfig上取端口, 取不到从provider上取, 仍然取不到的话, 从dubboProtocol上获取默认端口(20880)
final int defaultPort = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(name).getDefaultPort();
上面分析了这个name, 如果没配置的话, 取的dubbo. 这个Extension取到的是DubboProtocol.
7, 如果配置了协议但是XxxProtocol上没有默认端口, 那就随机生成一个端口.通过NetUtils.getAvailablePort(defaultPort)取得.
================6~7 为了得到端口========================
8, 准备一些公共默认参数值, 如Constants.SIDE_KEY,Constants.DUBBO_VERSION_KEY,Constants.TIMESTAMP_KEY等, 写入各核心实例(application,module,provider,protocolConfig,serviceConfig等)
9,如果有配置method子标签, 如:
10, 遍历method子标签
====================9~10 处理method及其argument子标签, 看配置手册, 还有parameter子标签, 这里没处理.
11, 如果是泛化实现. 往上面那个map写入标记generic = true;methods Constants.ANY_VALUE (就是一个*号, 表示任意方法)
(
泛接口实现方式主要用于服务器端没有API接口及模型类元的情况,参数及返回值中的所有POJO均用Map表示,通常用于框架集成,比如:实现一个通用的远程服务Mock框架,可通过实现GenericService接口处理所有服务请求。)
如果不是泛化实现, 给map写入revision, 取的是接口类的版本. 然后看看这个接口有没有包装器类, 要把所有包装器类的method都加进来, 用逗号隔开拼一个字符串写入methods
12, 如果有配置要求token, 默认的话, 随机给一个uuid, 写入map, 否则就用配置给定的token值. token作令牌验证用的.
13, 如果协议是"injvm", 就不需要注册.并且给map标记notify=false
14, 从protocolConfig上取得ContextPath, 如果为空, 从providerConfig上取.
15, 用host, port, contextPath等创建URL.
16, 如果这个url使用的协议(如dubbo)存在ConfiguratorFactory的扩展, 调用对应的扩展来配置修改url
目前只有override,absent, 用于向注册中心修改注册信息.
override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&timeout=1000
17, 从url中获取scope信息, 如果scope=none 啥都不做, 不暴露服务.
18, 如果scope != remote, 就在本地暴露服务
if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
exportLocal(url);
}
但是exportLocal方法里头, 只有当url不是injvm(LOCAL_PROTOCOL)的时候, 才去做一些暴露操作
也就是说injvm 在exportLocal里什么都不做
if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
URL local = URL.valueOf(url.toFullString())
.setProtocol(Constants.LOCAL_PROTOCOL)
.setHost(NetUtils.LOCALHOST)
.setPort(0);
Exporter> exporter = protocol.export(
proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
exporters.add(exporter);
logger.info("Export dubbo service " + interfaceClass.getName() +" to local registry");
}
19, 如果scope配置不是local. 遍历每一个注册中心url
如果存在monitor, 则在url参数里添加monitor信息.
20, 通过proxyFactory将url, 接口类型转化成invoker
proxyFactory在这里是由javassist实现的, 也就是JavassistProxyFactory
@SPI("javassist")
public interface ProxyFactory{
在JavassistProxyFactory的getInvoker中, 会去找这个接口类的Wrapper类
21, 通过protocol将invoker暴露出去
Exporter> exporter = protocol.export(invoker);
这里protocol 根据url中的registry协议, 尝试去获取RegistryProtocol
Protocol是如何知道要根据url的协议来创建Protocol?
在ServiceConfig中, Protocol一开始初始化是:
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
这说明是在调用方法的时候, 再决定用哪个扩展
下面是javassist生成的Protocol$Adpative字节码代码, 一目了然.
从下面这个调用链可以看出, registryProtocol不存在, 然后去创建, 然后注入各种属性, 属性实例不存在, 又调用各种create扩展的方法, ..
最后得到RegistryProtocol, 并执行其export方法.
断点执行时发现, 在调用registryProtocol的export方法之前, 还调用了两个Protocol的Wrapper:
根据ExtensionLoader里的逻辑可知, 只要某个类实现了Protocol接口, 又有Protocol类型入参的构造方法, 可认为此类是Protocol的包装器类.
ExtensionLoader的getExtension方法只返回最后最外层的包装器类
所以从上图调用关系来看,RegistryProtocol的export方法最后才被执行.
这两个包装器类主要用来添加过滤器及监听器.
RegistryProtocol的export方法:
1, 先做本地服务暴露(doLocalExport()),调用DubboProtocol export一下.
完了之后, 将"dubbo://192.168...."这个url作为key,绑定刚才暴露返回的exporter.
DubboProtocol export过程涉及底层具体协议(如Netty)创建服务的细节,不再深入探讨.
2,获得注册中心对象Registry(registryFactory根据协议zookeeper,知道返回的是ZookeeperRegistry
3, 可以看到往zookeeper上注册,实际就是调用zkClient往zk树上创建一个路径节点.
zkClient.create(toUrlPath(url),url.getParameter(Constants.DYNAMIC_KEY,true));
4,根据"dubbo://...."的注册url, 转换得到一个"provider://..."的URL, 绑定到一个OverrideListener, registry 订阅这个provider url的变化通知,并交由对应的listener处理.
5,最后实例化一个Exporter返回.
ServiceConfig 最后将exporter缓存, 至此, 以上便是服务暴露的过程.
总结一下, 就是在配置解析读取装配成bean之后, 初始化, 根据配置协议, 找到注册中心(如Zookeeper)注册, 找到对应服务Protocol(如DubboProtocol)暴露服务.
下面再来看看服务引用的过程:
从ReferenceBean看起, 这个类继承自ReferenceConfig, 因此可以得到关于dubbo:Reference的配置属性, 实现了FactoryBean,ApplicationContextAware,InitializingBean,DisposableBean接口:
实现FactoryBean接口, 定制实例化bean的逻辑, 可以定制返回的bean实例,返回是否单例, 返回实例的类型.
实现ApplicationContextAware, spring会给注入ApplicationContext
实现InitializingBean, 会自动调用afterPropertiesSet, 完成实例化之后的一些初始化工作
实现DisposableBean接口, spring会自动调用destroy方法, 做资源销毁或者回收操作.
ReferenceBean的配置解析装配没啥好说的, 主要看afterPropertiesSet方法, 初始化做了哪些事情:
afterPropertiesSet中貌似仍然做了一大堆初始化的事情, 一直到最后:
if (b != null && b.booleanValue()) {
getObject();
}
getObject();会触发ReferenceConfig的init方法:
public synchronized T get() {
if (destroyed){
throw new IllegalStateException("Already destroyed!");
}
if (ref == null) {
init();
}
return ref;
}
init 逻辑:
写入默认值之类的逻辑不再详细说明,核心逻辑:
ref=createProxy(map);
map 写了很多配置参数及公共参数, 默认参数等等.
通过注册中心配置拼装URL
List
调试过程中,抓取的us值
registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=consumer-of-helloworld-app&check=false&dubbo=2.0.0&pid=408500®istry=zookeeper×tamp=1467478634195
invoker=refprotocol.refer(interfaceClass,urls.get(0));
这里 refprotocol 是RegistryProtocol, 因为传入的url参数是registry协议
Protocol refprotocol=ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
RegistryProtocol中的refer方法:
1, 将"registry://..."协议的url转成"zookeeper://..."的协议url
2, registryFactory据此得到对应的Registry类, 我们使用zookeeper做实验, 实际就是ZookeeperRegistry
3,调用doRefer方法,
cluster实际是FailoverCluster(Cluster接口上有@SPI(FailoverCluster.NAME))
join方法直接返回new FailoverClusterInvoker
这里给interfaces接口创建代理(字节码实例), 并传入InvokerInvocationHandler实例化. 具体字节码代理生成逻辑有待深究.
DemoService demoService = (DemoService)context.getBean("demoService");
得到的bean实际就是这个代理实例
当调用时, String hello = demoService.sayHello("world");
实际发起调用的是包装了invoker的InvokerInvocationHandler对象.
下面是断点调试的截图, 可以发现InvokerInvocationHandler中的invoker变成了MockClusterInvoker, 是由代理实例生成的, 暂时不知道为啥传入的是MockClusterInvoker
跟进到MockClusterInvoker 的invoker继续观察, 可以看到这时的invoker是FailoverClusterInvoker, 怀疑是在字节码代理类中使用了包装器类
继续跟进, 由于FailoverClusterInvoker中没有invoke方法, 可以找到是在父类AbstractClusterInvoker中实现的.
父类做了一些负载均衡的逻辑, 最后调用doInvoke抽象方法, 在FailoverClusterInvoker中实现的:
doInvoke从父类的select方法得到一个负载均衡计算后的invoker, 并调用:
Result result = invoker.invoke(invocation);
继续往上跟踪, 一直跟到DubboInvoker的doInvoke方法
这个方法里头使用ExchangeClient发起远程调用
return (Result)currentClient.request(inv,timeout).get();
这个再往下挖, 就是基于具体rpc协议(如netty)层面上的调用了, 不再此专题细究.
总结一下, 客户端发起远程调用的大致思路是:
将配置转化为子节码生成的代理实例存到spring上下文中, 调用时, 通过代理实例, 找到相关协议方法发起远程调用.