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()
我们结合代码继续看
-
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();
-
}
-
}
代码执行逻辑大致如下:
- 首先执行 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() {
-
@Override
-
public 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() {
-
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×tamp=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() 小节。
本地暴露
介绍完 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 机制(详见系列第一篇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 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 机制。
-
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、filter1、filter2。我们对 filters 集合进行倒序遍历。最后获得的 last 其实为新建的 ivk2 对象。如果我们调用 last 的 invoke 方法,调用链如下图所示:
End
本文介绍了 Export() 方法被调用的时机以及基本流程。并且花了一定篇幅对 Dubbo 服务本地暴露进行了分析。其中掺杂了不少代码的分析,可能没有面面俱到吧。还是建议大家自己自己 Debug 一下,很多东西瞬间秒懂,有助于源码理解。