以Zookeeper作为Dubbo服务的注册中心为例, 先来看看如何使用:
到webapps/ROOT/WEB-INF下,有一个dubbo.properties文件,里面指向Zookeeper ,使用的是Zookeeper 的注册中心
服务端配置
<dubbo:application name="dubbo_provider"></dubbo:application>
<dubbo:registry address="zookeeper://127.0.0.1:2181" check="false" subscribe="false" register=""></dubbo:registry>
<!-- 暴露服务-->
<dubbo:service interface="cn.test.dubbo.registry.service.TestRegistryService" ref="testRegistryService" />
客户端配置:
<dubbo:application name="dubbo_consumer"></dubbo:application>
<!-- 使用zookeeper注册中心暴露服务地址 -->
<dubbo:registry address="zookeeper://192.168.74.129:2181" check="false"></dubbo:registry>
<!-- 要引用的服务 -->
<dubbo:reference interface="cn.test.dubbo.registry.service.TestRegistryService" id="testRegistryService"></dubbo:reference>
从配置上看, 可以以ApplicationConfig,RegistryConfig,ServiceConfig,ReferenceConfig这几个类为入口来分析.
这几个类主要存放配置信息, 需要关注:
1, dubbo是如何将配置类转变为spring上下文中的bean,
2, 如何暴露服务,
3, 在暴露服务的时候,
4, 是如何在zookeeper上注册的,
5, 客户端是如何发现服务的,
6, 如何发起远程服务调用的,
7, 服务端在收到请求之后, 是如何找到对应的服务的?
1,spring 配置读取,解析, 再到生成bean, 放到spring上下文的过程.
dubbo自定义了名称空间"dubbo",
spring支持自定义名称空间, 需要以下几步操作
- 继承抽象类NamespaceHandlerSupport, 在子类中调用registerBeanDefinitionParser方法, 注册解析器, 如
registerBeanDefinitionParser("service",new DubboBeanDefinitionParser(ServiceBean.class,true));
说明 dubbo:service, 最终会生成ServiceBean, 解析转换的细节是spring的源码范畴, 不再深究.
- 在META-INF下, 编写xsd定义文件
其中, 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<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener, BeanNameAware {
继承自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<URL> registryURLs = loadRegistries(true);
for (ProtocolConfig protocolConfig : protocols) {
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
先获取所有注册中心地址, 可能配置了多个, 就是下面这个配置
<dubbo:registry address="zookeeper://127.0.0.1:2181" check="false" subscribe="false" register=""></dubbo:registry>这里只使用了zookeeper作为注册中心.
断点调试查到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<URL> registryURLs)方法看看具体的实现, 这个方法比较长, 吐槽一下写得不是很好, 缩进嵌套太深, 可读性差.
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子标签, 如:
<dubbo:service interface="com.callback.CallbackService" ref="callbackService" connections="1" callbacks="1000">
<dubbo:method name="addListener">
<dubbo:argument index="1" callback="true" />
<!--也可以通过指定类型的方式-->
<!--<dubbo:argument type="com.demo.CallbackListener" callback="true" />-->
</dubbo:method>
</dubbo:service>
10, 遍历method子标签
- 写入method的重试次数值, (从service标签上取, 来自第8步的map), 如果没有的话写0
- 遍历method下面的argument子标签,
- 与这个要暴露的服务的接口方法匹配, 用index或者type属性来匹配, 并做标记存到map里;
- 看手册说这个argument配置主要用来反向代理回调用的, 暂时没看到关于callback的处理, 可能到了实际调用的时候, 才会用到.
====================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<URL>us=loadRegistries(false);
调试过程中,抓取的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方法,
- 创建RegistryDirectory,
- 构建订阅url , 调试得到consumer://192.168.99.1/com.linzp.dubbo.test.DemoService?application=consumer-of-helloworld-app&dubbo=2.0.0&interface=com.linzp.dubbo.test.DemoService&methods=sayHello&pid=408500&side=consumer×tamp=1467478613998
- 将此comsumer订阅url, 到注册中心注册
- 注册目录(RegistryDirectory)订阅subscribeUrl的通知, 此过程中会把invoker封装为invokerDelegate并在RegistryDirectory中缓存urlInvokerMap
- 最后由cluster.join(directory)返回invoker实例
cluster实际是FailoverCluster(Cluster接口上有@SPI(FailoverCluster.NAME))
join方法直接返回new FailoverClusterInvoker<T>(directory)
- 最后使用proxyFactory为invoker创建代理返回.
- proxyFactory.getProxy(invoker);
- proxyFactory 此刻是JavassistProxyFactory,从它的getProxy中可以得知
- return(T)Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
这里给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上下文中, 调用时, 通过代理实例, 找到相关协议方法发起远程调用.