前言
我们在第2章节里面,已经讲到Dubbo的初始化流程。Dubbo的初始化是随着Spring容器Bean的实例化而进行的,今天我们重点看这样一个节点,它在配置文件中是这样的:
它会完成Dubbo服务暴露的逻辑,我们先看下大概流程。
一、开始
上述配置文件中的节点信息对应的处理类是ServiceBean
。我们先看下它的结构
public class ServiceBean extends ServiceConfig implements
InitializingBean, DisposableBean, ApplicationContextAware,
ApplicationListener, BeanNameAware {}
我们可以看到,这个类实现了Spring的不同接口,这就意味着Spring在不同的时机就会调用到相应方法。
1、设置上下文
在Spring完成实例化和IOC之后,会调用到invokeAwareInterfaces
方法,来判断Bean是否实现了Aware
接口,然后调用对应的方法。
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
SpringExtensionFactory.addApplicationContext(applicationContext);
if (applicationContext != null) {
SPRING_CONTEXT = applicationContext;
try {
Method method = applicationContext.getClass().getMethod(
"addApplicationListener", new Class>[]{ApplicationListener.class});
method.invoke(applicationContext, new Object[]{this});
supportedApplicationListener = true;
} catch (Throwable t) {
//省略无关代码....
}
}
}
2、初始化方法
然后我们还看到它实现了InitializingBean
接口,那么初始化方法afterPropertiesSet
也是跑不掉呀。在这里面,就是拿到Dubbo中的应用信息、注册信息、协议信息等,设置到变量中。最后有个判断方法值得我们注意isDelay
当方法返回 true 时,表示无需延迟导出。返回 false 时,表示需要延迟导出。
public void afterPropertiesSet() throws Exception {
if (getProvider() == null) {
//......
}
if (getApplication() == null
&& (getProvider() == null || getProvider().getApplication() == null)) {
//......
}
if (getModule() == null
&& (getProvider() == null || getProvider().getModule() == null)) {
//......
}
if ((getRegistries() == null || getRegistries().isEmpty())) {
//......
}
if ((getProtocols() == null || getProtocols().isEmpty())
&& (getProvider() == null || getProvider().getProtocols() == null ||
getProvider().getProtocols().isEmpty())) {
//......
}
if (getPath() == null || getPath().length() == 0) {
if (beanName != null && beanName.length() > 0
&& getInterface() != null && getInterface().length() > 0
&& beanName.startsWith(getInterface())) {
setPath(beanName);
}
}
if (!isDelay()) {
export();
}
}
3、开始暴露
这个方法是在Spring 上下文刷新事件后被调用到,它是服务暴露的入口方法。
public void onApplicationEvent(ContextRefreshedEvent event) {
//是否有延迟暴露 && 是否已暴露 && 是不是已被取消暴露
if (isDelay() && !isExported() && !isUnexported()) {
if (logger.isInfoEnabled()) {
logger.info("The service ready on spring started. service: " + getInterface());
}
//暴露服务
export();
}
}
二、准备工作
Dubbo在暴露服务之前,要检查各种配置 ,设置参数信息,还要补充一些缺省的配置项,然后封装URL对象信息 。在这里,我们必须重视URL对象,Dubbo 使用 URL 作为配置载体,所有的拓展点都是通过 URL 获取配置。
1、检查配置
export()
方法在服务暴露之前,有两个配置项要检查。是否暴露服务以及是否延迟暴露服务。
public synchronized void export() {
// 获取 export 和 delay 配置
if (provider != null) {
if (export == null) {
export = provider.getExport();
}
if (delay == null) {
delay = provider.getDelay();
}
}
// 如果 export 为 false,则不暴露服务
if (export != null && !export) {
return;
}
// delay > 0,延时暴露服务
if (delay != null && delay > 0) {
delayExportExecutor.schedule(new Runnable() {
@Override
public void run() {
doExport();
}
}, delay, TimeUnit.MILLISECONDS);
}else {
doExport();
}
}
很显然,如果我们不想暴露服务,这样可以来配置:
同样的,如果我们想延迟暴露服务,就可以这样来配置它:
在Dubbo决定要暴露服务之后,还要做一些事情。
检查服务接口合法性
检查Conifg核心配置类等是否为空,为空就从其他配置中获取相应实例
区分泛化服务和普通服务
检查各种对象是否为空,创建或抛出异常等
protected synchronized void doExport() {
if (unexported) {
throw new IllegalStateException("Already unexported!");
}
if (exported) {
return;
}
exported = true;
if (interfaceName == null || interfaceName.length() == 0) {
throw new IllegalStateException("
interface not allow null!");
}
checkDefault();
if (provider != null) {
//检查配置对象是否为空并赋值
}
if (module != null) {
//检查配置对象是否为空并赋值
}
if (application != null) {
//检查配置对象是否为空并赋值
}
//区分泛化类和普通类
if (ref instanceof GenericService) {
interfaceClass = GenericService.class;
if (StringUtils.isEmpty(generic)) {
generic = Boolean.TRUE.toString();
}
} else {
//返回接口Class对象并检查
try {
interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
.getContextClassLoader());
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e.getMessage(), e);
}
checkInterfaceAndMethods(interfaceClass, methods);
checkRef();
generic = Boolean.FALSE.toString();
}
//检查各种对象是否为空,创建或抛出异常
checkApplication();
checkRegistry();
checkProtocol();
appendProperties(this);
checkStubAndMock(interfaceClass);
if (path == null || path.length() == 0) {
path = interfaceName;
}
//服务暴露
doExportUrls();
ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), this, ref);
ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel);
}
2、多协议多注册中心
2.1、配置
Dubbo 允许我们使用不同的协议暴露服务,也允许我们向多个注册中心注册服务。
比如我们想同时使用dubbo、rmi
两种协议来暴露服务:
又或者我们需要把服务注册到zookeeper、redis
:
我们这样配置后,在zookeeper、redis
里面都会保存基于两种协议的服务信息。
首先在redis中我们执行命令:
hgetall /dubbo/com.viewscenes.netsupervisor.service.InfoUserService/providers
得到以下结果:
1) "rmi://192.168.100.74:1099/com.viewscenes.netsupervisor.service.InfoUserService?anyhost=true&application=dubbo_producer1&dubbo=2.6.2&generic=false&interface=com.viewscenes.netsupervisor.service.InfoUserService&methods=getUserById,getAllUser,insertInfoUser,deleteUserById&pid=7064&side=provider×tamp=1545804399128"
2) "1545805385379"
3) "dubbo://192.168.100.74:20880/com.viewscenes.netsupervisor.service.InfoUserService?anyhost=true&application=dubbo_producer1&dubbo=2.6.2&generic=false&interface=com.viewscenes.netsupervisor.service.InfoUserService&methods=getUserById,getAllUser,insertInfoUser,deleteUserById&pid=7064&side=provider×tamp=1545804391176"
4) "1545805385379"
同时,我们在zookeeper中执行命令:
ls /dubbo/com.viewscenes.netsupervisor.service.InfoUserService/providers
得到以下结果:
[dubbo%3A%2F%2F192.168.100.74%3A20880%2Fcom.viewscenes.netsupervisor.service.InfoUserService%3Fanyhost%3Dtrue%26application%3
Ddubbo_producer1%26dubbo%3D2.6.2%26generic%3Dfalse%26interface%3Dcom.viewscenes.netsupervisor.service.InfoUserService%26methods%3D
getUserById%2CgetAllUser%2CinsertInfoUser%2CdeleteUserById%26pid%3D7064%26side%3Dprovider%26timestamp%3D1545804391176,
rmi%3A%2F%2F192.168.100.74%3A1099%2Fcom.viewscenes.netsupervisor.service.InfoUserService%3Fanyhost%3Dtrue%26application%3
Ddubbo_producer1%26dubbo%3D2.6.2%26generic%3Dfalse%26interface%3Dcom.viewscenes.netsupervisor.service.InfoUserService%26methods%3D
getUserById%2CgetAllUser%2CinsertInfoUser%2CdeleteUserById%26pid%3D7064%26side%3Dprovider%26timestamp%3D1545804399128]
2.2、多注册中心
首先是多个注册中心的加载,loadRegistries
方法返回一个List
。基于上面的配置文件,这里的registries
就是一个长度为2的List
,最终将它们解析为List
。
protected List loadRegistries(boolean provider) {
//检查配置
checkRegistry();
List registryList = new ArrayList();
if (registries != null && !registries.isEmpty()) {
for (RegistryConfig config : registries) {
String address = config.getAddress();
if (address == null || address.length() == 0) {
// 若 address 为空,则将其设为 0.0.0.0
address = Constants.ANYHOST_VALUE;
}
// 从系统属性中加载注册中心地址
String sysaddress = System.getProperty("dubbo.registry.address");
if (sysaddress != null && sysaddress.length() > 0) {
address = sysaddress;
}
//检查address合法性以及从封装map配置信息
if (address != null && address.length() > 0
&& !RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {
Map map = new HashMap();
appendParameters(map, application);
appendParameters(map, config);
map.put("path", RegistryService.class.getName());
map.put("dubbo", Version.getVersion());
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对象
List urls = UrlUtils.parseURLs(address, map);
for (URL url : urls) {
url = url.addParameter(Constants.REGISTRY_KEY, url.getProtocol());
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;
}
2.3 多协议支持
多协议的支持,也是一样,遍历数组循环暴露服务。其中,doExportUrlsFor1Protocol
方法为服务暴露的具体流程。
private void doExportUrls() {
//加载注册中心
List registryURLs = loadRegistries(true);
//遍历协议,为每个协议暴露一个服务
for (ProtocolConfig protocolConfig : protocols) {
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
三、服务暴露
经过各种配置检查后,来到doExportUrlsFor1Protocol
方法,它是基于单个协议来暴露服务。方法的前半部分是根据配置信息组装URL对象。前面我们说过,对于Dubbo来说,URL对象信息至关重要,它是Dubbo配置的载体。
关于它的组装过程,无非就是各种设置属性,我们不再细看,我们看看封装好的URL对象信息是什么样的。
dubbo://192.168.100.74:20880/com.viewscenes.netsupervisor.service.InfoUserService?
anyhost=true&application=dubbo_producer1&bind.ip=192.168.100.74&bind.port=20880&dubbo=2.6.2&generic=false&
interface=com.viewscenes.netsupervisor.service.InfoUserService&methods=getUserById,getAllUser,insertInfoUser,deleteUserById&pid=8596&side=provider×tamp=1545807234666
rmi://192.168.100.74:1099/com.viewscenes.netsupervisor.service.InfoUserService?
anyhost=true&application=dubbo_producer1&bind.ip=192.168.100.74&bind.port=1099&dubbo=2.6.2&generic=false&
interface=com.viewscenes.netsupervisor.service.InfoUserService&methods=getUserById,getAllUser,insertInfoUser,deleteUserById&pid=8596&side=provider×tamp=1545807267085
有服务暴露之前,我们还可以对暴露方式有所选择。
- 本地暴露
- 远程暴露
- 不暴露
它们的配置对应如下:
默认情况下,Dubbo两种方式都会暴露。即本地+远程。
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List registryURLs) {
//省略设置URL对象信息过程...
String scope = url.getParameter(Constants.SCOPE_KEY);
// 如果 scope = none,则不暴露服务
if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {
// scope != remote,服务暴露到本地
if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
exportLocal(url);
}
// scope != local,服务暴露到远程
if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) {
if (registryURLs != null && !registryURLs.isEmpty()) {
//循环注册中心进行暴露服务
for (URL registryURL : registryURLs) {
url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));
URL monitorUrl = loadMonitor(registryURL);
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);
}
//为服务类ref生成invoker
Invoker> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass,
registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
// DelegateProviderMetaDataInvoker 用于持有 Invoker 和 ServiceConfig
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
//调用对应的协议,暴露服务
Exporter> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
} else {
Invoker> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
Exporter> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
}
}
this.urls.add(url);
}
3.1、创建Invoker
在开始之前,我们必须了解另外一个东西:Invoker 。在 Dubbo 中,Invoker 是一个非常重要的模型。在服务提供端,以及服务引用端均会出现 Invoker。
Dubbo官网对它是这样说明的:
Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
在Dubbo中,Invoker通过以下代码创建:
ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).
getAdaptiveExtension();
Invoker> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
首先通过扩展点加载器创建ProxyFactory接口的自适应扩展类,在Dubbo中,默认是JavassistProxyFactory
,所以当调用proxyFactory.getInvoker
就会调用到JavassistProxyFactory
public class JavassistProxyFactory extends AbstractProxyFactory {
public Invoker getInvoker(T proxy, Class type, URL url) {
// 为目标类创建 Wrapper
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().
indexOf('$') < 0 ? proxy.getClass() : type);
// 创建匿名 Invoker 类对象,并实现 doInvoke 方法。
return new AbstractProxyInvoker(proxy, type, url) {
@Override
protected Object doInvoke(T proxy, String methodName,
Class>[] parameterTypes,
Object[] arguments) throws Throwable {
// 调用 Wrapper 的 invokeMethod 方法,invokeMethod 最终会调用目标方法
return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
}
};
}
}
如上方法,它主要完成了两件事:为目标类创建 Wrapper和实现doInvoke方法。这样一来,当调用AbstractProxyInvoker.doInvoke()
方法时,实际则调用的是,wrapper的invokeMethod()
方法。
那么,wrapper
又是怎么来的呢?它通过ClassGenerator
创建而来。
ClassGenerator
是 Dubbo 自己封装的,该类的核心是 toClass() 的重载方法 toClass(ClassLoader, ProtectionDomain),该方法通过 javassist 构建 Class。创建过程涉及的代码较长,我们不再细看,主要看看这个wrapper类生成后的样子。
package com.alibaba.dubbo.common.bytecode;
import com.viewscenes.netsupervisor.entity.InfoUser;
import com.viewscenes.netsupervisor.service.impl.InfoUserServiceImpl;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
public class Wrapper1 extends Wrapper implements ClassGenerator.DC {
public static String[] pns;
public static Map pts;
public static String[] mns;
public static String[] dmns;
public static Class[] mts0;
public static Class[] mts1;
public static Class[] mts2;
public static Class[] mts3;
public Class getPropertyType(String paramString) {
return ((Class) pts.get(paramString));
}
public String[] getPropertyNames() {
return pns;
}
public Object invokeMethod(Object paramObject, String paramString, Class[] paramArrayOfClass,
Object[] paramArrayOfObject) throws InvocationTargetException {
InfoUserServiceImpl localInfoUserServiceImpl;
try {
localInfoUserServiceImpl = (InfoUserServiceImpl) paramObject;
} catch (Throwable localThrowable1) {
throw new IllegalArgumentException(localThrowable1);
}
try {
if (("getAllUser".equals(paramString)) && (paramArrayOfClass.length == 0))
return localInfoUserServiceImpl.getAllUser();
if (("getUserById".equals(paramString)) && (paramArrayOfClass.length == 1))
return localInfoUserServiceImpl.getUserById((String) paramArrayOfObject[0]);
if (("insertInfoUser".equals(paramString)) && (paramArrayOfClass.length == 1)) {
localInfoUserServiceImpl.insertInfoUser((InfoUser) paramArrayOfObject[0]);
return null;
}
if (("deleteUserById".equals(paramString)) && (paramArrayOfClass.length == 1)) {
localInfoUserServiceImpl.deleteUserById((String) paramArrayOfObject[0]);
return null;
}
} catch (Throwable localThrowable2) {
throw new InvocationTargetException(localThrowable2);
}
throw new NoSuchMethodException("Not found method \"" + paramString
+ "\" in class com.viewscenes.netsupervisor.service.impl.InfoUserServiceImpl.");
}
public Object getPropertyValue(Object paramObject, String paramString) {
InfoUserServiceImpl localInfoUserServiceImpl;
try {
localInfoUserServiceImpl = (InfoUserServiceImpl) paramObject;
} catch (Throwable localThrowable) {
throw new IllegalArgumentException(localThrowable);
}
if (paramString.equals("allUser"))
return localInfoUserServiceImpl.getAllUser();
throw new NoSuchPropertyException("Not found property \"" + paramString
+ "\" filed or setter method in class com.viewscenes.netsupervisor.service.impl.InfoUserServiceImpl.");
}
public void setPropertyValue(Object paramObject1, String paramString, Object paramObject2) {
try {
InfoUserServiceImpl localInfoUserServiceImpl = (InfoUserServiceImpl) paramObject1;
} catch (Throwable localThrowable) {
throw new IllegalArgumentException(localThrowable);
}
throw new NoSuchPropertyException("Not found property \"" + paramString
+ "\" filed or setter method in class com.viewscenes.netsupervisor.service.impl.InfoUserServiceImpl.");
}
public String[] getMethodNames() {
return mns;
}
public String[] getDeclaredMethodNames() {
return dmns;
}
public boolean hasProperty(String paramString) {
return pts.containsKey(paramString);
}
}
我们重点可以关注invokeMethod
方法,当调用到它的时候,它根据参数直接调用的是目标类(ref)的对应方法。
综上所述,proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
这句代码的返回值就是JavassistProxyFactory
的实例,如下图示:
3.2、本地暴露
上面我们已经看到,如果scope != remote
就调用本地暴露。
if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
exportLocal(url);
}
本地暴露方法比较简单,首先根据 URL 协议头决定是否导出服务。若需导出,则创建一个新的 URL 并将协议头、主机名以及端口设置成新的值。然后创建 Invoker,并调用 InjvmProtocol 的 export 方法导出服务。
private void exportLocal(URL url) {
//判断协议头
if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
//设置本地暴露协议URL
URL local = URL.valueOf(url.toFullString())
.setProtocol(Constants.LOCAL_PROTOCOL)
.setHost(LOCALHOST)
.setPort(0);
ServiceClassHolder.getInstance().pushServiceClass(getServiceClass(ref));
//创建Invoker对象
//根据协议头,这里的 protocol 会在运行时调用 InjvmProtocol 的 export 方法
Exporter> exporter = protocol.export(
proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
exporters.add(exporter);
}
}
如上代码,当调用protocol.export
的时候,Dubbo SPI 自适应的特性的好处就出来了,可以自动根据 URL 参数,获得对应的拓展实现。比如这里是本地暴露,那么它就会调用到InjvmProtocol
。InjvmProtocol
的 export 方法仅创建了一个 InjvmExporter,无其他逻辑。至此,本地暴露完毕。
public Exporter export(Invoker invoker) throws RpcException {
return new InjvmExporter(invoker, invoker.getUrl().getServiceKey(), exporterMap);
}
3.3、远程暴露
远程暴露也一样,如果scope != local
则调用方法。值得注意的是,它的方法是在注册中心列表循环里面调用的。
if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) {
if (registryURLs != null && !registryURLs.isEmpty()) {
//循环注册中心
for (URL registryURL : registryURLs) {
//省略无关代码...
//先获取invoker
Invoker> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass,
registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
//将invoker和this对象封装成DelegateProviderMetaDataInvoker
DelegateProviderMetaDataInvoker wrapperInvoker = new
DelegateProviderMetaDataInvoker(invoker, this);
//服务暴露
Exporter> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
}
}
先注意这句代码:
registryURL.addParameterAndEncoded("export",url.toFullString())
它把当前的url信息,添加到registryURL
对象中去,key为export。
然后当获取到invoker
后,此时的url协议头会变成registry
此时,下面再调用protocol.export
就会调用到RegistryProtocol.export()
下面我们把目光移动到 RegistryProtocol 的 export 方法上。
public Exporter export(final Invoker originInvoker) throws RpcException {
//暴露服务
final ExporterChangeableWrapper exporter = doLocalExport(originInvoker);
// 获取注册中心 URL,以 zookeeper 注册中心为例,得到的示例 URL 如下:
//zookeeper://192.168.139.131:2181/com.alibaba.dubbo.registry.RegistryService...
URL registryUrl = getRegistryUrl(originInvoker);
//根据 URL 加载 Registry 实现类,比如 ZookeeperRegistry
final Registry registry = getRegistry(originInvoker);
// 获取已注册的服务提供者 URL,比如:
//dubbo://192.168.100.74:20880/com.viewscenes.netsupervisor.service.InfoUserService...
final URL registedProviderUrl = getRegistedProviderUrl(originInvoker);
boolean register = registedProviderUrl.getParameter("register", true);
ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registedProviderUrl);
if (register) {
// 向注册中心注册服务
register(registryUrl, registedProviderUrl);
ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
}
// 获取订阅 URL,比如:
//provider://192.168.100.74:20880/com.viewscenes.netsupervisor.service.InfoUserService...
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registedProviderUrl);
// 创建监听器
OverrideListener overrideSubscribeListener =
new OverrideListener(overrideSubscribeUrl, originInvoker);
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
// 向注册中心进行订阅 override 数据
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
// 创建并返回 DestroyableExporter
return new
DestroyableExporter(exporter, originInvoker, overrideSubscribeUrl, registedProviderUrl);
}
如上代码,它主要完成了两件事:
调用doLocalExport
进行服务暴露、向注册中心注册服务