【Dubbo源碼閱讀系列】服務暴露之本地暴露

在上一篇文章中咱們介紹 Dubbo 自定義標籤解析相關內容,其中咱們自定義的 XML 標籤 <dubbo:service /> 會被解析爲 ServiceBean 對象(傳送門:Dubbo XML 配置加載)。今天咱們講述的內容和 ServiceBean 密切相關!
細心的讀者在閱讀 ServiceBean 類時會發現 onApplicationEvent() 方法和 afterPropertiesSet() 方法調用了一個共同的方法 export()。直覺告訴咱們這個方法應該和服務的暴露有關,咱們接下來就 從 export() 方法入手分析。java

export()方法調用時機

爲了解答 export() 調用時機問題,咱們須要關注 ServiceBean 類中的三個方法spring

  1. setApplicationContext(ApplicationContext applicationContext)
    ServiceBean 實現了 ApplicationContextAware 接口,在 ServiceBean 初始化後,會調用 setApplicationContext 注入 Spring 上下文;
  2. afterPropertiesSet() 注入 ApplicationConfig、registries、protocols 等屬性;
  3. onApplicationEvent(ContextRefreshedEvent event) 這裏接受的 event 事件類型爲 ContextRefreshedEvent。當 applicationContext 被初始化或者刷新時,會調用該方法。 這三個方法在 Spring 生命週期中被調用的順序大體以下圖所示 setApplicationContext()——> afterPropertiesSet() ——> onApplicationEvent() 咱們結合代碼繼續看
public void setApplicationContext(ApplicationContext applicationContext) {
    this.applicationContext = applicationContext;
    SpringExtensionFactory.addApplicationContext(applicationContext);
    supportedApplicationListener = addApplicationListener(applicationContext, this);
}

public void onApplicationEvent(ContextRefreshedEvent event) {
    if (!isExported() && !isUnexported()) {
    	if (logger.isInfoEnabled()) {
    		logger.info("The service ready on spring started. service: " + getInterface());
    	}
    	export();
    }
}

public void afterPropertiesSet() throws Exception {
    // 省略...
    if (!supportedApplicationListener) {
    	export();
    }
}
複製代碼

代碼執行邏輯大體以下:apache

  1. 首先執行 setApplicationContext() 方法,注入上下文。這裏的 supportedApplicationListener 用於判斷 Spring 是否支持 Spring 監聽機制。
  2. 執行 afterPropertiesSet() 方法。若是 supportedApplicationListener 值爲 false,調用 export() 方法。
  3. 執行 onApplicationEvent() 方法。若是沒有執行過 export() 以及 unexport() 方法,調用 export() 方法。 經過上面簡單的分析咱們能夠看到 export() 方法只會在 onApplicationEvent() 和 export() 方法中調用一次。

export() 方法解析

public synchronized void export() {
	if (provider != null) {
		if (export == null) {
			export = provider.getExport();
		}
		if (delay == null) {
			delay = provider.getDelay();
		}
	}
	if (export != null && !export) {
		return;
	}

	if (delay != null && delay > 0) {
		delayExportExecutor.schedule(new Runnable() {
			@Override
			public void run() {
				doExport();
			}
		}, delay, TimeUnit.MILLISECONDS);
	} else {
		doExport();
	}
}
複製代碼

export()方法比較簡單。注意這裏有個 delay 變量,咱們可使用該變量延遲執行 export() 方法。 繼續看 doExport() 方法bash

protected synchronized void doExport() {
	// 省略...
	doExportUrls();
	ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), ref, interfaceClass);
	ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel);
}

private void doExportUrls() {
	List<URL> registryURLs = loadRegistries(true);
	for (ProtocolConfig protocolConfig : protocols) {
		doExportUrlsFor1Protocol(protocolConfig, registryURLs);
	}
}
複製代碼

doExport()方法省略了不少 ServiceBean 配置校驗和初始化代碼。你們有興趣能夠自行閱覽。這裏直接劃重點!!!分析 doExportUrls() 方法!!! 先看 loadRegistries() 方法:網絡

loadRegistries()

