dubbo服务暴露与注册
dubbo服務(wù)暴露與注冊
文章目錄
- dubbo服務(wù)暴露與注冊
- 配置解析
- 大致流程圖
- 服務(wù)暴露
- SPI機(jī)制
- 本地暴露
- 本地暴露作用
- 遠(yuǎn)程暴露
- 服務(wù)暴露:
- 服務(wù)注冊
- 遠(yuǎn)程暴露流程圖
- 總結(jié)
- 參考
配置解析
本文Dubbo源碼版本為2.8.4。
我們先從dubbo的配置講起,主要是Spring的原理:
我們一般通過XML或者annotation的方式,對dubbo進(jìn)行配置。我們下面來講一下XML的配置解析:
上面這要用到了Spring的自定義標(biāo)簽功能,這個配置文件中,主要定義了一個dubbo的命名空間,編寫了對應(yīng)的xsd文檔,用于約束 XML 配置時候的標(biāo)簽和對應(yīng)的屬性。這個xsd文檔在dubbo jar包中META-INF/dubbo.xsd。
在xsd文檔中,我們可以看到很多的標(biāo)簽,但是我們主要關(guān)注service和reference標(biāo)簽:
解析這些標(biāo)簽的時候,會去找dubbo jar包下的META-INF/spring.handlers和spring.schema:
spring.schema指明了約束文件的位置,而spring.handlers則是指明了解析約束文件中標(biāo)簽的處理類。
下面我們來看一下這個處理類:
public class DubboNamespaceHandler extends NamespaceHandlerSupport {public void init() {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));} }這個類就是將標(biāo)簽和對應(yīng)的解析類關(guān)聯(lián)起來,解析的時候,就知道用對應(yīng)的解析類去解析。
里面主要是拿到xml中的配置信息,然后生成spring中的BeanDefinition。BeanDefinition就是對Bean的一個抽象,主要保存了類名、scope、屬性、構(gòu)造函數(shù)參數(shù)列表、依賴的bean、是否是懶加載等等,后面對Bean的操作就直接對BeanDefinition進(jìn)行操作,如后面服務(wù)暴露中對是否遠(yuǎn)程暴露的判斷就是對scope進(jìn)行判斷就好了。
大致流程圖
我們先來看一下大致的流程圖,這樣在腦中有個大致的路線。
首先,入口是ServiceBean類,通過其中的export()方法開始進(jìn)入ServiceConfig類的export()方法,執(zhí)行暴露服務(wù)之前的一些邏輯判斷,如是否配置延遲發(fā)布。接著export()調(diào)用了DoExport()方法,doExportUrls()開始正式暴露服務(wù)。
doExportUrls()方法首先獲取了注冊中心路徑。然后開始進(jìn)入doExportUrlsFor1Protocol()做服務(wù)暴露與注冊。
doExportUrlsFor1Protocol()方法中,其中關(guān)鍵代碼為對scope的判斷,如果配置為none則表示不做暴露,直接結(jié)束;如果配置為local,表示只做本地暴露;如果配置為remote,則表示只做遠(yuǎn)程暴露;如果沒有配置,則表示既做本地暴露也做遠(yuǎn)程暴露。
其中的細(xì)節(jié),后面一一道來。
服務(wù)暴露
從上面所述可知,服務(wù)暴露主要分為本地暴露和遠(yuǎn)程暴露,遠(yuǎn)程暴露中又包括了遠(yuǎn)程服務(wù)暴露和服務(wù)注冊兩個過程。
下面我們來進(jìn)行源碼的分析:
/**這段代碼主要是服務(wù)暴露的入口,有配置delay的話,通過afterPropertiesSet()開始export(),否則Spring容器初始化完成后,通過onApplicationEvent開始export() **/ public class ServiceBean<T> extends ServiceConfig<T>{public void onApplicationEvent(ApplicationEvent event) {if (ContextRefreshedEvent.class.getName().equals(event.getClass().getName())) {if (isDelay() && ! isExported() && ! isUnexported()) {if (logger.isInfoEnabled()) {logger.info("The service ready on spring started. service: " + getInterface());}export();}}}private boolean isDelay() {Integer delay = getDelay();ProviderConfig provider = getProvider();if (delay == null && provider != null) {delay = provider.getDelay();}return supportedApplicationListener && (delay == null || delay.intValue() == -1);}public void afterPropertiesSet() throws Exception {...if (! isDelay()) {export();}}}首先是入口的ServiceBean類,這里afterPropertiesSet和onApplicationEvent兩個方法中都有export(),但只會執(zhí)行其中的一個,配置了延遲發(fā)布即delay,則會走afterPropertiesSet中的,否則走onApplicationEvent中的。
afterPropertiesSet中,是Spring容器初始化X秒(即你配置的delay是多長時間),進(jìn)行服務(wù)暴露。而onApplicationEvent則是等Spring容器初始化完成后,進(jìn)行服務(wù)暴露。
關(guān)于延遲發(fā)布。可以看我這篇文章:Dubbo優(yōu)雅上下線詳解
接下來進(jìn)入到ServiceConfig類中:
//這段代碼中開始執(zhí)行接口暴露邏輯 public class ServiceConfig<T> extends AbstractServiceConfig{//這段代碼是上面export()進(jìn)入的,還是做是否有配置delay的執(zhí)行邏輯public synchronized void export() {...if (delay != null && delay > 0) {Thread thread = new Thread(new Runnable() {public void run() {try {Thread.sleep(delay);} catch (Throwable e) {}doExport();}});thread.setDaemon(true);thread.setName("DelayExportServiceThread");thread.start();} else {doExport();}}protected synchronized void doExport() {//檢查配置...//開始做暴露doExportUrls();} }上面代碼,主要還是解決是否配置延遲發(fā)布的問題。
接下來,還是ServiceConfig類中,進(jìn)入doExportUrls()方法,這里開始,拉開了服務(wù)暴露的序幕,前面這些都是前期準(zhǔn)備。
public class ServiceConfig<T> extends AbstractServiceConfig{private void doExportUrls() {//獲取注冊中心,可以通過返回值發(fā)現(xiàn),可以有多個注冊中心List<URL> registryURLs = loadRegistries(true);//遍歷協(xié)議,每個協(xié)議都要向注冊中心注冊for (ProtocolConfig protocolConfig : protocols) {doExportUrlsFor1Protocol(protocolConfig, registryURLs);}} }從上面代碼可知:
-
dubbo支持多注冊中心,loadRegistries()這個方法,主要是通過配置組裝成注冊中心URL。
組裝成的URL可以看一下: registry://192.168.6.55:2181/com.alibaba.dubbo.registry.RegistryService?application=poseidon&backup=192.168.6.56:2181,192.168.6.57:2181&dubbo=2.8.4&group=dubbo&pid=10384®ister=true®istry=zookeeper&subscribe=true×tamp=1626684876194 -
dubbo也支持多協(xié)議,如果一個服務(wù)有多個協(xié)議的話,則都需要向注冊中心暴露注冊。
接下來,進(jìn)入doExportUrlsFor1Protocol(),因為這個方法太長了,只取其中關(guān)鍵的代碼:
public class ServiceConfig<T> extends AbstractServiceConfig{private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {//前面是做一些host和port的獲取;新建一個map,存儲配置,通過這個map構(gòu)建出URL...String scope = url.getParameter(Constants.SCOPE_KEY);//配置為none不暴露if (! Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {//配置不是remote的情況下做本地暴露 (配置為remote,則表示只暴露遠(yuǎn)程服務(wù))if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {exportLocal(url);}//如果配置不是local則暴露為遠(yuǎn)程服務(wù).(配置為local,則表示只暴露遠(yuǎn)程服務(wù))if (! Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope) ){if (logger.isInfoEnabled()) {logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);}//如果有注冊中心,向注冊中心注冊服務(wù)if (registryURLs != null && registryURLs.size() > 0&& url.getParameter("register", true)) {for (URL registryURL : registryURLs) {url = url.addParameterIfAbsent("dynamic", registryURL.getParameter("dynamic"));URL monitorUrl = loadMonitor(registryURL);//有監(jiān)控中心的話,添加后向其匯報if (monitorUrl != null) {url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());}if (logger.isInfoEnabled()) {logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);}//轉(zhuǎn)換成Invoker類型,用于向注冊中心注冊Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));//注冊完成之后,返回為exporterExporter<?> exporter = protocol.export(invoker);exporters.add(exporter);}} else {//這段代碼也是做暴露服務(wù),不過是直接暴露,沒有向注冊中心注冊Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);Exporter<?> exporter = protocol.export(invoker);exporters.add(exporter);}}}...} }總結(jié)一下流程圖:
上述代碼,主要就是前面所提的,對scope配置的判斷。為none,則表示不做暴露;為remote,只做遠(yuǎn)程暴露;為local,只做本地暴露;沒有配置,則表示既做遠(yuǎn)程暴露,也做本地暴露。
其中,如果沒有注冊中心的話,也會做服務(wù)暴露,大家都知道,dubbo客戶端訪問接口是可以不通過注冊中心直接訪問接口的。
有注冊中心的時候,中間主要先通過對象轉(zhuǎn)換成invoker,再注冊到注冊中心。返回的時候,對象轉(zhuǎn)換成了exporter。
我們看一下dubbo官網(wǎng)提供的對象轉(zhuǎn)換的大致流程圖:
我們看一下Exporter和Invoker里面有什么?
接著,我們來看一下ProxyFactory的生成。
private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();這里主要是用到了Dubbo的SPI(Service Provider Interface)機(jī)制。
SPI機(jī)制
我們來看一下官網(wǎng)給出的簡介:
SPI 全稱為 Service Provider Interface,是一種服務(wù)發(fā)現(xiàn)機(jī)制。SPI 的本質(zhì)是將接口實(shí)現(xiàn)類的全限定名配置在文件中,并由服務(wù)加載器讀取配置文件,加載實(shí)現(xiàn)類。這樣可以在運(yùn)行時,動態(tài)為接口替換實(shí)現(xiàn)類。正因此特性,我們可以很容易的通過 SPI 機(jī)制為我們的程序提供拓展功能。
SPI機(jī)制在Dubbo中大量地使用。
我們在項目中,經(jīng)常會訪問數(shù)據(jù)庫,而我們訪問數(shù)據(jù)庫的接口就是用java.sql.Driver接口。
市面上的數(shù)據(jù)庫有非常多種,不同的數(shù)據(jù)庫其底層的實(shí)現(xiàn)不同,這時候就需要一個接口,來統(tǒng)一一下訪問數(shù)據(jù)庫的方式。讓使用者訪問數(shù)據(jù)庫的時候只要面向接口編程就可以了。
數(shù)據(jù)庫廠商們會根據(jù)這個接口來提供自己的實(shí)現(xiàn),而使用的時候,怎么才知道到底用哪個實(shí)現(xiàn)呢?
這時候JAVA SPI機(jī)制就派上用場了,它約定在 Classpath 下的 META-INF/services/ 目錄里創(chuàng)建一個以服務(wù)接口命名的文件,然后文件里面記錄的是此 jar 包提供的具體實(shí)現(xiàn)類的全限定名。
我們在使用這個jar包的時候,就會去這個jar包下面的META-INF/services/目錄,再根據(jù)接口名找到文件,然后讀取文件里面的內(nèi)容去進(jìn)行實(shí)現(xiàn)類的加載與實(shí)例化。
但是JAVA SPI有個缺點(diǎn),他會遍歷SPI的配置文件,將實(shí)現(xiàn)類全部實(shí)例化,如果有類用不到的話,有可能會產(chǎn)生資源的浪費(fèi)。
所以,dubbo自己實(shí)現(xiàn)了SPI,實(shí)現(xiàn)了按需加載。通過類的名字去文件里面找到對應(yīng)的實(shí)現(xiàn)類全限定名然后加載實(shí)例化即可(配置文件中存儲的是鍵值對)。原理的話,這邊簡單介紹一下,就是先會拿接口中類的名字去存儲實(shí)例的緩存中看一下有沒有這個接口的實(shí)現(xiàn)類,有的話直接獲取,沒有的話通過反射機(jī)制新建一個。
這里給個每個協(xié)議所對應(yīng)的實(shí)現(xiàn)類。調(diào)用某接口時,如果處于該協(xié)議的狀態(tài)下,會去該協(xié)議所對應(yīng)的類下面找對應(yīng)的接口實(shí)現(xiàn)的方法。
由于本文主要講的是服務(wù)暴露,有興趣可以看以下文章:
- dubbo SPI
- 官網(wǎng):解析Dubbo SPI
本地暴露
從dubbo的2.2.0版本開始,每個服務(wù)默認(rèn)都會在本地暴露。在引用服務(wù)的時候,默認(rèn)優(yōu)先引用本地服務(wù)。如果希望引用遠(yuǎn)程服務(wù)可以使用一下配置強(qiáng)制引用遠(yuǎn)程服務(wù)。
<dubbo:reference ... scope="remote" />接下來,我們來看一下是如何做本地暴露的:
public class ServiceConfig<T> extends AbstractServiceConfig{//本地服務(wù)暴露,用于本地的調(diào)用,避免了網(wǎng)絡(luò)通信private void exportLocal(URL url) {if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {URL local = URL.valueOf(url.toFullString()).setProtocol(Constants.LOCAL_PROTOCOL).setHost(NetUtils.LOCALHOST).setPort(0);// modified by lishenServiceClassHolder.getInstance().pushServiceClass(getServiceClass(ref));Exporter<?> exporter = protocol.export(proxyFactory.getInvoker(ref, (Class) interfaceClass, local));exporters.add(exporter);logger.info("Export dubbo service " + interfaceClass.getName() +" to local registry");}} }本地服務(wù)暴露,用的是injvm協(xié)議,可以看到,上面的代碼中,在url中重新設(shè)置protocol的值。
我們看一下protocol.export()這個方法。
我們看一下export接口的實(shí)現(xiàn),這么多實(shí)現(xiàn)類,怎么才能確定是哪個方法呢?
這邊其實(shí)用的就是Dubbo SPI機(jī)制。
這個export方法可以看到,有個@Adaptive注解,通過這個注解會生成代理類,然后代理類會根據(jù) Invoker 里面的 URL 參數(shù)得知具體的協(xié)議,然后通過 Dubbo SPI 機(jī)制選擇對應(yīng)的實(shí)現(xiàn)類進(jìn)行 export,而這個方法就會調(diào)用 InjvmProtocol中的export 方法。
本地暴露作用
可能存在同一個JVM調(diào)用自身的服務(wù)的情況,開啟一個本地的服務(wù)暴露,可以在調(diào)用的時候,避免了網(wǎng)絡(luò)通信,加快了調(diào)用的速度。
遠(yuǎn)程暴露
public class RegistryProtocol implements Protocol{public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {//export invokerfinal ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);//根據(jù)URL加載Registry的實(shí)現(xiàn)類final Registry registry = getRegistry(originInvoker);//獲取注冊中心的URLfinal URL registedProviderUrl = getRegistedProviderUrl(originInvoker);//服務(wù)注冊registry.register(registedProviderUrl);// 訂閱override數(shù)據(jù)// FIXME 提供者訂閱時,會影響同一JVM即暴露服務(wù),又引用同一服務(wù)的的場景,因為subscribed以服務(wù)名為緩存的key,導(dǎo)致訂閱信息覆蓋。final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registedProviderUrl);final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl);overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);//保證每次export都返回一個新的exporter實(shí)例return new Exporter<T>() {...};} }服務(wù)暴露:
進(jìn)入到doLocalExport:
//做服務(wù)暴露 private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker){String key = getCacheKey(originInvoker);ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key);// DCL雙重檢查鎖,因為有各種緩存if (exporter == null) {synchronized (bounds) {exporter = (ExporterChangeableWrapper<T>) bounds.get(key);//還沒做服務(wù)暴露if (exporter == null) {//invoker中包含著URL,得到URL,URL中的 dubbo://final Invoker<?> invokerDelegete = new InvokerDelegete<T>(originInvoker, getProviderUrl(originInvoker));//調(diào)用dubboProtocol的exportexporter = new ExporterChangeableWrapper<T>((Exporter<T>)protocol.export(invokerDelegete), originInvoker);bounds.put(key, exporter);}}}return (ExporterChangeableWrapper<T>) exporter; }接下來,通過doLocalExport進(jìn)入到:
public class DubboProtocol extends AbstractProtocol {public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {URL url = invoker.getUrl();// export service.String key = serviceKey(url);DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);exporterMap.put(key, exporter);//export an stub service for dispaching event...//打開serveropenServer(url);// modified by lishenoptimizeSerialization(url);return exporter;}private void openServer(URL url) {// find server. 獲取IP地址String key = url.getAddress();//client 也可以暴露一個只有server可以調(diào)用的服務(wù)。boolean isServer = url.getParameter(Constants.IS_SERVER_KEY,true);if (isServer) {//獲取服務(wù)ExchangeServer server = serverMap.get(key);//如果是第一次做服務(wù)暴露if (server == null) {//創(chuàng)建server,往serverMap中放入這個服務(wù)提供urlserverMap.put(key, createServer(url));} else {//server支持reset,配合override功能使用 如果有了,就重置一下server.reset(url);}}}private ExchangeServer createServer(URL url) {//前面是開啟一些服務(wù),往url中加一些東西...ExchangeServer server;try {//開啟nettyServer,來進(jìn)行監(jiān)聽server = Exchangers.bind(url, requestHandler);} catch (RemotingException e) {throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);}...return server;} }到此,服務(wù)暴露就完成了。主要通過Netty完成的,bind()綁定該端口號, netty服務(wù)端將監(jiān)聽該端口號, 接收客戶端請求。
服務(wù)注冊
public abstract class FailbackRegistry extends AbstractRegistry {public void register(URL url) {super.register(url);//將該URL注冊失敗列表中去除failedRegistered.remove(url);failedUnregistered.remove(url);try {// 向服務(wù)器端發(fā)送注冊請求doRegister(url);} catch (Exception e) {...// 將失敗的注冊請求記錄到失敗列表,定時重試failedRegistered.add(url);}} }ZK注冊實(shí)現(xiàn):
//該類位于ZookeeperRegistry protected void doRegister(URL url) {try {zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true));} catch (Throwable e) {throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);} }//該類位于AbstractZookeeperClient public void create(String path, boolean ephemeral) {int i = path.lastIndexOf('/');if (i > 0) {//做遞歸。因為zookeeper建立節(jié)點(diǎn)的時候,只能一級一級的建立,所以每次都是取"/"前面的一部分來創(chuàng)建//由于zookeeper規(guī)定,除了葉子節(jié)點(diǎn)外,其余節(jié)點(diǎn)都必須為非臨時節(jié)點(diǎn),所以這點(diǎn)傳的第二個參數(shù)為FALSEcreate(path.substring(0, i), false);}//如果傳入的ephemeral=TRUE,即是臨時節(jié)點(diǎn)if (ephemeral) {//創(chuàng)建臨時節(jié)點(diǎn)createEphemeral(path);} else {//創(chuàng)建持久節(jié)點(diǎn)createPersistent(path);} }這里的服務(wù)注冊是通過zookeeper實(shí)現(xiàn)的。
遠(yuǎn)程暴露流程圖
總結(jié)
本文通過流程圖以及源碼的方式解析了Dubbo服務(wù)暴露的流程,Dubbo服務(wù)暴露主要有兩個過程:服務(wù)暴露和服務(wù)注冊;服務(wù)暴露默認(rèn)通過Netty Server進(jìn)行,服務(wù)注冊通過Zookeeper進(jìn)行。在其整個流程中,都離不開Dubbo SPI這個機(jī)制,通過這個機(jī)制,Dubbo才知道需要調(diào)用哪個方法。希望這篇文章能幫助到大家更好地了解Dubbo。
參考
-
阿里面試官:你知道Dubbo的服務(wù)暴露機(jī)制么?
-
Dubbo服務(wù)暴露與注冊
-
手把手帶你閱讀dubbo源碼(一) 服務(wù)暴露
總結(jié)
以上是生活随笔為你收集整理的dubbo服务暴露与注册的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JavaWeb前端开发注册表单验证
- 下一篇: ES6三种暴露方法详解