Dubbo是一个RPC远程调用的框架,对于一个服务提供方,暴露了一个接口给外部消费方调用。如果服务提供方自身也需要调用这个接口会怎么样呢,难道也需要走远程编解码和数据网络传输这套流程吗?
对于Dubbo这么优秀的开源框架,显然是需要支持本地调用的,Dubbo提供了本地调用的InjvmProtocol
协议。今天我们代码演示来详细分析下InjvmProtocol
是的工作原理。
application.xml如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://code.alibabatech.com/schema/dubbohttp://code.alibabatech.com/schema/dubbo/dubbo.xsd"><dubbo:application name="demo-provider"/><dubbo:registry protocol="zookeeper" address="127.0.0.1:2181"/><dubbo:protocol name="dubbo" port="20880"/><!-- 和本地bean一样实现服务 --><bean id="helloWorldServiceImpl" class="cn.gov.zcy.dubbotest.api.HelloWorldServiceImpl"/><!-- 声明需要暴露的服务接口 --><dubbo:service interface="cn.gov.zcy.dubbotest.api.HelloWorldSerivice" ref="helloWorldServiceImpl"/><dubbo:reference id="helloWorldService" interface="cn.gov.zcy.dubbotest.api.HelloWorldSerivice"/></beans>
public class DubboInjvmTest {public static void main(String[] args) throws Exception {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("application.xml");HelloWorldSerivice helloWorldSerivice = context.getBean("helloWorldService", HelloWorldSerivice.class);Response<Optional<String>> returnMessage = helloWorldSerivice.hello("aaa");System.out.println(returnMessage.getResult().get());LockSupport.park();}}
dubbo:service举例,其对应的后台解析bean为
com.alibaba.dubbo.config.spring.ServiceBean,我们来跟踪其代码看看Dubbo服务暴露的过程:
/*** ServiceFactoryBean** @author william.liangf* @export*/public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener, BeanNameAware {......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();}}}......}
ApplicationListener接口,那么它就会监听容器相关事件。通过上述代码片段我们看到这里它监听了容器的
ContentRefreshedEvent事件。并且调用了
export()方法。
ApplicationContextEvent相关事件见下图:

