一、export()方法调用时机
为了解答 export() 调用时机问题,我们需要关注 ServiceBean 类中的三个方法
setApplicationContext(ApplicationContext applicationContext)
ServiceBean 实现了 ApplicationContextAware 接口,在 ServiceBean 初始化后,会调用 setApplicationContext 注入 Spring 上下文;afterPropertiesSet()
注入 ApplicationConfig、registries、protocols 等属性;onApplicationEvent(ContextRefreshedEvent event)
这里接受的 event 事件类型为 ContextRefreshedEvent。当 applicationContext 被初始化或者刷新时,会调用该方法。这三个方法在 Spring 生命周期中被调用的顺序大致如下图所示setApplicationContext()——> afterPropertiesSet() ——> onApplicationEvent()我们结合代码继续看
代码执行逻辑大致如下:
首先执行 setApplicationContext() 方法,注入上下文。这里的 supportedApplicationListener 用于判断 Spring 是否支持 Spring 监听机制。
执行 afterPropertiesSet() 方法。如果 supportedApplicationListener 值为 false,调用 export() 方法。
执行 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() {@Overridepublic void run() {doExport();}}, delay, TimeUnit.MILLISECONDS);} else {doExport();}}
export()方法比较简单。注意这里有个 delay 变量,我们可以使用该变量延迟执行 export() 方法。
继续看 doExport() 方法protected synchronized void doExport() {// 省略...doExportUrls();ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), ref, interfaceClass);ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel);}private void doExportUrls() {ListregistryURLs = loadRegistries(true);for (ProtocolConfig protocolConfig : protocols) {doExportUrlsFor1Protocol(protocolConfig, registryURLs);}}
doExport()方法省略了很多 ServiceBean 配置校验和初始化代码。大家有兴趣可以自行阅览。这里直接划重点!!!分析 doExportUrls() 方法!!!
先看 loadRegistries() 方法:loadRegistries()
protected ListloadRegistries(boolean provider) {checkRegistry();List registryList = new ArrayList ();// 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 map = new HashMap ();// 将 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.conta×××Key("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×tamp=1549005672530List urls = UrlUtils.parseURLs(address, map);for (URL url : urls) {// 将此时 url 的 protocol 保存到 registry 参数中url = url.addParameter(Constants.REGISTRY_KEY, url.getProtocol());// 设置 url protcol 属性为 registryurl = 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 />
标签。
三、本地暴露
介绍完 loadRegistries() 方法,我们接着看 doExportUrlsFor1Protocol()。doExportUrlsFor1Protocol() 方法比较长,这里我们挑出和本地暴露相关的内容进行分析。
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 机制(详见系列第一篇)。让我们看看这里到底做了什么?
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() 方法。
四、核心方法 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 ×××tead.", 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() 方法,如果不明白可以先看下 。
public class StubProxyFactoryWrapper implements ProxyFactory {publicInvoker getInvoker(T proxy, Class type, URL url) throws RpcException {return proxyFactory.getInvoker(proxy, type, url);}}public class JavassistProxyFactory extends AbstractProxyFactory {public Invoker getInvoker(T proxy, Class type, URL url) {// TODO Wrapper cannot handle this scenario correctly: the classname conta××× '$'final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);return new AbstractProxyInvoker (proxy, type, url) {@Overrideprotected 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 ×××tead.", 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 类有疑问的可以看下。
这里会涉及到一个方法 buildInvokerChain() 方,道它用于构建一个调用链。整体调用时序简图如下所示: 最后 exportLocal() 方法中获取到的是一个 InjvmExporter 对象,并将其添加到 ServiceConfig 类的 exporters 集合中。buildInvokerChain()
ProtocolFilterWrapper.javaprivate staticInvoker buildInvokerChain(final Invoker invoker, String key, String group) {Invoker last = invoker;List 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 next = last;last = new Invoker () {// 省略 Invoker 构建代码...@Overridepublic Result invoke(Invocation invocation) throws RpcException {return filter.invoke(next, invocation);}// 省略 Invoker 构建代码...};}}return last;}
buildInvokerChain() 方法用于构建调用链,初步浏览下来发现调用链应该是由 Filter 扩展类构成。那么这些 Filter 扩展类又从何而来呢?这行代码很关键!!!
Listfilters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
对于这段代码我们应该有很强的亲切感,但仔细看又稍稍有所不同。实际上被 @Activate 注解标记的扩展类会被加载到 ExtensionLoader 类的 cachedActivates 集合中。
我们在调用 ExtensionLoader 类的 getActivateExtension() 时,会根据我们传入的 key 和 group 值从 cachedActivates 集合中获取满足当前条件的 filter 对象。拿到 filters 集合后,会用链表的形式拼接 filter 调用链,举个例子:假设当前获取到的 filters 集合中保存的 filter 对象为 filter0、filter1、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();// 1、方法长度大于3;2、方法以 set 开头;3、方法修饰符类型为 public;4、形参个数为 1;5、形参类型为基本类型if (name.length() > 3 && name.startsWith("set") && Modifier.isPublic(method.getModifiers())&& method.getParameterTypes().length == 1 && isPrimitive(method.getParameterTypes()[0])) {// camelToSplitName: 举个例子 ApplicationConfig——>application.configString 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.xxxString 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(Mapparameters, Object config) {appendParameters(parameters, config, null);}protected static void appendParameters(Map 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-8if (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 map = (Map ) method.invoke(config, new Object[0]);if (map != null && map.size() > 0) {String pre = (prefix != null && prefix.length() > 0 ? prefix + "." : "");for (Map.Entry 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 集合中。