暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

详解Dubbo中的InjvmProtocol运行原理

编程阁楼 2021-01-01
399

Dubbo是一个RPC远程调用的框架,对于一个服务提供方,暴露了一个接口给外部消费方调用。如果服务提供方自身也需要调用这个接口会怎么样呢,难道也需要走远程编解码和数据网络传输这套流程吗?


对于Dubbo这么优秀的开源框架,显然是需要支持本地调用的,Dubbo提供了本地调用的InjvmProtocol
协议。今天我们代码演示来详细分析下InjvmProtocol
是的工作原理。


01. 演示代码

演示配置文件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/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://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本地调用并不需做特殊配置,按正常 Dubbo 服务暴露即可。Dubbo服务在暴露远程服务的同时,也会同时以 injvm 协议暴露本地服务。injvm 是一个伪协议,不会像其他协议那样对外开启端口,仅用于本地调用。

02. Service暴露行为

首先,我们知道,任何一个dubbo配置标签都对应一个后台的解析Bean,拿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();
}
}
}

......
}
我们看到它实现了Spring框架的ApplicationListener
接口,那么它就会监听容器相关事件。通过上述代码片段我们看到这里它监听了容器的ContentRefreshedEvent
事件。并且调用了export()
方法。

说明:关于Spring容器的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层意思:

  1. 配置项scope = none
    ,则不暴露服务。

  2. 配置项scope != remote
    ,则先暴露本地服务。如果同时scope !=local
    ,继续暴露远程服务

显然,我们演示代码中并没有配置scope,也就是说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可能是对点对直连地址,也可能是注册中心URL
String[] 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 { // 通过注册中心配置拼装URL
List<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 只用 AvailableCluster
URL u = registryURL.addParameter(Constants.CLUSTER_KEY, AvailableCluster.NAME);
invoker = cluster.join(new StaticDirectory(u, invokers));
} else { // 不是 注册中心的URL
invoker = 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网络通讯,也没有了编解码的过程。



文章转载自编程阁楼,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论