dubbo服务暴露与注册
配置解析
本文Dubbo源码版本为2.8.4。
我们先从dubbo的配置讲起,主要是Spring的原理:
我们一般通过XML或者annotation的方式,对dubbo进行配置。我们下面来讲一下XML的配置解析:
上面这要用到了Spring的自定义标签功能,这个配置文件中,主要定义了一个dubbo的命名空间,编写了对应的xsd文档,用于约束 XML 配置时候的标签和对应的属性。这个xsd文档在dubbo jar包中META-INF/dubbo.xsd。
在xsd文档中,我们可以看到很多的标签,但是我们主要关注service和reference标签:
解析这些标签的时候,会去找dubbo jar包下的META-INF/spring.handlers和spring.schema:
spring.schema指明了约束文件的位置,而spring.handlers则是指明了解析约束文件中标签的处理类。
下面我们来看一下这个处理类:
public class DubboNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
}
}
这个类就是将标签和对应的解析类关联起来,解析的时候,就知道用对应的解析类去解析。
里面主要是拿到xml中的配置信息,然后生成spring中的BeanDefinition。BeanDefinition就是对Bean的一个抽象,主要保存了类名、scope、属性、构造函数参数列表、依赖的bean、是否是懒加载等等,后面对Bean的操作就直接对BeanDefinition进行操作,如后面服务暴露中对是否远程暴露的判断就是对scope进行判断就好了。
大致流程图
我们先来看一下大致的流程图,这样在脑中有个大致的路线。
首先,入口是ServiceBean类,通过其中的export()方法开始进入ServiceConfig类的export()方法,执行暴露服务之前的一些逻辑判断,如是否配置延迟发布。接着export()调用了DoExport()方法,doExportUrls()开始正式暴露服务。
doExportUrls()方法首先获取了注册中心路径。然后开始进入doExportUrlsFor1Protocol()做服务暴露与注册。
doExportUrlsFor1Protocol()方法中,其中关键代码为对scope的判断,如果配置为none则表示不做暴露,直接结束;如果配置为local,表示只做本地暴露;如果配置为remote,则表示只做远程暴露;如果没有配置,则表示既做本地暴露也做远程暴露。
其中的细节,后面一一道来。
服务暴露
从上面所述可知,服务暴露主要分为本地暴露和远程暴露,远程暴露中又包括了远程服务暴露和服务注册两个过程。
下面我们来进行源码的分析:
/**
这段代码主要是服务暴露的入口,有配置delay的话,通过afterPropertiesSet()开始export(),
否则Spring容器初始化完成后,通过onApplicationEvent开始export()
**/
public class ServiceBean<T> extends ServiceConfig<T>{
public void onApplicationEvent(ApplicationEvent event) {
if (ContextRefreshedEvent.class.getName().equals(event.getClass().getName())) {
if (isDelay() && ! isExported() && ! isUnexported()) {
if (logger.isInfoEnabled()) {
logger.info("The service ready on spring started. service: " + getInterface());
}
export();
}
}
}
private boolean isDelay() {
Integer delay = getDelay();
ProviderConfig provider = getProvider();
if (delay == null && provider != null) {
delay = provider.getDelay();
}
return supportedApplicationListener && (delay == null || delay.intValue() == -1);
}
public void afterPropertiesSet() throws Exception {
...
if (! isDelay()) {
export();
}
}
}
首先是入口的ServiceBean类,这里afterPropertiesSet和onApplicationEvent两个方法中都有export(),但只会执行其中的一个,配置了延迟发布即delay,则会走afterPropertiesSet中的,否则走onApplicationEvent中的。
afterPropertiesSet中,是Spring容器初始化X秒(即你配置的delay是多长时间),进行服务暴露。而onApplicationEvent则是等Spring容器初始化完成后,进行服务暴露。
关于延迟发布。可以看我这篇文章:Dubbo优雅上下线详解
接下来进入到ServiceConfig类中:
//这段代码中开始执行接口暴露逻辑
public class ServiceConfig<T> extends AbstractServiceConfig{
//这段代码是上面export()进入的,还是做是否有配置delay的执行逻辑
public synchronized void export() {
...
if (delay != null && delay > 0) {
Thread thread = new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(delay);
} catch (Throwable e) {
}
doExport();
}
});
thread.setDaemon(true);
thread.setName("DelayExportServiceThread");
thread.start();
} else {
doExport();
}
}
protected synchronized void doExport() {
//检查配置
...
//开始做暴露
doExportUrls();
}
}
上面代码,主要还是解决是否配置延迟发布的问题。
接下来,还是ServiceConfig类中,进入doExportUrls()方法,这里开始,拉开了服务暴露的序幕,前面这些都是前期准备。
public class ServiceConfig<T> extends AbstractServiceConfig{
private void doExportUrls() {
//获取注册中心,可以通过返回值发现,可以有多个注册中心
List<URL> registryURLs = loadRegistries(true);
//遍历协议,每个协议都要向注册中心注册
for (ProtocolConfig protocolConfig : protocols) {
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
}
从上面代码可知:
-
dubbo支持多注册中心,loadRegistries()这个方法,主要是通过配置组装成注册中心URL。
组装成的URL可以看一下: registry://192.168.6.55:2181/com.alibaba.dubbo.registry.RegistryService?application=poseidon&backup=192.168.6.56:2181,192.168.6.57:2181&dubbo=2.8.4&group=dubbo&pid=10384®ister=true®istry=zookeeper&subscribe=true×tamp=1626684876194
-
dubbo也支持多协议,如果一个服务有多个协议的话,则都需要向注册中心暴露注册。
接下来,进入doExportUrlsFor1Protocol(),因为这个方法太长了,只取其中关键的代码:
public class ServiceConfig<T> extends AbstractServiceConfig{
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
//前面是做一些host和port的获取;新建一个map,存储配置,通过这个map构建出URL
...
String scope = url.getParameter(Constants.SCOPE_KEY);
//配置为none不暴露
if (! Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {
//配置不是remote的情况下做本地暴露 (配置为remote,则表示只暴露远程服务)
if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
exportLocal(url);
}
//如果配置不是local则暴露为远程服务.(配置为local,则表示只暴露远程服务)
if (! Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope) ){
if (logger.isInfoEnabled()) {
logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
}
//如果有注册中心,向注册中心注册服务
if (registryURLs != null && registryURLs.size() > 0
&& url.getParameter("register", true)) {
for (URL registryURL : registryURLs) {
url = url.addParameterIfAbsent("dynamic", registryURL.getParameter("dynamic"));
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);
}
//转换成Invoker类型,用于向注册中心注册
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
//注册完成之后,返回为exporter
Exporter<?> exporter = protocol.export(invoker);
exporters.add(exporter);
}
} else {
//这段代码也是做暴露服务,不过是直接暴露,没有向注册中心注册
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
Exporter<?> exporter = protocol.export(invoker);
exporters.add(exporter);
}
}
}
...
}
}
总结一下流程图:
上述代码,主要就是前面所提的,对scope配置的判断。为none,则表示不做暴露;为remote,只做远程暴露;为local,只做本地暴露;没有配置,则表示既做远程暴露,也做本地暴露。
其中,如果没有注册中心的话,也会做服务暴露,大家都知道,dubbo客户端访问接口是可以不通过注册中心直接访问接口的。
有注册中心的时候,中间主要先通过对象转换成invoker,再注册到注册中心。返回的时候,对象转换成了exporter。
我们看一下dubbo官网提供的对象转换的大致流程图:
我们看一下Exporter和Invoker里面有什么?
接着,我们来看一下ProxyFactory的生成。
private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
这里主要是用到了Dubbo的SPI(Service Provider Interface)机制。
SPI机制
我们来看一下官网给出的简介:
SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。
SPI机制在Dubbo中大量地使用。
我们在项目中,经常会访问数据库,而我们访问数据库的接口就是用java.sql.Driver
接口。
市面上的数据库有非常多种,不同的数据库其底层的实现不同,这时候就需要一个接口,来统一一下访问数据库的方式。让使用者访问数据库的时候只要面向接口编程就可以了。
数据库厂商们会根据这个接口来提供自己的实现,而使用的时候,怎么才知道到底用哪个实现呢?
这时候JAVA SPI机制就派上用场了,它约定在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名。
我们在使用这个jar包的时候,就会去这个jar包下面的META-INF/services/目录,再根据接口名找到文件,然后读取文件里面的内容去进行实现类的加载与实例化。
但是JAVA SPI有个缺点,他会遍历SPI的配置文件,将实现类全部实例化,如果有类用不到的话,有可能会产生资源的浪费。
所以,dubbo自己实现了SPI,实现了按需加载。通过类的名字去文件里面找到对应的实现类全限定名然后加载实例化即可(配置文件中存储的是键值对)。原理的话,这边简单介绍一下,就是先会拿接口中类的名字去存储实例的缓存中看一下有没有这个接口的实现类,有的话直接获取,没有的话通过反射机制新建一个。
这里给个每个协议所对应的实现类。调用某接口时,如果处于该协议的状态下,会去该协议所对应的类下面找对应的接口实现的方法。
由于本文主要讲的是服务暴露,有兴趣可以看以下文章:
本地暴露
从dubbo的2.2.0版本开始,每个服务默认都会在本地暴露。在引用服务的时候,默认优先引用本地服务。如果希望引用远程服务可以使用一下配置强制引用远程服务。
<dubbo:reference ... scope="remote" />
接下来,我们来看一下是如何做本地暴露的:
public class ServiceConfig<T> extends AbstractServiceConfig{
//本地服务暴露,用于本地的调用,避免了网络通信
private void exportLocal(URL url) {
if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
URL local = URL.valueOf(url.toFullString())
.setProtocol(Constants.LOCAL_PROTOCOL)
.setHost(NetUtils.LOCALHOST)
.setPort(0);
// modified by lishen
ServiceClassHolder.getInstance().pushServiceClass(getServiceClass(ref));
Exporter<?> exporter = protocol.export(
proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
exporters.add(exporter);
logger.info("Export dubbo service " + interfaceClass.getName() +" to local registry");
}
}
}
本地服务暴露,用的是injvm协议,可以看到,上面的代码中,在url中重新设置protocol的值。
我们看一下protocol.export()这个方法。
我们看一下export接口的实现,这么多实现类,怎么才能确定是哪个方法呢?
这边其实用的就是Dubbo SPI机制。
这个export方法可以看到,有个@Adaptive注解,通过这个注解会生成代理类,然后代理类会根据 Invoker 里面的 URL 参数得知具体的协议,然后通过 Dubbo SPI 机制选择对应的实现类进行 export,而这个方法就会调用 InjvmProtocol中的export 方法。
本地暴露作用
可能存在同一个JVM调用自身的服务的情况,开启一个本地的服务暴露,可以在调用的时候,避免了网络通信,加快了调用的速度。
远程暴露
public class RegistryProtocol implements Protocol{
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
//export invoker
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);
//根据URL加载Registry的实现类
final Registry registry = getRegistry(originInvoker);
//获取注册中心的URL
final URL registedProviderUrl = getRegistedProviderUrl(originInvoker);
//服务注册
registry.register(registedProviderUrl);
// 订阅override数据
// FIXME 提供者订阅时,会影响同一JVM即暴露服务,又引用同一服务的的场景,因为subscribed以服务名为缓存的key,导致订阅信息覆盖。
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registedProviderUrl);
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl);
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
//保证每次export都返回一个新的exporter实例
return new Exporter<T>() {
...
};
}
}
服务暴露:
进入到doLocalExport:
//做服务暴露
private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker){
String key = getCacheKey(originInvoker);
ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
// DCL双重检查锁,因为有各种缓存
if (exporter == null) {
synchronized (bounds) {
exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
//还没做服务暴露
if (exporter == null) {
//invoker中包含着URL,得到URL,URL中的 dubbo://
final Invoker<?> invokerDelegete = new InvokerDelegete<T>(originInvoker, getProviderUrl(originInvoker));
//调用dubboProtocol的export
exporter = new ExporterChangeableWrapper<T>((Exporter<T>)protocol.export(invokerDelegete), originInvoker);
bounds.put(key, exporter);
}
}
}
return (ExporterChangeableWrapper<T>) exporter;
}
接下来,通过doLocalExport进入到:
public class DubboProtocol extends AbstractProtocol {
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
URL url = invoker.getUrl();
// export service.
String key = serviceKey(url);
DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
exporterMap.put(key, exporter);
//export an stub service for dispaching event
...
//打开server
openServer(url);
// modified by lishen
optimizeSerialization(url);
return exporter;
}
private void openServer(URL url) {
// find server. 获取IP地址
String key = url.getAddress();
//client 也可以暴露一个只有server可以调用的服务。
boolean isServer = url.getParameter(Constants.IS_SERVER_KEY,true);
if (isServer) {
//获取服务
ExchangeServer server = serverMap.get(key);
//如果是第一次做服务暴露
if (server == null) {
//创建server,往serverMap中放入这个服务提供url
serverMap.put(key, createServer(url));
} else {
//server支持reset,配合override功能使用 如果有了,就重置一下
server.reset(url);
}
}
}
private ExchangeServer createServer(URL url) {
//前面是开启一些服务,往url中加一些东西
...
ExchangeServer server;
try {
//开启nettyServer,来进行监听
server = Exchangers.bind(url, requestHandler);
} catch (RemotingException e) {
throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
}
...
return server;
}
}
到此,服务暴露就完成了。主要通过Netty完成的,bind()绑定该端口号, netty服务端将监听该端口号, 接收客户端请求。
服务注册
public abstract class FailbackRegistry extends AbstractRegistry {
public void register(URL url) {
super.register(url);
//将该URL注册失败列表中去除
failedRegistered.remove(url);
failedUnregistered.remove(url);
try {
// 向服务器端发送注册请求
doRegister(url);
} catch (Exception e) {
...
// 将失败的注册请求记录到失败列表,定时重试
failedRegistered.add(url);
}
}
}
ZK注册实现:
//该类位于ZookeeperRegistry
protected void doRegister(URL url) {
try {
zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true));
} catch (Throwable e) {
throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
//该类位于AbstractZookeeperClient
public void create(String path, boolean ephemeral) {
int i = path.lastIndexOf('/');
if (i > 0) {
//做递归。因为zookeeper建立节点的时候,只能一级一级的建立,所以每次都是取"/"前面的一部分来创建
//由于zookeeper规定,除了叶子节点外,其余节点都必须为非临时节点,所以这点传的第二个参数为FALSE
create(path.substring(0, i), false);
}
//如果传入的ephemeral=TRUE,即是临时节点
if (ephemeral) {
//创建临时节点
createEphemeral(path);
} else {
//创建持久节点
createPersistent(path);
}
}
这里的服务注册是通过zookeeper实现的。
远程暴露流程图
总结
本文通过流程图以及源码的方式解析了Dubbo服务暴露的流程,Dubbo服务暴露主要有两个过程:服务暴露和服务注册;服务暴露默认通过Netty Server进行,服务注册通过Zookeeper进行。在其整个流程中,都离不开Dubbo SPI这个机制,通过这个机制,Dubbo才知道需要调用哪个方法。希望这篇文章能帮助到大家更好地了解Dubbo。