protected List<URL> loadRegistries(boolean provider) {
	checkRegistry();
	List<URL> registryList = new ArrayList<URL>();
	// registries 在 afterPropertiesSet() 方法中初始化
	if (registries != null && !registries.isEmpty()) {
		for (RegistryConfig config : registries) {
			String address = config.getAddress();
			if (address == null || address.length() == 0) {
				address = Constants.ANYHOST_VALUE;
			}
			String sysaddress = System.getProperty("dubbo.registry.address");
			if (sysaddress != null && sysaddress.length() > 0) {
				address = sysaddress;
			}
			if (address.length() > 0 && !RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {
				Map<String, String> map = new HashMap<String, String>();
				// 將 application/config 部分屬性整合到 map 中,詳細見:
				appendParameters(map, application);
				appendParameters(map, config);
				map.put("path", RegistryService.class.getName());
				map.put("dubbo", Version.getProtocolVersion());
				map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis()));
				if (ConfigUtils.getPid() > 0) {
					map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid()));
				}
				if (!map.containsKey("protocol")) {
					if (ExtensionLoader.getExtensionLoader(RegistryFactory.class).hasExtension("remote")) {
						map.put("protocol", "remote");
					} else {
						map.put("protocol", "dubbo");
					}
				}
				// 構建 url ,返回結果相似 zookeeper://192.168.0.100:2181/org.apache.dubbo.registry.RegistryService?
				// application=demo-provider&dubbo=2.0.2&pid=22705&qos.port=22222&timestamp=1549005672530
				List<URL> urls = UrlUtils.parseURLs(address, map);
				for (URL url : urls) {
				    // 將此時 url 的 protocol 保存到 registry 參數中
					url = url.addParameter(Constants.REGISTRY_KEY, url.getProtocol());
					// 設置 url protcol 屬性爲 registry
					url = url.setProtocol(Constants.REGISTRY_PROTOCOL);
					if ((provider && url.getParameter(Constants.REGISTER_KEY, true))
							|| (!provider && url.getParameter(Constants.SUBSCRIBE_KEY, true))) {
						registryList.add(url);
					}
				}
			}
		}
	}
	return registryList;
}
複製代碼

loadRegistries() 用於加載註冊中心。歸納來講就是用於解析咱們在配置文件中定義的 <dubbo:registry /> 標籤。 checkRegistry() 方法用於校驗註冊中心配置校驗,裏面有一些版本兼容的代碼。appendParameters() 方法詳見 appendParameters() 小節。app

本地暴露

介紹完 loadRegistries() 方法,咱們接着看 doExportUrlsFor1Protocol()。doExportUrlsFor1Protocol() 方法比較長,這裏咱們挑出和本地暴露相關的內容進行分析。jvm

if (!Constants.SCOPE_NONE.equalsIgnoreCase(scope)) {
    // export to local if the config is not remote (export to remote only when config is remote)
    if (!Constants.SCOPE_REMOTE.equalsIgnoreCase(scope)) {
        exportLocal(url);
    }
    if (!Constants.SCOPE_LOCAL.equalsIgnoreCase(scope)) {
        // 遠程暴露相關內容,省略...
    }
}
private void exportLocal(URL url) {
    if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
        URL local = URL.valueOf(url.toFullString())
                .setProtocol(Constants.LOCAL_PROTOCOL)
                .setHost(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");
    }
}
複製代碼

看到 exportLocal() 方法,意味着咱們已經快要直達本地服務暴露的核心了!更使人按捺不住的是!這裏又用到了 Dubbo 中的 SPI 機制(詳見系列第一篇Dubbo SPI)。讓咱們看看這裏到底作了什麼?ide

private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
複製代碼

熟悉的配方熟悉的料,在這裏咱們獲取了 Protocol 和 ProxyFactory 對應的自適應擴展類。根據方法調用的嵌套邏輯,先來看 ProxyFactory 自適應擴展類 ProxyFactory$Adaptive 的 getInvoker() 方法。post

核心方法 proxyFactory.getInvoker()

