export()方法调用时机

为了解答 export() 调用时机问题,我们需要关注 ServiceBean 类中的三个方法

  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()
    我们结合代码继续看
 
  1. public void setApplicationContext(ApplicationContext applicationContext) {

  2. this.applicationContext = applicationContext;

  3. SpringExtensionFactory.addApplicationContext(applicationContext);

  4. supportedApplicationListener = addApplicationListener(applicationContext, this);

  5. }

  6.  
  7. public void onApplicationEvent(ContextRefreshedEvent event) {

  8. if (!isExported() && !isUnexported()) {

  9. if (logger.isInfoEnabled()) {

  10. logger.info("The service ready on spring started. service: " + getInterface());

  11. }

  12. export();

  13. }

  14. }

  15.  
  16. public void afterPropertiesSet() throws Exception {

  17. // 省略...

  18. if (!supportedApplicationListener) {

  19. export();

  20. }

  21. }

代码执行逻辑大致如下:

  1. 首先执行 setApplicationContext() 方法,注入上下文。这里的 supportedApplicationListener 用于判断 Spring 是否支持 Spring 监听机制。
  2. 执行 afterPropertiesSet() 方法。如果 supportedApplicationListener 值为 false,调用 export() 方法。
  3. 执行 onApplicationEvent() 方法。如果没有执行过 export() 以及 unexport() 方法,调用 export() 方法。
    通过上面简单的分析我们可以看到 export() 方法只会在 onApplicationEvent() 和 export() 方法中调用一次。

export() 方法解析

 
  1. public synchronized void export() {

  2. if (provider != null) {

  3. if (export == null) {

  4. export = provider.getExport();

  5. }

  6. if (delay == null) {

  7. delay = provider.getDelay();

  8. }

  9. }

  10. if (export != null && !export) {

  11. return;

  12. }

  13.  
  14. if (delay != null && delay > 0) {

  15. delayExportExecutor.schedule(new Runnable() {

  16. @Override

  17. public void run() {

  18. doExport();

  19. }

  20. }, delay, TimeUnit.MILLISECONDS);

  21. } else {

  22. doExport();

  23. }

  24. }

export()方法比较简单。注意这里有个 delay 变量,我们可以使用该变量延迟执行 export() 方法。
继续看 doExport() 方法

 
  1. protected synchronized void doExport() {

  2. // 省略...

  3. doExportUrls();

  4. ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), ref, interfaceClass);

  5. ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel);

  6.  
  7. private void doExportUrls() {

  8. List<URL> registryURLs = loadRegistries(true);

  9. for (ProtocolConfig protocolConfig : protocols) {

  10. doExportUrlsFor1Protocol(protocolConfig, registryURLs);

  11. }

  12. }

doExport()方法省略了很多 ServiceBean 配置校验和初始化代码。大家有兴趣可以自行阅览。这里直接划重点!!!分析 doExportUrls() 方法!!!
先看 loadRegistries() 方法:

loadRegistries()

 
  1. protected List<URL> loadRegistries(boolean provider) {

  2. checkRegistry();

  3. List<URL> registryList = new ArrayList<URL>();

  4. // registries 在 afterPropertiesSet() 方法中初始化

  5. if (registries != null && !registries.isEmpty()) {

  6. for (RegistryConfig config : registries) {

  7. String address = config.getAddress();

  8. if (address == null || address.length() == 0) {

  9. address = Constants.ANYHOST_VALUE;

  10. }

  11. String sysaddress = System.getProperty("dubbo.registry.address");

  12. if (sysaddress != null && sysaddress.length() > 0) {

  13. address = sysaddress;

  14. }

  15. if (address.length() > 0 && !RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {

  16. Map<String, String> map = new HashMap<String, String>();

  17. // 将 application/config 部分属性整合到 map 中,详细见:

  18. appendParameters(map, application);

  19. appendParameters(map, config);

  20. map.put("path", RegistryService.class.getName());

  21. map.put("dubbo", Version.getProtocolVersion());

  22. map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis()));

  23. if (ConfigUtils.getPid() > 0) {

  24. map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid()));

  25. }

  26. if (!map.containsKey("protocol")) {

  27. if (ExtensionLoader.getExtensionLoader(RegistryFactory.class).hasExtension("remote")) {

  28. map.put("protocol", "remote");

  29. } else {

  30. map.put("protocol", "dubbo");

  31. }

  32. }

  33. // 构建 url ,返回结果类似 zookeeper://192.168.0.100:2181/org.apache.dubbo.registry.RegistryService?

  34. // application=demo-provider&dubbo=2.0.2&pid=22705&qos.port=22222&timestamp=1549005672530

  35. List<URL> urls = UrlUtils.parseURLs(address, map);

  36. for (URL url : urls) {

  37. // 将此时 url 的 protocol 保存到 registry 参数中

  38. url = url.addParameter(Constants.REGISTRY_KEY, url.getProtocol());

  39. // 设置 url protcol 属性为 registry

  40. url = url.setProtocol(Constants.REGISTRY_PROTOCOL);

  41. if ((provider && url.getParameter(Constants.REGISTER_KEY, true))

  42. || (!provider && url.getParameter(Constants.SUBSCRIBE_KEY, true))) {

  43. registryList.add(url);

  44. }

  45. }

  46. }

  47. }

  48. }

  49. return registryList;

  50. }