继续看export()方法的代码:
public synchronized void export() {if (provider != null) {if (export == null) {export = provider.getExport();}if (delay == null) {delay = provider.getDelay();}}if (export != null && ! export.booleanValue()) {return;}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();}}
这里没什么好讲的,重点是doExport()
方法。(当然了,上述代码中有个服务延迟暴露的知识点,我们留作以后再做分析)。
继续向下看:
com.alibaba.dubbo.config.ServiceConfig#doExport----> com.alibaba.dubbo.config.ServiceConfig#doExportUrls----> com.alibaba.dubbo.config.ServiceConfig#doExportUrlsFor1Protocol
代码比较长,我们重点看这部分:
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {......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 = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));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);}}}......}
代码已经很清晰了,大致有以下2层意思:
配置项
scope = none
,则不暴露服务。配置项
scope != remote
,则先暴露本地服务。如果同时scope !=local
,继续暴露远程服务。
scope = null,以上代码就会先暴露本地服务,然后再暴露远程服务(实际上这也是Dubbo默认的服务暴露行为)。
03. Reference初始化
同dubbo:service
一样,dubbo:reference
也对应一个后台解析Bean,其代码为com.alibaba.dubbo.config.spring.ReferenceBean
。
public class ReferenceBean<T> extends ReferenceConfig<T> implements FactoryBean, ApplicationContextAware, InitializingBean, DisposableBean {......public Object getObject() throws Exception {return get();}......}
这里我们看到ReferenceBean是一个FactoryBean,而Spring的FactoryBean有特殊的含义,调用其getObject()
方法时返回其真实的类型。而该方法又会调用父类ReferenceConfig的get()
方法。
public class ReferenceConfig<T> extends AbstractReferenceConfig {......public synchronized T get() {if (destroyed){throw new IllegalStateException("Already destroyed!");}if (ref == null) {init();}return ref;}private void init() {if (initialized) {return;}initialized = true;......//attributes通过系统context进行存储.StaticContext.getSystemContext().putAll(attributes);ref = createProxy(map);}......//这段代码重点就是最后一行private T createProxy(Map<String, String> map) {URL tmpUrl = new URL("temp", "localhost", 0, map);final boolean isJvmRefer;if (isInjvm() == null) {if (url != null && url.length() > 0) { //指定URL的情况下,不做本地引用isJvmRefer = false;} else if (InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl)) {//默认情况下如果本地有服务暴露,则引用本地服务.isJvmRefer = true;} else {isJvmRefer = false;}} else {isJvmRefer = isInjvm().booleanValue();}if (isJvmRefer) {URL url = new URL(Constants.LOCAL_PROTOCOL, NetUtils.LOCALHOST, 0, interfaceClass.getName()).addParameters(map);invoker = refprotocol.refer(interfaceClass, url);if (logger.isInfoEnabled()) {logger.info("Using injvm service " + interfaceClass.getName());}} else {if (url != null && url.length() > 0) { // 用户指定URL,指定的URL可能是对点对直连地址,也可能是注册中心URLString[] us = Constants.SEMICOLON_SPLIT_PATTERN.split(url);if (us != null && us.length > 0) {for (String u : us) {URL url = URL.valueOf(u);if (url.getPath() == null || url.getPath().length() == 0) {url = url.setPath(interfaceName);}if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {urls.add(url.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));} else {urls.add(ClusterUtils.mergeUrl(url, map));}}}} else { // 通过注册中心配置拼装URLList<URL> us = loadRegistries(false);if (us != null && us.size() > 0) {for (URL u : us) {URL monitorUrl = loadMonitor(u);if (monitorUrl != null) {map.put(Constants.MONITOR_KEY, URL.encode(monitorUrl.toFullString()));}urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));}}if (urls == null || urls.size() == 0) {throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");}}if (urls.size() == 1) {invoker = refprotocol.refer(interfaceClass, urls.get(0));} else {List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();URL registryURL = null;for (URL url : urls) {invokers.add(refprotocol.refer(interfaceClass, url));if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {registryURL = url; // 用了最后一个registry url}}if (registryURL != null) { // 有 注册中心协议的URL// 对有注册中心的Cluster 只用 AvailableClusterURL u = registryURL.addParameter(Constants.CLUSTER_KEY, AvailableCluster.NAME);invoker = cluster.join(new StaticDirectory(u, invokers));} else { // 不是 注册中心的URLinvoker = cluster.join(new StaticDirectory(invokers));}}}Boolean c = check;if (c == null && consumer != null) {c = consumer.isCheck();}if (c == null) {c = true; // default true}if (c && !invoker.isAvailable()) {throw new IllegalStateException("Failed to check the status of the service " + interfaceName + ". No provider available for the service " + (group == null ? "" : group + "/") + interfaceName + (version == null ? "" : ":" + version) + " from the url " + invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());}if (logger.isInfoEnabled()) {logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());}// 创建服务代理return (T) proxyFactory.getProxy(invoker);}......}
com.alibaba.dubbo.config.spring.ReferenceBean#getObject----> com.alibaba.dubbo.config.ReferenceConfig#get----> com.alibaba.dubbo.config.ReferenceConfig#init----> com.alibaba.dubbo.config.ReferenceConfig#createProxy
我们继续看这段非常关键的代码InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl)
:
public class InjvmProtocol extends AbstractProtocol implements Protocol {......private static InjvmProtocol INSTANCE;public InjvmProtocol() {INSTANCE = this;}public static InjvmProtocol getInjvmProtocol() {if (INSTANCE == null) {ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(InjvmProtocol.NAME); // load}return INSTANCE;}......static Exporter<?> getExporter(Map<String, Exporter<?>> map, URL key) {Exporter<?> result = null;if (!key.getServiceKey().contains("*")) {result = map.get(key.getServiceKey());} else {if (map != null && !map.isEmpty()) {for (Exporter<?> exporter : map.values()) {if (UrlUtils.isServiceKeyMatch(key, exporter.getInvoker().getUrl())) {result = exporter;break;}}}}if (result == null) {return null;} else if (ProtocolUtils.isGeneric(result.getInvoker().getUrl().getParameter(Constants.GENERIC_KEY))) {return null;} else {return result;}}......public boolean isInjvmRefer(URL url) {final boolean isJvmRefer;String scope = url.getParameter(Constants.SCOPE_KEY);//本身已经是jvm协议了,走正常流程就是了.if (Constants.LOCAL_PROTOCOL.toString().equals(url.getProtocol())) {isJvmRefer = false;} else if (Constants.SCOPE_LOCAL.equals(scope) || (url.getParameter("injvm", false))) {//如果声明为本地引用//scope=local || injvm=true 等价 injvm标签未来废弃掉.isJvmRefer = true;} else if (Constants.SCOPE_REMOTE.equals(scope)){//声明了是远程引用,则不做本地引用isJvmRefer = false;} else if (url.getParameter(Constants.GENERIC_KEY, false)){//泛化调用不走本地isJvmRefer = false;} else if (getExporter(exporterMap, url) != null) {//默认情况下如果本地有服务暴露,则引用本地服务.isJvmRefer = true;} else {isJvmRefer = false;}return isJvmRefer;}}
以上代码关键点是这个getExporter(exporterMap, url) != null
,仔细阅读其代码,意思大致是说:先查找本地暴露的服务列表,如果要调用的接口在本地暴露列表中存在的话,就通过InjvmProtocol调用服务。
04. 结束语
至此,我们解密了Dubbo框架的本地调用的实现原理,代码虽然层次较多,但整体还是思路比较清晰的。最后我们再说下InjvmProtocol和DubboProtocol的区别。其实InjvmProtocol也会像远程服务一样走Dubbo调用链,只不过InjvmProtocol协议因为是调用的本地服务,不再需要进行Socket网络通讯,也没有了编解码的过程。