public class ProxyFactory$Adaptive implements org.apache.dubbo.rpc.ProxyFactory {
    public org.apache.dubbo.rpc.Invoker getInvoker(java.lang.Object arg0, java.lang.Class arg1, org.apache.dubbo.common.URL arg2) throws org.apache.dubbo.rpc.RpcException {
        if (arg2 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg2;
        String extName = url.getParameter("proxy", "javassist");
        if(extName == null) throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.ProxyFactory) name from url(" + url.toString() + ") use keys([proxy])");
        org.apache.dubbo.rpc.ProxyFactory extension = null;
        try {
            extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(extName);
        }catch(Exception e){
            if (count.incrementAndGet() == 1) {
                logger.warn("Failed to find extension named " + extName + " for type org.apache.dubbo.rpc.ProxyFactory, will use default extension javassist instead.", e);
            }
            extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension("javassist");
        }
        return extension.getInvoker(arg0, arg1, arg2);
    }
}
複製代碼

這裏咱們實際會去調用 StubProxyFactoryWrapper 包裝類的 getInvoker() 方法,若是不明白能夠先看下 【Dubbo源碼閱讀系列】之 Dubbo SPI 機制ui

public class StubProxyFactoryWrapper implements ProxyFactory {
    public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) throws RpcException {
        return proxyFactory.getInvoker(proxy, type, url);
    }
}
public class JavassistProxyFactory extends AbstractProxyFactory {
    public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
        // TODO Wrapper cannot handle this scenario correctly: the classname contains '$'
        final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
        return new AbstractProxyInvoker<T>(proxy, type, url) {
            @Override
            protected Object doInvoke(T proxy, String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Throwable {
                return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
            }
        };
    }
}
複製代碼

結合上面的代碼咱們發現,發現最後調用的是 JavassistProxyFactory 類的 getInvoker() 方法。其中 wrapper 是動態生成的代理對象。最後返回一個 AbstractProxyInvoker 對象,doInvoke() 方法會調用 wrapper 代理類的 invokeMethod() 方法,其中 invokeMethod() 方法大概以下所示:

public Object invokeMethod(Object o, String n, Class[] p, Object[] v) throws java.lang.reflect.InvocationTargetException {
    org.apache.dubbo.demo.provider.DemoServiceImpl w;
    try {
        w = ((org.apache.dubbo.demo.provider.DemoServiceImpl) $1);
    } catch (Throwable e) {
        throw new IllegalArgumentException(e);
    }
    try {
        if ("sayHello".equals($2) && $3.length == 1) {
            return ($w) w.sayHello((java.lang.String) $4[0]);
        }
    } catch (Throwable e) {
        throw new java.lang.reflect.InvocationTargetException(e);
    }
    throw new org.apache.dubbo.common.bytecode.NoSuchMethodException("Not found method \"" + $2 + "\" in class org.apache.dubbo.demo.provider.DemoServiceImpl.");
}
複製代碼

稍微有一點繞,至少咱們已經看完了 proxyFactory.getInvoker() 方法了,咱們獲取到了一個包裝了動態代理類的 AbstractProxyInvoker 對象。接下來繼續看 protocol.export() 方法。

核心方法 protocol.export()

public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException {
    if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
    if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");org.apache.dubbo.common.URL url = arg0.getUrl();
    String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
    if(extName == null) throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
    org.apache.dubbo.rpc.Protocol extension = null;
    try {
        extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
    }catch(Exception e){
        if (count.incrementAndGet() == 1) {
            logger.warn("Failed to find extension named " + extName + " for type org.apache.dubbo.rpc.Protocol, will use default extension dubbo instead.", e);
        }
        extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension("dubbo");
    }
    return extension.export(arg0);
}
複製代碼

因爲此時的 url 中 protocol 值爲 injvm(url 通過 setProtocol(LOCAL_PROTOCOL) 操做後 protocol 已經更新爲 injvm),所以咱們這裏得到的擴展類實際爲包裝了 InjvmProtocol 的包裝類對象,對 wrapper 類有疑問的能夠看下【Dubbo源碼閱讀系列】之 Dubbo SPI 機制。 這裏會涉及到一個方法 buildInvokerChain() 方,道它用於構建一個調用鏈。 總體調用時序簡圖以下所示:

最後 exportLocal() 方法中獲取到的是一個 InjvmExporter 對象,並將其添加到 ServiceConfig 類的 exporters 集合中。

buildInvokerChain()

