根据前面两篇自定义RPC框架的文章,我们带着问题来学习Dubbo。
学习Dubbo是如何解决这些服务调用、注册中心、序列化、集群容错等问题。
注意:笔者使用的Dubbo版本为2.7.7,源码地址为:https://github.com/apache/dubbo;
控制台为Dubbo-admin,源码地址为:https://github.com/apache/dubbo-admin
本文主要集中在Dubbo服务端,使用最原始的API方式来构建一个可用服务。
1.使用API构建服务端public class ProviderApplication {
public static void main(String[] args) {
// 服务实现(自定义DemoService接口)
DemoService demoService = new DemoServiceImpl();
// 当前应用配置
ApplicationConfig application = new ApplicationConfig();
application.setName("provider");
// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
// 本地zookeeper作为配置中心
registry.setAddress("zookeeper://localhost:2181");
// 服务提供者协议配置
ProtocolConfig protocol = new ProtocolConfig();
// dubbo协议,并以20881端口暴露
protocol.setName("dubbo");
protocol.setPort(20881);
// 服务提供者暴露服务配置
ServiceConfig service = new ServiceConfig();
service.setApplication(application);
service.setRegistry(registry);
service.setProtocol(protocol);
service.setInterface(DemoService.class);
service.setRef(demoService);
service.setVersion("1.0.0");
// 暴露及注册服务
service.export();
}
}
// 接口
public interface DemoService {
String sayHello(String name);
}
我们使用最原始的这种方式来构造一个Dubbo provider。不建议在分析源码时使用spring-dubbo等方式,因为最终还是解析相关dubbo标签然后生成这样原始的bean的方式来发起dubbo服务的。
有些比较关键的配置类,我们一起来简单分析下。
1.1 ApplicationConfig解析// 应用相关信息
public class ApplicationConfig extends AbstractConfig {
// 名称
private String name;
// 版本
private String version;
// Java字节码编译器,用于动态类的生成,可选:jdk或javassist
private String compiler;
// 日志输出方式,可选:slf4j,jcl,log4j,log4j2,jdk,默认为slf4j
private String logger;
// 配置参数
private Map parameters;
// dubbo 2.5.8 新版本增加了 QOS 模块,提供了新的 telnet 命令支持
// 具体可参考https://dubbo.apache.org/zh/docsv2.7/user/references/qos/
private Boolean qosEnable;
private String qosHost;
private Integer qosPort;
private Boolean qosAcceptForeignIp;
// 对应的注册中心信息
private List registries;
// 对应的监控配置信息
private MonitorConfig monitor;
// 更多可参考:https://dubbo.apache.org/zh/docsv2.7/user/references/xml/dubbo-application/
...
}
ApplicationConfig主要描述了该服务端应用的配置信息。
1.2 RegistryConfig// 注册中心信息
public class RegistryConfig extends AbstractConfig {
// 注册中心地址,如本地zookeeper,则address为zookeeper://localhost:2181
private String address;
private Integer port;
// 协议名称,支持dubbo, multicast, zookeeper, redis等
private String protocol;
// 网络传输方式,可选mina,netty
private String transporter;
// 更多配置可参考:https://dubbo.apache.org/zh/docsv2.7/user/references/xml/dubbo-registry/
...
}
为什么需要注册中心,因为我们需要一个能够保存服务提供者信息的地方,需要能够动态的提醒消费者服务的上下线。
1.3 ProtocolConfig// 协议配置
public class ProtocolConfig extends AbstractConfig {
// 协议名称,默认为dubbo
private String name;
// 协议端口号,默认dubbo协议为20880端口
private Integer port;
// 请求及响应数据包大小限制,默认为8M
private Integer payload;
// 协议编码方式,默认为dubbo
private String codec;
// 序列化方式,默认dubbo为hessian2
private String serialization;
// 服务提供者上下文路径,为服务path的前缀
private String contextpath;
// 协议的消息派发方式,用于指定线程模型,比如:dubbo协议的all, direct, message, execution, connection等
private String dispatcher;
// 网络读写缓冲区大小,默认为8Kb
private Integer buffer;
// 该协议的服务是否注册到注册中心
private Boolean register;
// 用户可自定义线程池信息,用于在接收请求时分配线程
// 比较简单的参数定义,具体可直接看源码注释
private String threadpool;
private String threadname;
private Integer corethreads;
private Integer threads;
private Integer iothreads;
private Integer queues;
// 更多协议配置信息可参考:https://dubbo.apache.org/zh/docsv2.7/user/references/xml/dubbo-protocol/
...
}
Dubbo提供了很多种的协议实现方式,org.apache.dubbo.rpc.Protocol接口,所有的实现类即dubbo提供的协议。具体如下:
后续我们会仔细分析,本文我们先知道有这么多协议类型即可
1.4 ServiceConfigpublic class ServiceConfig extends ServiceConfigBase {
// 主要属性如下
// 服务接口名
protected String interfaceName;
// 服务对象实现引用
protected T ref;
// 服务路径
protected String path;
// 远程服务调用超时时间(毫秒),默认为1秒
protected Integer timeout;
// 远程服务调用重试次数,不包括第一次调用,默认为2,则说明总共会调用3次
protected Integer retries;
// 服务是否动态注册,如果设为false,注册后将显示后disable状态,需人工启用,并且服务提供者停止时,也不会自动取消册,需人工禁用。
protected Boolean dynamic = true;
// 更多参数请参考https://dubbo.apache.org/zh/docsv2.7/user/references/xml/dubbo-service/
...
}
ServiceConfig本身并没有什么属性,主要就是一些方法。
我们使用ServiceConfig主要就是为了指定接口和实现类。
它的属性都在父类中提供,具体类结构图如下:
2.ServiceConfig.export()
上面所有的配置都是为了集成到ServiceConfig中,最重要的还是ServiceConfig.export()方法。
public class ServiceConfig extends ServiceConfigBase {
public synchronized void export() {
...
// 支持延时
if (shouldDelay()) {
DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
} else {
// 交由doExport处理
doExport();
}
exported();
}
// doExport
protected synchronized void doExport() {
if (unexported) {
throw new IllegalStateException("The service " + interfaceClass.getName() + " has already unexported!");
}
// 只能export一次
if (exported) {
return;
}
exported = true;
if (StringUtils.isEmpty(path)) {
path = interfaceName;
}
// 交由doExportUrls处理
doExportUrls();
}
// doExportUrls
private void doExportUrls() {
...
List registryURLs = ConfigValidationUtils.loadRegistries(this, true);
// 这个protocols就是我们上面ServiceConfig添加的的ProtocolConfig对象,如果添加多个,则说明当前服务支持多协议暴露
for (ProtocolConfig protocolConfig : protocols) {
String pathKey = URL.buildKey(getContextPath(protocolConfig)
.map(p -> p + "/" + path)
.orElse(path), group, version);
repository.registerService(pathKey, interfaceClass);
serviceMetadata.setServiceKey(pathKey);
// 重点在这里
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
// 真正的暴露服务的方法
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List registryURLs) {
String name = protocolConfig.getName();
if (StringUtils.isEmpty(name)) {
name = DUBBO;
}
// 配置参数添加到map中
Map map = new HashMap();
map.put(SIDE_KEY, PROVIDER_SIDE);
ServiceConfig.appendRuntimeParameters(map);
...
MetadataReportConfig metadataReportConfig = getMetadataReportConfig();
if (metadataReportConfig != null && metadataReportConfig.isValid()) {
map.putIfAbsent(METADATA_KEY, REMOTE_METADATA_STORAGE_TYPE);
}
// 当MethodConfig不为空时,对其参数进行解析,不是本文重点,直接略过
if (CollectionUtils.isNotEmpty(getMethods())) {
for (MethodConfig method : getMethods()) {
AbstractConfig.appendParameters(map, method, method.getName());
String retryKey = method.getName() + ".retry";
List arguments = method.getArguments();
...
}
}
// GenericService是Dubbo提供的泛化接口,用来进行泛化调用。后续会详细介绍
if (ProtocolUtils.isGeneric(generic)) {
map.put(GENERIC_KEY, generic);
map.put(METHODS_KEY, ANY_VALUE);
} else {
...
String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
if (methods.length == 0) {
map.put(METHODS_KEY, ANY_VALUE);
} else {
map.put(METHODS_KEY, StringUtils.join(new HashSet(Arrays.asList(methods)), ","));
}
}
...
// 获取当前服务在当前协议下的所暴露的ip和port
String host = findConfigedHosts(protocolConfig, registryURLs, map);
Integer port = findConfigedPorts(protocolConfig, name, map);
// dubbo中的服务最终都会以URL的形式暴露出去。该URL是dubbo自定义的
// 就是将所有必须的参数都拼接上去
// 在本例中url为 dubbo://192.168.xxx.xx:20881/xw.demo.DemoService?anyhost=true&application=provider&bind.ip=192.168.xxx.xx&bind.port=20881&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=xw.demo.DemoService&methods=sayHello&pid=14816&release=&revision=1.0.0&side=provider×tamp=1627271120573&version=1.0.0
URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map);
...
// scope可选项为local(只暴露在本地)、remote(暴露在远程,可以对外提供服务)。默认为空,说明远程和本地都要暴露
String scope = url.getParameter(SCOPE_KEY);
if (!SCOPE_NONE.equalsIgnoreCase(scope)) {
if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {
// 暴露在本地
exportLocal(url);
}
// export to remote if the config is not local (export to local only when config is local)
if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
if (CollectionUtils.isNotEmpty(registryURLs)) {
for (URL registryURL : registryURLs) {
...
url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY));
URL monitorUrl = ConfigValidationUtils.loadMonitor(this, registryURL);
if (monitorUrl != null) {
url = url.addParameterAndEncoded(MONITOR_KEY, monitorUrl.toFullString());
}
...
Invoker invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
// 不同的协议有不同的暴露方式
// 最终在这里实现
Exporter exporter = PROTOCOL.export(wrapperInvoker);
exporters.add(exporter);
}
} else {
...
}
...
}
}
this.urls.add(url);
}
}
虽然代码量比较大,但是真正有用的代码不算多,主要就是拼装URL的参数。
最终会根据ServiceConfig所拥有的ProtocolConfig,进行不同协议的暴露。
有关于服务暴露到本地和远程的逻辑,我们后续专门来说明一下。
3.dubbo-admin控制台展示dubbo控制台,作为服务提供者和消费者的展示平台,有助于我们对服务的分析和控制。
笔者的控制台展示如下:
总结:
本文主要从API示例的角度,来展示了dubbo服务提供者的的创建过程。并且分析了在创建过程中使用到的配置类,正如下图所示:
应用所属配置是provider和consumer都需要的:ApplicationConfig、RegistryConfig、MonitorConfig(监控项,非必须项)
服务提供者则需要:ProtocolConfig、ServiceConfig、ProviderConfig(相比ServiceConfig而言,ProviderConfig作为一种服务提供者的基础配置,ServiceConfig中没有配置的项则使用ProviderConfig的配置项)
本文中没有涉及更深层次的调用,只走到Protocol就结束了,后面还有很多代码,后续会继续分析。