Dubbo流程及源码分析(二)

        扑街前言:上篇文章说了关于dubbo和Java的SPI机制,本次说下关于Spring对于dubbo的一个集成,从spring的集成出发分析整个dubbo的启动流程。


目录

Spring的Schema扩展机制

Dubbo架构图

ServiceBean

ServiceConfig

Protocol

服务注册


Spring的Schema扩展机制

        在了解dubbo的服务注册和服务发现之前,我们首先需要掌握Spring是如何集成dubbo的。

        我们可以先了解一下Spring的Schema 扩展机制,Spring 约定在META-INF文件夹下的spring.schemas文件和spring.handlers文件就是实现Spring Schema的关键。spring.schemas文件中指向的是xml文件中的合法构建模块,也就是以“.xsd”结尾的文件。spring.handlers文件中指向的是xml文件中的标签解析对象的指向。也就说如果我们自己需要创建一个标签的话,就需要这个两个文件来指向对应的具体文件和对象。

        至于指向的文件和对象,我们来看下dubbo是怎么实现的。打开dubbo源码首先要找到的是dubbo-config项目下的spring项目,然后找到spring.schemas文件和spring.handlers文件,前者的内容就是指向DubboNamespaceHandler 对象,用于解析xml中标签的内容,后者就是指向xml文件中的合法构建模块dubbo.xsd。(可参考下面图片)

        上述的dubbo.xsd文件我们就不做解析了,重点分析一下DubboNamespaceHandler 对象,先上代码。

        代码内容非常的直观,可以清晰的看到所有dubbo标签的解析,我们重点先看service标签和reference标签,前者是服务暴露,后者是服务获取,这两个其实就能看出dubbo服务端和客户端的流程,下面我们一个一个分析。

/**
 * DubboNamespaceHandler
 *
 * @export
 */
public class DubboNamespaceHandler extends NamespaceHandlerSupport {

    static {
        Version.checkDuplicate(DubboNamespaceHandler.class);
    }

    @Override
    public void init() {
        /**
         * 解析配置文件中<dubbo:xxx></dubbo:xxx> 相关的配置,并向容器中注册bean信息
         */
        registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
        registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
        registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
        registerBeanDefinitionParser("config-center", new DubboBeanDefinitionParser(ConfigCenterBean.class, true));
        registerBeanDefinitionParser("metadata-report", new DubboBeanDefinitionParser(MetadataReportConfig.class, true));
        registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
        registerBeanDefinitionParser("metrics", new DubboBeanDefinitionParser(MetricsConfig.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 AnnotationBeanDefinitionParser());
    }

}

Dubbo架构图

        在源码解析正式开始之前,还有一个事,先上一幅图,Dubbo架构图,这个图是官网文档中的,地址是:框架设计 | Apache Dubbo,这个图很重要,看不懂的话,源码一定歇菜。这个图我就不做解析了,我自己也无法详细说明。


ServiceBean

        这里开始就是正式的源码分析了,从上述的代码中可以看到,dubbo的service 标签解析出来的对象就是ServiceBean 对象,这个对象也是有多个接口实现,我们重点要看的就是ApplicationListener<ContextRefreshedEvent> 接口,对于spring 容器的监听事件,这里就以onApplicationEvent 方法为起始点来分析provider 提供者的启动流程。因为dubbo的源码比较复杂,我们就目前就只关注主流程,具体的详细可以看下我上传的dubbo 源码解析资源,里面有详细的dubbo源码注释。

/**
 * dubbo服务导出入口,
 * 当容器初始化完成之后,需要处理一些操作,比如一些数据的加载、初始化缓存、特定任务的注册等等。
 * 这个时候我们就可以使用Spring提供的ApplicationListener来进行操作
 * @param event
 */
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
	if (!isExported() && !isUnexported()) {
		if (logger.isInfoEnabled()) {
			logger.info("The service ready on spring started. service: " + getInterface());
		}
		/**
		 * 导出服务
		 */
		export();
	}
}

ServiceConfig

        从上面的onApplicationEvent 方法,我们可以一步一步跟到export() 方法,这里我们就到了ServiceConfig 对象里面,从结构图我们也可以看到服务端这边从service层下来,config层的开始就是从ServiceConfig,然后继续是doExport() 方法,然后是doExportUrls() 方法,这里我们看下源码。这里再提一点,ServiceConfig 是ServiceBean父类对象。

        首先是加在配置文件中的所有注册中心配置,dubbo 本身是支持多多协议服务的,所有这里会将所有的协议都注册一遍监听,不用纠结很详细的代码,我们debug直接看参数,这里其实就能看到registryURLs 对象里面存储的就是服务端xml配置的注册中心,我这里只配置了一个,所以这里URL里面就只有一个。

@SuppressWarnings({"unchecked", "rawtypes"})
/**
 * dubbo服务导出核心
 */
