1. 服务导出的入口方法
通过在服务实现类上面加上@Service
注解,Dubbo可以扫描并生成两个bean对象:Spring中的bean 和 ServiceBean,然后把服务导出到zookeeper、redis等注册中心!
@Service(version = "async")
public class AsyncDemoService implements DemoService {
@Override
public String sayHello(String name) {
System.out.println("sayhello方法 " + name);
return name;
}
上一篇文章中通过源码,我们明白了 ServiceBean的生成过程, 那么Dubbo是如何进行服务导出的呢?ServiceBean
由于实现了ApplicationListener<ContextRefreshedEvent>
,发布了一个容器刷新事件,意味着在spring容器启动完毕后,会调用onApplicationEvent
方法,在方法内部调用export()
向注册中心暴露服务!源码如下:
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 当前服务没有被导出并且没有卸载,才导出服务
if (!isExported() && !isUnexported()) {
if (logger.isInfoEnabled()) {
logger.info("The service ready on spring started. service: " + getInterface());
}
// 服务导出(服务注册)
export();
}
}
2. 服务导出原理
服务导出:主要是指 服务提供者向注册中心上提供服务的过程
Dubbo的服务导出主要做以下几件事情
- 根据配置方式的优先级,刷新Dubbo配置参数
- 确定协议,生成URL
- 根据服务的参数信息,启动对应的网络服务器(netty、tomcat、jetty等),用来接收网络请求
- 将服务的信息注册到注册中心
- 启动监听器,监听动态配置修改
2.1 刷新配置参数
一个Dubbo服务的配置参数有多种,比如version、group、delay、weight
等等,这些配置参数的配置方式也有多种,而Dubbo选择配置方式的优先级从高到低如图所示,高优先级的配置覆盖低优先级的配置
Dubbo使用一个LinkedList集合
来保存各种配置方式的配置信息,因为LinkedList
是有序集合,方便后续对配置按优先级顺序进行覆盖和更新,源码如下所示:
public CompositeConfiguration getConfiguration(String prefix, String id) {
// 存储所有配置方式的集合:compositeConfiguration = Linklist
// 按添加顺序决定优先级
CompositeConfiguration compositeConfiguration = new CompositeConfiguration();
// JVM环境变量配置
compositeConfiguration.addConfiguration(this.getSystemConfig(prefix, id));
// 操作系统环境变量配置
compositeConfiguration.addConfiguration(this.getEnvironmentConfig(prefix, id));
// 配置中心APP配置
compositeConfiguration.addConfiguration(this.getAppExternalConfig(prefix, id));
// 配置中心Global配置
compositeConfiguration.addConfiguration(this.getExternalConfig(prefix, id));
// dubbo.properties中的配置
compositeConfiguration.addConfiguration(this.getPropertiesConfig(prefix, id));
return compositeConfiguration;
}
有发现上面源码中缺失了@Service()
中的配置了吗?Dubbo对@Service()
上的配置优先级进行了特殊处理
// isConfigCenterFirst()默认是true,
if (Environment.getInstance().isConfigCenterFirst()) {
//把@Service上的配置 放入集合中第五位,优先级位于Dubbo控制台配置之后
compositeConfiguration.addConfiguration(4, config);
} else {
//把@Service上的配置 放入集合中第三位,优先级位于Dubbo控制台配置之前
compositeConfiguration.addConfiguration(2, config);
}
我们可以自定义isConfigCenterFirst
的属性,决定@Service()
上的配置优先级
最后遍历LinkedList
集合中的元素(配置方式),进行配置信息覆盖和更新
//遍历LinkedList集合,更新或者覆盖配置信息
for (Configuration config : configList) {
try {
if (config.containsKey(key)) {
firstMatchingConfiguration = config;
break;
}
} catch (Exception e) {
logger.error("Error when trying to get value for key " + key + " from " + config + ", will continue to try the next one.");
}
}
if (firstMatchingConfiguration != null) {
return firstMatchingConfiguration.getProperty(key);
} else {
return null;
}
从以上分析我们可以看出,在服务导出之前,首先得确定服务的参数。服务的参数除开来自于服务的自身配置外,还可以来自比自身优先级高的配置。所以在确定服务参数时,需要先从上级获取参数,获取之后,如果服务本身配置了相同的参数,那么则进行覆盖
如果不同优先级之间没有出现冲突,则会互补。比如:@Service
本身没有配置timeout
参数,但是如果服务所属的应用的properties配置文件
配置了timeout
,那么这个应用下的服务都会继承这个timeout
配置。
2.1 确定协议,生成URL
一个服务可以配置多个协议:
# 端口号不同的两个Dubbo协议
dubbo.protocols.p1.name=dubbo
dubbo.protocols.p1.port=20880
dubbo.protocols.p1.host=0.0.0.0
dubbo.protocols.p2.name=http
dubbo.protocols.p2.port=20881
dubbo.protocols.p2.host=0.0.0.0
源码中会遍历所有的协议,每一种协议导出一个单独的服务
// 遍历每个协议
// 一个协议一个服务
for (ProtocolConfig protocolConfig : protocols) {
// path表示服务名
// contextPath表示应用名(可配置)
// pathKey = group/contextpath/path:version
// 例子:myGroup/user/org.apache.dubbo.demo.DemoService:1.0.1
String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
// ProviderModel中存在服务提供者访问路径,实现类,接口,以及接口中的各个方法对应的ProviderMethodModel
// ProviderMethodModel表示某一个方法,方法名,所属的服务的,
ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);
// ApplicationModel表示应用中有哪些服务提供者和引用了哪些服务
ApplicationModel.initProviderModel(pathKey, providerModel);
// 每种协议导出一个单独的服务,注册到各个注册中心
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
有了确定的协议,服务名,服务参数后,自然就可以组装成服务的URL了,在doExportUrlsFor1Protocol()
方法中组装服务URL
有了准确的服务URL之后,就可以把URL注册到注册中心上去了,在注册之前,还需要获得注册中心的URL
注意:上面所说的服务URL
或者注册中心URL
,并不是浏览器上的url连接地址,而是一个名为URL的类,类内部封装了端口号、ip、协议等注册信息,如下所示
class URL implements Serializable {
private static final long serialVersionUID = -1985165475234910535L;
private final String protocol;
private final String username;
private final String password;
private final String host;
private final int port;
private final String path;
//上面不够的,由parameters参数做更详细的补充!
private final Map<String, String> parameters;
2.3 启动对应协议的服务器
Dubbo在与其他组件交互的时候,会使用在服务URL中指定的协议,根据不同的协议启动不同的服务器,比如:
Http
协议就启动Tomcat
、Jetty
。Dubbo
协议就启动Netty
。
不能只启动Server
,还需要绑定一个RequestHandler
,用来处理请求。
比如,Http协议对应的就是InternalHandler
。Dubbo协议对应的就是ExchangeHandler
。
源码中方法如下:
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) {
String key = getCacheKey(originInvoker);
return (ExporterChangeableWrapper<T>) bounds.computeIfAbsent(key, s -> {
Invoker<?> invokerDelegate = new InvokerDelegate<>(originInvoker, providerUrl);
// protocol属性的值是哪来的,是在SPI中注入进来的,是一个代理类
// 这里实际利用的就是DubboProtocol或HttpProtocol去export NettyServer
// 为什么需要ExporterChangeableWrapper?方便注销已经被导出的服务
return new ExporterChangeableWrapper<>((Exporter<T>) protocol.export(invokerDelegate), originInvoker);
});
}
//如果是Dubbo协议最终会调用openServer方法 ,启动服务器
// 开启NettyServer
openServer(url);
2.4 向注册中心注册服务
有了服务URL 和 注册中心的地址的URL,就可以向zookeeper注册服务,注册失败的话会进行重试!
// 注册服务,把简化后的服务提供者url注册到registryUrl中去,失败会重试
// 通过zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true));
register(registryUrl, registeredProviderUrl);
// zk的注册服务方法
@Override
public void doRegister(URL url) {
try {
//使用zk客户端创建节点,注册服务
zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true));
} catch (Throwable e) {
throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
如上2.1中的配置,可以在zookeeper中生成两个服务
以dubbo协议的url为例,把dubbo协议的url通过编解码工具,解码后得到:
dubbo://192.168.100.1:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-provider1-application&bean.name=ServiceBean:org.apache.dubbo.demo.DemoService&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&logger=log4j&methods=sayHello&pid=12236&release=2.7.0&side=provider×tamp=1614435347611&token=tulingtoken
可以看到注册到zookeeper上的服务其实就是这个服务配置的全部信息,其中token
的主要作用是:调用服务时,为了分辨是否从zookeeper上拉取的服务!
2.6 监听动态配置修改
服务在导出的过程中需要向动态配置中心的数据进行订阅,以便当管理人员修改了动态配置中心中对应服务的参数后,服务提供者能及时做出变化。
服Dubbo如果使用的是zookeeper
作为配置中心,那么服务配置就存储在zookeeper
节点上,就需要利用Zookeeper
的Watcher
机制,监听的是节点变化。所以在一个服务进行导出时,需要在服务提供者端给当前服务生成一个对应的监听器实例,这个监听器实例为OverrideListener
,它负责监听对应服务的动态配置变化,并且根据动态配置中心的参数重写服务URL。
// 一个overrideSubscribeUrl对应一个OverrideListener,用来监听变化事件,监听到overrideSubscribeUrl的变化后,
// OverrideListener就会根据变化进行相应处理,具体处理逻辑看OverrideListener的实现
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
当动态配置中心修改了某个服务的配置后,就会触发OverrideListener
中的notify
对注册中心的URL
进行重写覆盖,相当于重新发布服务,实现实时发布服务
@Override
public synchronized void notify(List<URL> urls) {
logger.debug("original override urls: " + urls);
List<URL> matchedUrls = getMatchedUrls(urls, subscribeUrl.addParameter(CATEGORY_KEY,
CONFIGURATORS_CATEGORY));
logger.debug("subscribe url: " + subscribeUrl + ", override urls: " + matchedUrls);
// No matching results
if (matchedUrls.isEmpty()) {
return;
}
// 对发生了变化的url进行过滤,只取url是override协议,或者参数category等于configurators的url
this.configurators = Configurator.toConfigurators(classifyUrls(matchedUrls, UrlUtils::isConfigurator))
.orElse(configurators);
// 根据最新配置进行URL重写,并重新发布服务!
doOverrideIfNecessary();
}