前言
今天开始,将正式的进入Dubbo 核心功能的掌握与学习,Dubbo的最核心功能是什么?自然是其RPC功能,而该功能其实分为三个方面:
1.服务暴露
2.服务引用
3.服务调用
三者分别专注于服务提供者、服务消费者、服务调用过程。我们今天就先关注服务暴露,我们至少有这么几个疑问待解决:
服务暴露有几种,分别是怎么实现的
我们如何进行并配置服务暴露
Dubbo和Spirng框架是怎么结合的
一、服务暴露的分类
看过我们之前的文章 超简单,手把手教你搭建Dubbo工程(内附源码) 的读者,应该有所了解了。即Dubbo 实际上有两种服务暴露的类型,即远程暴露和本地暴露
1.远程暴露
远程暴露是指将服务发布到远程注册中心,供消费者调用。Dubbo支持多种远程通信协议,例如dubbo、rmi、hessian、http等。远程暴露的实现原理大致如下:
服务提供者在启动时,将服务实现类通过协议进行网络传输,发布到注册中心上。
服务消费者在启动时,从注册中心上获取到服务提供者的地址,然后通过协议进行网络通信,调用服务提供者的方法。
2.本地暴露
本地暴露是指将服务发布到本地JVM上,供同一JVM中的其他服务调用。Dubbo的本地通信使用了JVM内部的IPC或者共享内存等方式,从而避免了网络传输的开销。本地暴露的实现原理大致如下:
服务提供者在启动时,将服务实现类通过代理方式,注入到本地JVM的缓存中。
服务消费者在启动时,直接从本地JVM缓存中获取到服务提供者的代理对象,然后调用服务提供者的方法。
PS:需要注意的是,本地暴露仅限于同一JVM内的服务调用,如果需要跨进程或跨机器进行通信,仍需要使用远程通信方式
二、本地暴露
有的人会问,Dubbo提供本地调用的服务是什么意思?因为大部分人都是把Dubbo当作RPC框架使用的,为什么其还会提供本地暴露这种用法?
应该说:Dubbo的本地暴露实际上是比较少用到的,但对于一些需要高性能、低延迟、本地调用的应用场景,使用本地暴露可以提高服务的性能和可用性。从实际项目上来讲,笔者接触过一些项目,其功能全部实现了对外暴露,也就是说有很多功能,既允许外部远程调用,也能被本地其他方法调用。这个时候,本地的调用如果还走RPC的路子,不仅带来延时,还无谓地浪费了带宽,支持本地调用就很有必要了
1. 配置
在Dubbo 2.2 0版本开始,Dubbo默认在本地以JVM的方式暴露服务,当然,对于老版本而言,还需要进行一些配置。这样的话,在第一步需要在整体配置文件里开启
#开启服务暴露 dubbo.provider.export= true
其次就是在单个接口上,我们需要配置引用的源scope = local
<bean id="demoServiceTarget" class="org.apache.dubbo.samples.local.impl.DemoServiceImpl"/> <!-- 服务提供者指定scope --> <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"/>
2. 实现原理
我们来看进行服务暴露(或者说导出)的基础方法 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协议的暴露,实际都会存储在该位置,并没有什么不同。