private void doExportUrls() {
	//加载配置文件中的所有注册中心配置,并且封装为dubbo内部的URL对象列表
	List<URL> registryURLs = loadRegistries(true);
	//循环所有协议配置,根据不同的协议,向注册中心中发起注册 dubbo provider可能提供多种协议服务,默认dubbo协议,还有其他的比如Redis,Thrift等
	for (ProtocolConfig protocolConfig : protocols) {
		String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
		ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);
		ApplicationModel.initProviderModel(pathKey, providerModel);
		//核心
		doExportUrlsFor1Protocol(protocolConfig, registryURLs);
	}
}

        我们继续看核心doExportUrlsFor1Protocol 方法的调用,这里有传入两个参数,protocolConfig 远程调用层protocol 层的协议,还有就是dubbo的重要对象URL,为什么说URL是dubbo的重要呢,之前我们分析dubbo 的SPI 机制的时候就了解到,dubbo 的扩展点是动态选择的,而选择的条件就是URL对象带来的。继续看代码,当跟到doExportUrlsFor1Protocol 方法之后,前面有一大段内容,这个不用太在意,其目的就是为了组装URL对象。

        然后我们可以将断点放在642行(这里是我的注释版代码的行数),获取Invoker,这里我们又可以结合架构图看,在config层的下一层就是proxy层服务代理层,在这里我们调用getInvoker,获取到封装的Invoker 对象,这个代码中需要注意的是PROXY_FACTORY 常量对象本身就是一个ProxyFactory 扩展点的自适应代理,这个会根据URL对象中的参数,自适应使用扩展点对应的实现,代码继续就还是对于获取到的Invoker 对象进行wrapper 增强封装。我们可以看下目前这两个对象的值。

        然后看代码,当获取到Invoker 对象之后,直接就使用protocol 常量(这个就是Protocol 扩展点的自适应代理)调用export 方法,结合架构图来看,我们是没有进过注册中心层、路由层(当然路由层服务端是不需要的)、监控层(这个我们一般无法体会到,这个基本上都是dubbo自身监控),因为dubbo的对于服务端是先启动后注册,所以服务端的Invoker 对象直接从proxy层到了protocol层。

/**
 * A {@link ProxyFactory} implementation that will generate a exported service proxy,the JavassistProxyFactory is its
 * default implementation
 */
private static final ProxyFactory PROXY_FACTORY = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();


// 为服务提供类(ref)生成 Invoker
/**
 * Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,
 * 它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现
 *
 * 在服务提供方,Invoker用于调用服务提供类。在服务消费方,Invoker用于执行远程调用
 *
 * Invoker 是由 ProxyFactory 创建而来,Dubbo 默认的 ProxyFactory 实现类是 JavassistProxyFactory
 */
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
// DelegateProviderMetaDataInvoker 用于持有 Invoker 和 ServiceConfig
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

/**
 * 导出服务,并生成 Exporter 与导出服务到本地相比,导出服务到远程的过程要复杂不少,其包含了服务导出与服务注册两个过程
 *
 * debug此处查看应该走哪个protocol的export
 * 走的是 RegistryProtocol
 */
Exporter<?> exporter = protocol.export(wrapperInvoker);

Protocol

        上述代码中我们可以知道export 方法的入参是wrapperInvoker,那么我么就可以在wrapperInvoker 中的URL对象中找到我的远程调用层协议是registry,那么在Protocol 扩展点的实现就应该是RegistryProtocol 对象。

 

         RegistryProtocol 对象的export 方法,我们又可以看到一大段的代码,我们这边只关注主流程,那么只关注两个代码段getSubscribedOverrideUrl(providerUrl) 和register(registryUrl, registeredProviderUrl),前者是执行服务导出,后者是想注册中心注册服务。

        我们先看getSubscribedOverrideUrl(providerUrl) 的具体代码。这块会有很多的套娃,所以我可能描述不太清楚,具体还是要自己跟一遍代码。继续说下面的代码,首先我们需要关注的点是protocol.export(invokerDelegate) 这里任然是protocol 扩展点的调用,注意我们入参的URL对象的参数不再是registry 了,而是dubbo,所以这个调用的实现就是DubboProtocol 的export 方法。

@SuppressWarnings("unchecked")
private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) {
	// dubbo://192.168.200.10:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=demo-provider&bean.name=org.apache.dubbo.demo.DemoService&bind.ip=192.168.200.10&bind.port=20880&deprecated=false&dubbo=2.0.2&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=15804&qos.port=22222&register=true&release=&side=provider&timestamp=1622539267498
	String key = getCacheKey(originInvoker);//访问缓存

	return (ExporterChangeableWrapper<T>) bounds.computeIfAbsent(key, s -> {
		//创建 Invoker 委托类对象 Delegate
		Invoker<?> invokerDelegate = new InvokerDelegate<>(originInvoker, providerUrl);
		//----****重点跟 protocol.export(invokerDelegate) 方法,此处protocol根据SPI机制,根据URL中的参数找 DubboProtocol 实现 即根据协议导出
		return new ExporterChangeableWrapper<>((Exporter<T>) protocol.export(invokerDelegate), originInvoker);
	});
}

        DubboProtocol 的export 方法中我们重点关注的是openServer(url) 方法的调用内容, openServer 方法中也是一层套娃,我们继续跟到createServer(url) 方法,这里就可以看到绑定并启动服务的调用了,然后继续可以跟到ExchangeServer 扩展点调用,结合上面的结构图看Exchange 信息交换层,用于请求响应模式,需要注意的是跟进扩展点后,可以发现URL中是没有关于这个扩展点实现的配置,那么我们选择的就是默认配置,可以看到@SPI 注解对应的默认扩展点实现是HeaderExchanger,然后继续跟进可以看到bind 方法主要是封装HeaderExchangeServer,还可以看到Transporters 的bind 方法,这里就是可以跟到下一层transport 网络传输层,然后一层一层往下跟,最后我们可以跟到netty 的AbstractServer 方法,然后就是doOpen() 开启netty服务。

 