ProtocolFilterWrapper.java
private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
	Invoker<T> last = invoker;
	List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
	if (!filters.isEmpty()) {
		for (int i = filters.size() - 1; i >= 0; i--) {
			final Filter filter = filters.get(i);
			final Invoker<T> next = last;
			last = new Invoker<T>() {
				// 省略 Invoker 構建代碼...
				@Override
				public Result invoke(Invocation invocation) throws RpcException {
					return filter.invoke(next, invocation);
				}
				// 省略 Invoker 構建代碼...
			};
		}
	}
	return last;
}
複製代碼

buildInvokerChain() 方法用於構建調用鏈,初步瀏覽下來發現調用鏈應該是由 Filter 擴展類構成。那麼這些 Filter 擴展類又從何而來呢?這行代碼很關鍵!!!

List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
複製代碼

對於這段代碼咱們應該有很強的親切感,但仔細看又稍稍有所不一樣。實際上被 @Activate 註解標記的擴展類會被加載到 ExtensionLoader 類的 cachedActivates 集合中。 咱們在調用 ExtensionLoader 類的 getActivateExtension() 時,會根據咱們傳入的 key 和 group 值從 cachedActivates 集合中獲取知足當前條件的 filter 對象。
拿到 filters 集合後,會用鏈表的形式拼接 filter 調用鏈,舉個例子:
假設當前獲取到的 filters 集合中保存的 filter 對象爲 filter0、filter一、filter2。咱們對 filters 集合進行倒序遍歷。最後得到的 last 其實爲新建的 ivk2 對象。若是咱們調用 last 的 invoke 方法,調用鏈以下圖所示:

End

本文介紹了 Export() 方法被調用的時機以及基本流程。而且花了必定篇幅對 Dubbo 服務本地暴露進行了分析。其中摻雜了很多代碼的分析,可能沒有面面俱到吧。仍是建議你們本身本身 Debug 一下,不少東西瞬間秒懂,有助於源碼理解。下一篇文章咱們介紹 Dubbo 服務遠程暴露。

appendProperties()

protected static void appendProperties(AbstractConfig config) {
	if (config == null) {
		return;
	}
	// getTagName:獲取去除了 Bean/Config 結尾的小寫類名(ApplicationConfig->application)
	String prefix = "dubbo." + getTagName(config.getClass()) + ".";
	Method[] methods = config.getClass().getMethods();
	for (Method method : methods) {
		try {
			String name = method.getName();
			// 一、方法長度大於3;二、方法以 set 開頭;三、方法修飾符類型爲 public;四、形參個數爲 1;五、形參類型爲基本類型
			if (name.length() > 3 && name.startsWith("set") && Modifier.isPublic(method.getModifiers())
					&& method.getParameterTypes().length == 1 && isPrimitive(method.getParameterTypes()[0])) {
				// camelToSplitName: 舉個例子 ApplicationConfig——>application.config
				String property = StringUtils.camelToSplitName(name.substring(3, 4).toLowerCase() + name.substring(4), ".");

				String value = null;
				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) {
					// 好比當前 config 爲 ApplicationConfig,pn = dubbo.application.xxx
					String pn = prefix + property;
					value = System.getProperty(pn);
					if (!StringUtils.isBlank(value)) {
						logger.info("Use System Property " + pn + " to config dubbo");
					}
				}
				if (value == null || value.length() == 0) {
					Method getter;
					try {
						getter = config.getClass().getMethod("get" + name.substring(3));
					} catch (NoSuchMethodException e) {
						try {
							getter = config.getClass().getMethod("is" + name.substring(3));
						} catch (NoSuchMethodException e2) {
							getter = null;
						}
					}
					if (getter != null) {
						if (getter.invoke(config) == null) {
							// 嘗試使用 ConfigUtils.getProperty() 方法獲取屬性值
							// 嘗試從 dubbo.properties.file 文件或 dubbo.properties 文件中讀取屬性
							if (config.getId() != null && config.getId().length() > 0) {
								value = ConfigUtils.getProperty(prefix + config.getId() + "." + property);
							}
							if (value == null || value.length() == 0) {
								value = ConfigUtils.getProperty(prefix + property);
							}
							if (value == null || value.length() == 0) {
								String legacyKey = legacyProperties.get(prefix + property);
								if (legacyKey != null && legacyKey.length() > 0) {
									value = convertLegacyValue(legacyKey, ConfigUtils.getProperty(legacyKey));
								}
							}

						}
					}
				}
				if (value != null && value.length() > 0) {
					method.invoke(config, convertPrimitive(method.getParameterTypes()[0], value));
				}
			}
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
		}
	}
}
複製代碼