loadRegistries() 用于加载注册中心。概括来说就是用于解析我们在配置文件中定义的 <dubbo:registry /> 标签。
checkRegistry() 方法用于校验注册中心配置校验,里面有一些版本兼容的代码。appendParameters() 方法详见 appendParameters() 小节。

本地暴露

介绍完 loadRegistries() 方法,我们接着看 doExportUrlsFor1Protocol()。doExportUrlsFor1Protocol() 方法比较长,这里我们挑出和本地暴露相关的内容进行分析。

 
  1. if (!Constants.SCOPE_NONE.equalsIgnoreCase(scope)) {

  2. // export to local if the config is not remote (export to remote only when config is remote)

  3. if (!Constants.SCOPE_REMOTE.equalsIgnoreCase(scope)) {

  4. exportLocal(url);

  5. }

  6. if (!Constants.SCOPE_LOCAL.equalsIgnoreCase(scope)) {

  7. // 远程暴露相关内容,省略...

  8. }

  9. }

  10. private void exportLocal(URL url) {

  11. if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {

  12. URL local = URL.valueOf(url.toFullString())

  13. .setProtocol(Constants.LOCAL_PROTOCOL)

  14. .setHost(LOCALHOST)

  15. .setPort(0);

  16. Exporter<?> exporter = protocol.export(

  17. proxyFactory.getInvoker(ref, (Class) interfaceClass, local));

  18. exporters.add(exporter);

  19. logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry");

  20. }

  21. }

看到 exportLocal() 方法,意味着我们已经快要直达本地服务暴露的核心了!更令人按捺不住的是!这里又用到了 Dubbo 中的 SPI 机制(详见系列第一篇Dubbo SPI)。让我们看看这里到底做了什么?

 
  1. private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

  2. private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();

熟悉的配方熟悉的料,在这里我们获取了 Protocol 和 ProxyFactory 对应的自适应扩展类。根据方法调用的嵌套逻辑,先来看 ProxyFactory 自适应扩展类 ProxyFactory$Adaptive 的 getInvoker() 方法。

核心方法 proxyFactory.getInvoker()

 
  1. public class ProxyFactory$Adaptive implements org.apache.dubbo.rpc.ProxyFactory {

  2. 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 {

  3. if (arg2 == null) throw new IllegalArgumentException("url == null");

  4. org.apache.dubbo.common.URL url = arg2;

  5. String extName = url.getParameter("proxy", "javassist");

  6. if(extName == null) throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.ProxyFactory) name from url(" + url.toString() + ") use keys([proxy])");

  7. org.apache.dubbo.rpc.ProxyFactory extension = null;

  8. try {

  9. extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(extName);

  10. }catch(Exception e){

  11. if (count.incrementAndGet() == 1) {

  12. logger.warn("Failed to find extension named " + extName + " for type org.apache.dubbo.rpc.ProxyFactory, will use default extension javassist instead.", e);

  13. }

  14. extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension("javassist");

  15. }

  16. return extension.getInvoker(arg0, arg1, arg2);

  17. }

  18. }

这里我们实际会去调用 StubProxyFactoryWrapper 包装类的 getInvoker() 方法,如果不明白可以先看下 【Dubbo源码阅读系列】之 Dubbo SPI 机制

 
  1. public class StubProxyFactoryWrapper implements ProxyFactory {

  2. public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) throws RpcException {

  3. return proxyFactory.getInvoker(proxy, type, url);

  4. }

  5. }

  6. public class JavassistProxyFactory extends AbstractProxyFactory {

  7. public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {

  8. // TODO Wrapper cannot handle this scenario correctly: the classname contains '$'

  9. final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);

  10. return new AbstractProxyInvoker<T>(proxy, type, url) {

  11. @Override

  12. protected Object doInvoke(T proxy, String methodName,

  13. Class<?>[] parameterTypes,

  14. Object[] arguments) throws Throwable {

  15. return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);

  16. }

  17. };

  18. }

  19. }

