【收藏向】从用法到源码,一篇文章让你精通Dubbo的SPI机制
面试Dubbo ,却问我和Springcloud有什么区别?
超简单,手把手教你搭建Dubbo工程(内附源码)
今天开始,将正式的进入Dubbo 核心功能的掌握与学习,Dubbo的最核心功能是什么?自然是其RPC功能,而该功能其实分为三个方面:
1.服务暴露
2.服务引用
3.服务调用
三者分别专注于服务提供者、服务消费者、服务调用过程。我们今天就先关注服务暴露,我们至少有这么几个疑问待解决:
作者简介:战斧,多年开发及管理经验,爱好广泛,致力于创作更多高质量内容
本文收录于 Dubbo专栏,有需要者,可直接订阅专栏实时获取更新
高质量专栏 RabbitMQ、Spring全家桶 等仍在更新,欢迎指导
Zookeeper Redis kafka docker netty等诸多框架,以及架构与分布式专题即将上线,敬请期待
看过我们之前的文章 超简单,手把手教你搭建Dubbo工程(内附源码) 的读者,应该有所了解了。即Dubbo 实际上有两种服务暴露的类型,即远程暴露
和本地暴露
PS:需要注意的是,本地暴露仅限于同一JVM内的服务调用,如果需要跨进程或跨机器进行通信,仍需要使用远程通信方式
有的人会问,Dubbo提供本地调用的服务是什么意思?因为大部分人都是把Dubbo当作RPC框架使用的,为什么其还会提供本地暴露这种用法?
应该说:Dubbo的本地暴露实际上是比较少用到的,但对于一些需要高性能、低延迟、本地调用的应用场景,使用本地暴露可以提高服务的性能和可用性。从实际项目上来讲,笔者接触过一些项目,其功能全部实现了对外暴露,也就是说有很多功能,既允许外部远程调用,也能被本地其他方法调用。这个时候,本地的调用如果还走RPC的路子,不仅带来延时,还无谓地浪费了带宽,支持本地调用就很有必要了
在Dubbo 2.2 0版本开始,Dubbo默认在本地以JVM的方式暴露服务,当然,对于老版本而言,还需要进行一些配置。这样的话,在第一步需要在整体配置文件里开启
#开启服务暴露
dubbo.provider.export= true
其次就是在单个接口上,我们需要配置引用的源scope = local
<bean id="demoServiceTarget" class="org.apache.dubbo.samples.local.impl.DemoServiceImpl"/>
<dubbo:service interface="org.apache.dubbo.samples.local.api.DemoService" ref="demoServiceTarget" scope="local"/>
<dubbo:reference id="demoService" interface="org.apache.dubbo.samples.local.api.DemoService"/>
我们来看进行服务暴露
(或者说导出
)的基础方法 ServiceConfig 下私有方法 exportUrl
private void exportUrl(URL url, List<URL> registryURLs) {
String scope = url.getParameter(SCOPE_KEY);
// 如果配置scope为"none" ,就不进行服务暴露
if (!SCOPE_NONE.equalsIgnoreCase(scope)) {
// 除非显式的配置了远程暴露,不然都会进行本地暴露
if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {
exportLocal(url);
}
// 除非显式的指定了本地暴露,不然都会进行远程暴露
if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
// 远程导出省略.....
}
}
// 不难看出,`scope` 字段采取的判断逻辑非常宽容,试想一下,如果填入 scope = "abc" 会发生什么?
this.urls.add(url);
}
通过上述方法,我们知道了除非指定某接口就是远程暴露,否则都会进行本地暴露。那具体本地暴露是怎么回事呢?我们继续来看
private Protocol protocolSPI = this.getExtensionLoader(Protocol.class).getAdaptiveExtension();
String LOCAL_PROTOCOL = "injvm";
String LOCALHOST_VALUE = "127.0.0.1";
private void exportLocal(URL url) {
URL local = URLBuilder.from(url)
.setProtocol(LOCAL_PROTOCOL)
.setHost(LOCALHOST_VALUE)
.setPort(0)
.build();
local = local.setScopeModel(getScopeModel())
.setServiceModel(providerModel);
doExportUrl(local, false);
logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry url : " + local);
}
private void doExportUrl(URL url, boolean withMetaData) {
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
if (withMetaData) {
invoker = new DelegateProviderMetaDataInvoker(invoker, this);
}
Exporter<?> exporter = protocolSPI.export(invoker);
exporters.add(exporter);
}
不难发现,本地暴露并没有采用取巧的方式去做特殊处理,而是老老实实构建了个URL,并且像模像样的进行导出,只是这个URL的设置了协议类型为injvm
,主机更是直接127.0.0.1。当然最重要的是 protocolSPI.export(invoker) 这个代码,这里的 protocolSPI
是利用的Dubbo-SPI的自适应机制,所以导致最终执行导出的实现类为 InjvmProtocol
(关于Dubbo-SPI的内容可见 一篇文章让你精通Dubbo的SPI机制),我们来看 InjvmProtocol
的 export 方法
// AbstractProtocol.class
protected final Map<String, Exporter<?>> exporterMap = new ConcurrentHashMap<>();
// InjvmProtocol.class 继承自 AbstractProtocol
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap);
}
// InjvmExporter.class
public class InjvmExporter<T> extends AbstractExporter<T> {
private final String key;
private final Map<String, Exporter<?>> exporterMap;
InjvmExporter(Invoker<T> invoker, String key, Map<String, Exporter<?>> exporterMap) {
super(invoker);
this.key = key;
this.exporterMap = exporterMap;
exporterMap.put(key, this);
}
@Override
public void afterUnExport() {
exporterMap.remove(key);
}
}
可以看到这里其实使用了一个 Map 结构存储了这样的服务暴露的结果,因为这个Map 结构存储在抽象类中,也就是说不管是本地暴露还是哪种RPC协议的暴露,实际都会存储在该位置,并没有什么不同。
远程暴露的配置其实与本地暴露雷同,只需将 scope
的值赋成 remote 即可,并指定使用的协议
<bean id="demoServiceTarget" class="org.apache.dubbo.samples.local.impl.DemoServiceImpl"/>
<dubbo:service interface="org.apache.dubbo.samples.local.api.DemoService" ref="demoServiceTarget" scope="remote"/>
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:reference id="demoService" interface="org.apache.dubbo.samples.local.api.DemoService"/>
同本地暴露一样,我们仍然来看基础方法 ServiceConfig 下私有方法 exportUrl
private void exportUrl(URL url, List<URL> registryURLs) {
String scope = url.getParameter(SCOPE_KEY);
if (!SCOPE_NONE.equalsIgnoreCase(scope)) {
if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {
// 本地暴露略
}
// 远程暴露
if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
// 远程暴露获取额外设置的协议
String extProtocol = url.getParameter("ext.protocol", "");
List<String> protocols = new ArrayList<>();
if (StringUtils.isNotBlank(extProtocol)) {
// export original url
url = URLBuilder.from(url).
addParameter(IS_PU_SERVER_KEY, Boolean.TRUE.toString()).
removeParameter("ext.protocol").
build();
}
// 远程暴露
url = exportRemote(url, registryURLs);
if (!isGeneric(generic) && !getScopeModel().isInternal()) {
MetadataUtils.publishServiceDefinition(url, providerModel.getServiceModel(), getApplicationModel());
}
if (StringUtils.isNotBlank(extProtocol)) {
String[] extProtocols = extProtocol.split(",", -1);
protocols.addAll(Arrays.asList(extProtocols));
}
// 对于额外设定的协议,也进行暴露
for(String protocol : protocols) {
if(StringUtils.isNotBlank(protocol)){
URL localUrl = URLBuilder.from(url).
setProtocol(protocol).
build();
localUrl = exportRemote(localUrl, registryURLs);
if (!isGeneric(generic) && !getScopeModel().isInternal()) {
MetadataUtils.publishServiceDefinition(localUrl, providerModel.getServiceModel(), getApplicationModel());
}
this.urls.add(localUrl);
}
}
}
}
this.urls.add(url);
}
通过上述方法,我们可以知道,在服务远程暴露的时候,可以设置额外的导出协议,这将导致一个服务将以多个协议的形式进行暴露,但不管是哪种方式,其核心方法都是调用的 exportRemote,该方法第一个参数为要暴露的服务的信息,第二个参数为各注册中心的信息,我们来看看该方法。
private URL exportRemote(URL url, List<URL> registryURLs) {
if (CollectionUtils.isNotEmpty(registryURLs)) {
// 遍历各注册中心
for (URL registryURL : registryURLs) {
// 判断是否是service-discovery-registry协议,该协议用于自省架构
if (SERVICE_REGISTRY_PROTOCOL.equals(registryURL.getProtocol())) {
url = url.addParameterIfAbsent(SERVICE_NAME_MAPPING_KEY, "true");
}
// 本地暴露不予执行
if (LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
continue;
}
url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY));
URL monitorUrl = ConfigValidationUtils.loadMonitor(this, registryURL);
if (monitorUrl != null) {
url = url.putAttribute(MONITOR_KEY, monitorUrl);
}
// For providers, this is used to enable custom proxy to generate invoker
String proxy = url.getParameter(PROXY_KEY);
if (StringUtils.isNotEmpty(proxy)) {
registryURL = registryURL.addParameter(PROXY_KEY, proxy);
}
if (logger.isInfoEnabled()) {
if (url.getParameter(REGISTER_KEY, true)) {
logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL.getAddress());
} else {
logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
}
}
// 把服务消息作为一个属性包裹在注册中心url内,并执行导出
doExportUrl(registryURL.putAttribute(EXPORT_KEY, url), true);
}
} else {
if (logger.isInfoEnabled()) {
logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
}
doExportUrl(url, true);
}
return url;
}
可以看到,如果指定了注册中心,会把服务消息包装成在各个注册中心的URL中,我们以ZK为例,当我们向Zookeeper 进行注册时,会获得如下的注册Url
并进行发布;如果没有,则执行默认的发布,方法都是 doExportUrl,这个我们在本地暴露时已经讲过,会根据协议类型来选择实现类来进行发布,此处显然就是 RegistryProtocol 作为实现类,我们直接找到其注册的位置
// RegistryProtocol
private void register(Registry registry, URL registeredProviderUrl) {
registry.register(registeredProviderUrl);
}
最后则是以 ZookeeperRegistry 通过ZK客户端向Zookeeper 注册服务信息
// ZookeeperRegistry
public void doRegister(URL url) {
try {
checkDestroyed();
zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true), false);
} catch (Throwable e) {
throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
除了上面通过xml文件进行的配置,Dubbo其实更适合使用注解进行服务的启用,当然由于历代Dubbo的变化,注解也发生了变化
我们以最新的 @DubboService
来看看,其中很多字段都是与XML的配置完全一样的
当然,我们上面提到的 scope
也在其中,用法也是完全相同
我们前面说的服务暴露,都是以 ServiceConfig 下私有方法 exportUrl 为起点,但是当应用启动时,这个方法是怎样被触发的呢,我们现在来看这个问题。我们以结合Spring 的项目来进行研究
我们在前面进行工程搭建的时候,就提到部署时需要在启动类上写上注解 @EnableDubbo
,大部分三方框架都需要以类似的方式执行与Spring工程的融合,我们先来看看加上该注解的原理吧。
这两个注解顾名思义,一个负责启用默认配置,一个负责加载组件。我们现在看组件部分
关于注解上再使用 @Import
注解,其实也是诸多框架里经常采用的导入Spring的模式了,该注解能用我们在外层注解上的值进行某些操作,在此处,就是把该目录下的组件扫描进Spring容器中
经过上述的组件扫描,我们要获得什么呢?当然是我们配置的服务或者引用能够正确的装载进Spring容器了
// DubboComponentScanRegistrar
private void registerServiceAnnotationPostProcessor(Set<String> packagesToScan, BeanDefinitionRegistry registry) {
// 获取 ServiceAnnotationPostProcessor 的信息,将其的Bean定义注入Spring容器
BeanDefinitionBuilder builder = rootBeanDefinition(ServiceAnnotationPostProcessor.class);
builder.addConstructorArgValue(packagesToScan);
builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
BeanDefinitionReaderUtils.registerWithGeneratedName(beanDefinition, registry);
}
public static String registerWithGeneratedName(AbstractBeanDefinition definition, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException {
String generatedName = generateBeanName(definition, registry, false);
// 注入进Spring 容器
registry.registerBeanDefinition(generatedName, definition);
return generatedName;
}
这里,我们需要知道,把ServiceAnnotationPostProcessor
的Bean定义注入进Spring的原因是什么,因为其是一个后置处理器,它继承了BeanDefinitionRegistryPostProcessor
,能负责在Bean工厂创建完成后执行操作,我们在MyBatis+Springboot 启动到SQL执行全流程 和 pringBean生成流程详解 两篇文章中都有解释,此处就不再赘述。来关注下它做了什么。
// 至今为止,Dubbo 经历过几个阶段,使用的服务注解各不相同,此处算做个兼容
private final static List<Class<? extends Annotation>> serviceAnnotationTypes = asList(
// @since 2.7.7 Add the @DubboService , the issue : https://github.com/apache/dubbo/issues/6007
DubboService.class,
// @since 2.7.0 the substitute @com.alibaba.dubbo.config.annotation.Service
Service.class,
// @since 2.7.3 Add the compatibility for legacy Dubbo's @Service , the issue : https://github.com/apache/dubbo/issues/4330
com.alibaba.dubbo.config.annotation.Service.class
);
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (this.registry == null) {
// In spring 3.x, may be not call postProcessBeanDefinitionRegistry()
this.registry = (BeanDefinitionRegistry) beanFactory;
}
// 扫描Bean定义
String[] beanNames = beanFactory.getBeanDefinitionNames();
for (String beanName : beanNames) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
Map<String, Object> annotationAttributes = getServiceAnnotationAttributes(beanDefinition);
if (annotationAttributes != null) {
// process @DubboService at java-config @bean method
processAnnotatedBeanDefinition(beanName, (AnnotatedBeanDefinition) beanDefinition, annotationAttributes);
}
}
if (!scanned) {
// 把目录下符合条件的类定义,注册进Spring容器
scanServiceBeans(resolvedPackagesToScan, registry);
}
}
首先我们得知道Spring 提供的监听器机制(不了解的可以看这篇:Spring监听器用法与原理详解),知道了在Spring容器刷新完毕后的时候会发送事件 ContextRefreshedEvent
// AbstractApplicationContext.class
protected void finishRefresh() {
this.clearResourceCaches();
this.initLifecycleProcessor();
this.getLifecycleProcessor().onRefresh();
this.publishEvent((ApplicationEvent)(new ContextRefreshedEvent(this)));
if (!NativeDetector.inNativeImage()) {
LiveBeansView.registerApplicationContext(this);
}
}
而Dubbo 正是利用了这个机制来进行启动的,Dubbo内建了一个监听器 DubboDeployApplicationListener 来监听 ContextRefreshedEvent
事件,以此为触发,进行Dubbo模块的启动
// DubboDeployApplicationListener.class
private void onContextRefreshedEvent(ContextRefreshedEvent event) {
ModuleDeployer deployer = moduleModel.getDeployer();
Assert.notNull(deployer, "Module deployer is null");
// 启动模块
Future future = deployer.start();
// if the module does not start in background, await finish
if (!deployer.isBackground()) {
try {
future.get();
} catch (InterruptedException e) {
logger.warn(CONFIG_FAILED_START_MODEL, "", "", "Interrupted while waiting for dubbo module start: " + e.getMessage());
} catch (Exception e) {
logger.warn(CONFIG_FAILED_START_MODEL, "", "", "An error occurred while waiting for dubbo module start: " + e.getMessage(), e);
}
}
}
但是,我们还有一个问题,就是我们知道Spring的监听器必须置于Spring容器下,如果是我们自己写代码,类上加个注解@Componen
t就可以解决,但眼下情形显然不同,==这个监听器是如何融入Spring体系并将自身加入进Spring容器的呢?==其实上面就已经提到了
这里的 DubboSpringInitializer.initialize(registry)
就是把Dubbo 自身的很多内容注册进Spring 容器的,其调用链路为
initialize -> initContext -> DubboBeanUtils.registerCommonBeans(registry)
static void registerCommonBeans(BeanDefinitionRegistry registry) {
registerInfrastructureBean(registry, ServicePackagesHolder.BEAN_NAME, ServicePackagesHolder.class);
registerInfrastructureBean(registry, ReferenceBeanManager.BEAN_NAME, ReferenceBeanManager.class);
// Since 2.5.7 Register @Reference Annotation Bean Processor as an infrastructure Bean
registerInfrastructureBean(registry, ReferenceAnnotationBeanPostProcessor.BEAN_NAME,
ReferenceAnnotationBeanPostProcessor.class);
// TODO Whether DubboConfigAliasPostProcessor can be removed ?
// Since 2.7.4 [Feature] https://github.com/apache/dubbo/issues/5093
registerInfrastructureBean(registry, DubboConfigAliasPostProcessor.BEAN_NAME,
DubboConfigAliasPostProcessor.class);
// 注册监听器,其中就包含了我们这次提到的 DubboDeployApplicationListener
registerInfrastructureBean(registry, DubboDeployApplicationListener.class.getName(), DubboDeployApplicationListener.class);
registerInfrastructureBean(registry, DubboConfigApplicationListener.class.getName(), DubboConfigApplicationListener.class);
// Since 2.7.6 Register DubboConfigDefaultPropertyValueBeanPostProcessor as an infrastructure Bean
registerInfrastructureBean(registry, DubboConfigDefaultPropertyValueBeanPostProcessor.BEAN_NAME,
DubboConfigDefaultPropertyValueBeanPostProcessor.class);
// Dubbo config initializer
registerInfrastructureBean(registry, DubboConfigBeanInitializer.BEAN_NAME, DubboConfigBeanInitializer.class);
// register infra bean if not exists later
registerInfrastructureBean(registry, DubboInfraBeanRegisterPostProcessor.BEAN_NAME, DubboInfraBeanRegisterPostProcessor.class);
}