private void openServer(URL url) {
	// find server. key=192.168.200.10:20880
	String key = url.getAddress();
	//client can export a service which's only for server to invoke
	boolean isServer = url.getParameter(IS_SERVER_KEY, true);
	if (isServer) {
		ExchangeServer server = serverMap.get(key); // serverMap根据ip:port缓存Server对象      因为服务端可能在本机不同端口暴露
		if (server == null) {
			synchronized (this) {
				server = serverMap.get(key);
				if (server == null) {
					serverMap.put(key, createServer(url)); // createServer是核心----****重点去关注******
				}
			}
		} else {
			// server supports reset, use together with override
			server.reset(url);
		}
	}
}

        netty 的服务启动其实我之前的文章描述得差不多(手写rpc和netty详解都可以看到),下面看下代码。这个要说讲吧,好像也没有难点了,主要就是主从reactor多线程模型,这部分可以在我的初始netty(一)文章中看到详细的描述,还可以在zookeeper(一)文章看到zookeeper 对于reactor 模型做的封装,下面的代码真的没什么好说的,就这样吧。

/**
 * 启动netty服务
 */
protected void doOpen() throws Throwable {
	bootstrap = new ServerBootstrap();
	bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("NettyServerBoss", true));
	workerGroup = new NioEventLoopGroup(getUrl().getPositiveParameter(IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS),
			new DefaultThreadFactory("NettyServerWorker", true));
	final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
	channels = nettyServerHandler.getChannels();
	bootstrap.group(bossGroup, workerGroup)
			.channel(NioServerSocketChannel.class)
			.childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
			.childOption(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
			.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
			.childHandler(new ChannelInitializer<NioSocketChannel>() {
				@Override
				protected void initChannel(NioSocketChannel ch) throws Exception {
					// FIXME: should we use getTimeout()?
					int idleTimeout = UrlUtils.getIdleTimeout(getUrl());
					// 初始化编解码器  运行时是 DubboCodec---> ExchangeCodec----> TelnetCodec
					NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
					ch.pipeline()//.addLast("logging",new LoggingHandler(LogLevel.INFO))//for debug
							.addLast("decoder", adapter.getDecoder()) //服务端请求解码器
							.addLast("encoder", adapter.getEncoder()) // 服务端响应编码器
							.addLast("server-idle-handler", new IdleStateHandler(0, 0, idleTimeout, MILLISECONDS))
							//netty服务端业务处理器
							.addLast("handler", nettyServerHandler);
				}
			});
	// bind
	ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
	channelFuture.syncUninterruptibly();
	channel = channelFuture.channel();

}

服务注册

        当netty 启动完成之后,我们想目光回到RegistryProtocol,也就是doLocalExport(originInvoker, providerUrl) 这段代码的结束,从之前的手写rpc文章中可以知道,服务端的启动主要做两件事情,一是netty服务启动,二是服务注册,那么我们现在要看到的是register 方法,服务注册的内容。

        从代码中可以一步一步跟到registryFactory.getRegistry(registryUrl) 的调用,registryFactory又是扩展点,这里我们要跟到的是AbstractRegistryFactory 实现,这里也是一层套娃,我们要找到的是createRegistry(url) 方法,然后继续跟就是根据URL中不同注册中心组建的配置参数,找到对应实现。我这里选择的是zookeeper,那么继续跟就是下面这段代码,跟上面使用netty是一样的,在我之前的文章中就能找到对应的代码说明,这里没什么说的了,服务注册完成之后,在RegistryProtocol 中还需要将服务进行订阅。

public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) { // ZookeeperTransporter
	super(url);
	if (url.isAnyHost()) {
		throw new IllegalStateException("registry address == null");
	}
	// 获取组名,默认为 dubbo
	String group = url.getParameter(GROUP_KEY, DEFAULT_ROOT);
	if (!group.startsWith(PATH_SEPARATOR)) {
		group = PATH_SEPARATOR + group;
	}
	this.root = group;
	// 创建 Zookeeper 客户端,默认为 CuratorZookeeperTransporter
	zkClient = zookeeperTransporter.connect(url);
	zkClient.addStateListener(state -> {
		if (state == StateListener.RECONNECTED) {
			try {
				recover();
			} catch (Exception e) {
				logger.error(e.getMessage(), e);
			}
		}
	});
}

        上述内容就是大体的服务端启动流程,后续文章我们再讨论客户端的启动,还有数据传输流程的过程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值