结合上面的代码我们发现,发现最后调用的是 JavassistProxyFactory 类的 getInvoker() 方法。其中 wrapper 是动态生成的代理对象。最后返回一个 AbstractProxyInvoker 对象,doInvoke() 方***调用 wrapper 代理类的 invokeMethod() 方法,其中 invokeMethod() 方法大概如下所示:

 
  1. public Object invokeMethod(Object o, String n, Class[] p, Object[] v) throws java.lang.reflect.InvocationTargetException {

  2. org.apache.dubbo.demo.provider.DemoServiceImpl w;

  3. try {

  4. w = ((org.apache.dubbo.demo.provider.DemoServiceImpl) $1);

  5. } catch (Throwable e) {

  6. throw new IllegalArgumentException(e);

  7. }

  8. try {

  9. if ("sayHello".equals($2) && $3.length == 1) {

  10. return ($w) w.sayHello((java.lang.String) $4[0]);

  11. }

  12. } catch (Throwable e) {

  13. throw new java.lang.reflect.InvocationTargetException(e);

  14. }

  15. throw new org.apache.dubbo.common.bytecode.NoSuchMethodException("Not found method \"" + $2 + "\" in class org.apache.dubbo.demo.provider.DemoServiceImpl.");

  16. }

稍微有一点绕,至少我们已经看完了 proxyFactory.getInvoker() 方法了,我们获取到了一个包装了动态代理类的 AbstractProxyInvoker 对象。接下来继续看 protocol.export() 方法。

核心方法 protocol.export()

 
  1. public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException {

  2. if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");

  3. if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");org.apache.dubbo.common.URL url = arg0.getUrl();

  4. String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );

  5. if(extName == null) throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");

  6. org.apache.dubbo.rpc.Protocol extension = null;

  7. try {

  8. extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);

  9. }catch(Exception e){

  10. if (count.incrementAndGet() == 1) {

  11. logger.warn("Failed to find extension named " + extName + " for type org.apache.dubbo.rpc.Protocol, will use default extension dubbo instead.", e);

  12. }

  13. extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension("dubbo");

  14. }

  15. return extension.export(arg0);

  16. }

由于此时的 url 中 protocol 值为 injvm(url 经过 setProtocol(LOCAL_PROTOCOL) 操作后 protocol 已经更新为 injvm),因此我们这里获得的扩展类实际为包装了 InjvmProtocol 的包装类对象,对 wrapper 类有疑问的可以看下【Dubbo源码阅读系列】之 Dubbo SPI 机制
这里会涉及到一个方法 buildInvokerChain() 方,道它用于构建一个调用链。
整体调用时序简图如下所示:


最后 exportLocal() 方法中获取到的是一个 InjvmExporter 对象,并将其添加到 ServiceConfig 类的 exporters 集合中。

buildInvokerChain()

 
  1. ProtocolFilterWrapper.java

  2. private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {

  3. Invoker<T> last = invoker;

  4. List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);

  5. if (!filters.isEmpty()) {

  6. for (int i = filters.size() - 1; i >= 0; i--) {

  7. final Filter filter = filters.get(i);

  8. final Invoker<T> next = last;

  9. last = new Invoker<T>() {

  10. // 省略 Invoker 构建代码...

  11. @Override

  12. public Result invoke(Invocation invocation) throws RpcException {

  13. return filter.invoke(next, invocation);

  14. }

  15. // 省略 Invoker 构建代码...

  16. };

  17. }

  18. }

  19. return last;

  20. }

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、filter1、filter2。我们对 filters 集合进行倒序遍历。最后获得的 last 其实为新建的 ivk2 对象。如果我们调用 last 的 invoke 方法,调用链如下图所示:

End

本文介绍了 Export() 方法被调用的时机以及基本流程。并且花了一定篇幅对 Dubbo 服务本地暴露进行了分析。其中掺杂了不少代码的分析,可能没有面面俱到吧。还是建议大家自己自己 Debug 一下,很多东西瞬间秒懂,有助于源码理解。