appendParameters()

protected static void appendParameters(Map<String, String> parameters, Object config) {
	appendParameters(parameters, config, null);
}
protected static void appendParameters(Map<String, String> parameters, Object config, String prefix) {
	if (config == null) {
		return;
	}
	Method[] methods = config.getClass().getMethods();
	// 遍歷 config 類方法集合
	for (Method method : methods) {
		try {
			String name = method.getName();
			// 找到知足如下的方法:以set/is 開頭,非 getClass;方法修飾符爲 public;方法參數個數爲 0;返回類型爲基本類型
			if ((name.startsWith("get") || name.startsWith("is"))
					&& !"getClass".equals(name)
					&& Modifier.isPublic(method.getModifiers())
					&& method.getParameterTypes().length == 0
					&& isPrimitive(method.getReturnType())) {
				// 獲取 parameter 註解
				Parameter parameter = method.getAnnotation(Parameter.class);
				// @Parameter(excluded = true),直接跳過
				if (method.getReturnType() == Object.class || parameter != null && parameter.excluded()) {
					continue;
				}
				int i = name.startsWith("get") ? 3 : 2;
				String prop = StringUtils.camelToSplitName(name.substring(i, i + 1).toLowerCase() + name.substring(i + 1), ".");
				String key;
				if (parameter != null && parameter.key().length() > 0) {
					key = parameter.key();
				} else {
					key = prop;
				}
				// 利用反射調用 config 類中的 get/is 方法
				Object value = method.invoke(config);
				String str = String.valueOf(value).trim();
				if (value != null && str.length() > 0) {
					// 是否須要轉義,UTF-8
					if (parameter != null && parameter.escaped()) {
						str = URL.encode(str);
					}
					if (parameter != null && parameter.append()) {
						String pre = parameters.get(Constants.DEFAULT_KEY + "." + key);
						if (pre != null && pre.length() > 0) {
							str = pre + "," + str;
						}
						pre = parameters.get(key);
						if (pre != null && pre.length() > 0) {
							str = pre + "," + str;
						}
					}
					if (prefix != null && prefix.length() > 0) {
						key = prefix + "." + key;
					}
					// key/value 添加到 parameters 集合
					parameters.put(key, str);
				} else if (parameter != null && parameter.required()) {
					throw new IllegalStateException(config.getClass().getSimpleName() + "." + key + " == null");
				}
				// 方法名爲 getParameters();方法修飾符爲 public;方法形參個數爲0;返回類型爲 Map
			} else if ("getParameters".equals(name)
					&& Modifier.isPublic(method.getModifiers())
					&& method.getParameterTypes().length == 0
					&& method.getReturnType() == Map.class) {
				Map<String, String> map = (Map<String, String>) method.invoke(config, new Object[0]);
				if (map != null && map.size() > 0) {
					String pre = (prefix != null && prefix.length() > 0 ? prefix + "." : "");
					for (Map.Entry<String, String> entry : map.entrySet()) {
						parameters.put(pre + entry.getKey().replace('-', '.'), entry.getValue());
					}
				}
			}
		} catch (Exception e) {
			throw new IllegalStateException(e.getMessage(), e);
		}
	}
}
複製代碼

該方法會調用當前類對象的 isXXX/getXXX 方法(非 getClass 方法;方法修飾符爲 public;形參個數爲 0;返回類型爲基本類型),獲取其返回值構造鍵值對添加到指定 map 集合中;同時也會解析 getParameters() 返回的結果,構造鍵值對注入到 map 集合中。

本BLOG上原創文章未經本人許可,不得用於商業用途及傳統媒體。網絡媒體轉載請註明出處,不然屬於侵權行爲。https://juejin.im/post/5c2b7ab46fb9a049d236273b

相關文章
相關標籤/搜索