SPI
Dubbo采用微内核+插件体系。设计优雅,可扩展性强。
微内核+插件体系的实现是基于SPI机制。(service provider interface)
框架定义服务标准接口,使用者可以自己扩展服务实现。
1, Dubbo定义了@SPI
注解
对于打了@spi注解的接口,dubbo会从以下目录依次查找实现。
META-INF/dubbo/internal/ //dubbo 内部实现的各种扩展都放在了这个目录
了
META-INF/dubbo/
META-INF/services/
2, ExtensionLoader是SPI加载器。
com.alibaba.dubbo.common.extension.ExtensionLoader#loadExtensionClasses
java中的SPI
全称是Service Provider Interface。和开发人员面对的interface是类似的概念,给商家或者插件提供商开放不同的服务实现。
约定:
当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader。java.util.ServiceLoader会查找这个路径,找到实现。
服务暴露
关注com.alibaba.dubbo.config.spring.ServiceBean
管理服务的生命周期。
- setApplicationContext 设置容器 applicationContext
- afterPropertiesSet 回调InitializingBean 的 afterPropertiesSet
- onApplicationEvent spring的最后一步 finishRefresh 会触发 ContextRefreshedEvent 事件,而 ServiceBean
实现了 ApplicationListener 接口监听了此事件, 而在之前一步实例化的
ServiceBean 注 册 了 这 个 事 件 , 所 以 ServiceBean 的
onApplicationEvent(ApplicationEvent event)方法被触发, 在这个方法中触
发了 export 方法来暴露服务。 - destroy 服务下线
export
看一下export具体是怎么实现的
- ServiceBean实现了ApplicationListener接口,监听容器加载完成事件ContextRefreshedEvent。
开始export() - com.alibaba.dubbo.config.ServiceConfig#export()会判断是否是延迟暴露。
是则起一个守护线程Thread.sleep(delay)后暴露,否则直接暴露。 - com.alibaba.dubbo.config.ServiceConfig#doExport().装载注册中心监控中心等
- com.alibaba.dubbo.config.ServiceConfig#doExportUrls,
执行loadRegistries()遍历注册中心,根据注册中心、Dubbo版本、Pid等生成要发布的URL;
URL示例: zookeeper://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=ordercenter_serviceImpl
&dubbo=2.8.4&pid=15836®istry=zookeeper×tamp=1484018365125
遍历所有协议,遍历服务协议,为每个协议执行doExportUrlsFor1Protocol() com.alibaba.dubbo.config.ServiceConfig#doExportUrlsFor1Protocol
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url); Exporter<?> exporter = protocol.export(invoker);
关键的两句,
- 将具体的服务转换成invoker,
ref是接口实现类引用,interfaceClass是接口,url是组装的服务url。
转换使用JavassistProxyFactory或者JdkProxyFactory中一个。 - 然后将invoker转换成exporter。
转化过程使用众多协议中的一个,DubboProtocol、hessionProtocol、redisProtocol等等
- 将具体的服务转换成invoker,
比如DubboProtocol
export() -> openServer() ->createServer()
createServer()中通过server = Exchangers.bind(url, requestHandler);
代码转到com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchanger#bind
最终转到创建NettyServer。
服务引用
关注com.alibaba.dubbo.config.spring.ReferenceBean
ReferenceBean实现了FactoryBean接口,spring的bean工厂在获取的bean的时候会判断下是不是FactoryBean的实例,如果是调factoryBean.getObject()返回,否则返回bean。
我们使用时并不是想要ReferenceBean这个实例,而是希望通过这个实例获取对应的代理,通过代理调用远程服务。
所以在factoryBean.getObject()中封装了复杂实现,使我们可以拿到代理对象。
- ReferenceBean实现InitializingBean接口,spring回调afterPropertiesSet()
- getObject()
- 检查dubbo配置,创建代理。
- 向注册中心订阅服务。
- 返回代理对象。
dubbo的spring配置是如何解析的?
以META-INF/spring/dubbo-demo-provider.xml
为例。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<bean id="demoService" class="com.alibaba.dubbo.demo.provider.DemoServiceImpl" />
<dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService" />
</beans>
其中的dubbo:service
是自定义的扩展schema,他是如何被spring解析的呢?
原理:spring支持自定义扩展schema。
具体原理:spring默认会加载两个文件:这两个文件的地址必须是META-INF/spring.handlers和META-INF/spring.schemas。
对应的dubbo,可在dubbo-config-spring下找到。
META-INF/spring.schemas的内容:
http://code.alibabatech.com/schema/dubbo/dubbo.xsd=META-INF/dubbo.xsd
放的是自己定义的xsd文件位置。
META-INF/spring.handlers的内容:
http\://code.alibabatech.com/schema/dubbo=com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler
这个类DubboNamespaceHandler是将xsd中定义的标签,找到对应的解析器。
handler要继承spring提供的NamespaceHandlerSupport。
DubboBeanDefinitionParser要实现spring提供的BeanDefinitionParser。
这样,spring就可以找到xsd以及对应的解析器。
最后在spring的xml中使用是,引入即可。
分析下DubboBeanDefinitionParser
- 需要实现spring提供的BeanDefinitionParser。
- 主要看parse方法。很长。主要做了这几件事。
- 创建bean id。具体逻辑不讲,看代码。然后注册到spring的上下文中,重复,抛错。
- 对某些类型做一下特殊处理,比如ServiceBean,需要将对应的实现注册到ref里。
- 然后获取class的set方法,判断是否可以set注入。通过set返货获取属性,进而得到get或者is方法。再从xml的element中拿到对应的值。最后组装到BeanDefinition中。spring通过BeanDefinition完成注入。
dubbo如何获取properties配置文件中的配置项
对于dubbo的任何Config类,比如ServiceConfig都是继承自AbstractConfig,这个类是配置解析的工具方法、公共方法。
关注appendProperties
方法。
首先获取前缀。
String prefix = "dubbo." + getTagName(config.getClass()) + ".";
private static String getTagName(Class<?> cls) {
String tag = cls.getSimpleName();
for (String suffix : SUFFIXS) {
if (tag.endsWith(suffix)) {
tag = tag.substring(0, tag.length() - suffix.length());
break;
}
}
tag = tag.toLowerCase();
return tag;
}
private static final String[] SUFFIXS = new String[] {"Config", "Bean"};
把结尾的Config或Bean截掉,比如ReferenceBean
的配置项都会以dubbo.reference.开头。
然后拿到类的所有方法,执行其中的set方法,注入值。
for (Method method : methods) {
try {
String name = method.getName();
if (name.length() > 3 && name.startsWith("set") && Modifier.isPublic(method.getModifiers())
&& method.getParameterTypes().length == 1 && isPrimitive(method.getParameterTypes()[0])) {
...
...
method.invoke(config, new Object[] {convertPrimitive(method.getParameterTypes()[0], value)});
}
}
注意,获取properties时,首先从System.getProperty(也就是java -D中指定的参数),再从ConfigUtils.getProperty中取(即properties文件中的配置)。
再注意,从 ConfigUtils.getProperty取之前,会通过get方法判断bean中该属性是否已经有值了(通过spring注入的),没有值才会从ConfigUtils.getProperty中取。
所以最终的优先级是:
System.getProperty —-> spring bean注入 —>ConfigUtils.getProperty
另外注意到:
从System.getProperty或者从ConfigUtils.getProperty中取都会取两次
if (config.getId() != null && config.getId().length() > 0) {
String pn = prefix + config.getId() + "." + property;
value = System.getProperty(pn);
if(! StringUtils.isBlank(value)) {
logger.info("Use System Property " + pn + " to config dubbo");
}
}
if (value == null || value.length() == 0) {
String pn = prefix + property;
value = System.getProperty(pn);
if(! StringUtils.isBlank(value)) {
logger.info("Use System Property " + pn + " to config dubbo");
}
}
第一次带着config.getId(),这个id就是bean id。
就是说,先取针对这个bean的配置,没有再取通用配置。
动态配置
这种方式主要原理就是通过管理器将动态参数发布到注册中心(zookeeper)中,然后各个节点可以获得最新的配置变更,然后进行动态调整。
想知道这一块内容,需要取看看类RegistryDirectory的实现,该类它实现了NotifyListener。那么它就可以监听到注册中心的任何变更。再了解RegistryDirectory之前,先看看DUBBO对每个服务在注册中心存储了哪些信息?如果用的时zookeeper,那么你会在每个服务节点目录下面看到一下几个目录consumers,providers,configurators,routers。其中动态配置时放在configurators节点目录下。服务消费端会监听configurators目录变更,如果变更则会调用RegistryDirectory的void notify(List urls)方法。监听configurators目录变更触发的void notify(List urls)方法时,urls的是类似override://…,表示将覆盖调用该服务的某些配置(dubbo中对所有的调用配置都是通过URL的形式来展示的),讲这些URL上面的参数信息替换到调用服务端的URL上面取,并且重新构造该服务的Invoke对象,从而达到更新参数的目的。
[zk: localhost:2181(CONNECTED) 11] ls /dubbo/com.alibaba.dubbo.demo.DemoService
[consumers, configurators, routers, providers]