1. 前言
DubboBootstrap启动时,首先会通过initialize()方法完成初始化,装配各种Config对象,为后续的服务暴露和引用准备好环境。
ReferenceConfig是Dubbo对服务引用的描述类,它记录了需要引用的服务的协议、版本、服务名称、服务地址等信息,get()方法可以获取接口对象,接口是不能实例化的,这个对象其实是Dubbo创建的代理对象,Dubbo会在生成的代理对象中帮我们处理RPC调用、集群容错、负载均衡等复杂的逻辑。
DubboBootstrap初始化结束后,会调用DubboBootstrap#referServices()方法引用依赖的服务,其实就是从ConfigManager提取出所有的ReferenceConfig对象,然后挨个调用get()方法创建代理对象,以它为入口分析即可。
2. 源码分析
ref属性记录了Dubbo生成的代理对象,
get()方法会判断ref是否已经生成,已经生成就直接返回,否则会调用init()方法进行初始化,初始化的过程中会创建代理对象。
public synchronized T get() {
if (destroyed) {
throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");
}
if (ref == null) {
init();
}
return ref;
}
init()方法主要做了以下事情:
- 检查和更新配置
- 检查stub、local
- 创建代理对象
- 分发init事件
我们重点关注1和3,尤其是第三步,在这个创建的代理对象中,实现了Dubbo RPC调用的所有核心逻辑。
2.1 checkAndUpdateSubConfigs()
和ServiceConfig类似,ReferenceConfig在引用服务前也需要对相关配置进行检查和更新。 首先是如果没有指定相关配置,使用ConsumerConfig的默认配置,以及相关的Check和自身属性的刷新。
// 使用ConsumerConfig默认配置
completeCompoundConfigs(consumer);
if (consumer != null) {
if (StringUtils.isEmpty(registryIds)) {
setRegistryIds(consumer.getRegistryIds());
}
}
// ConsumerConfig不存在会自动创建
checkDefault();
// 自身属性根据优先级刷新
this.refresh();
然后是通用服务相关的处理:
if (getGeneric() == null && getConsumer() != null) {
setGeneric(getConsumer().getGeneric());
}
if (ProtocolUtils.isGeneric(generic)) {// 通用服务
interfaceClass = GenericService.class;
} else {
try {
interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
.getContextClassLoader());
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e.getMessage(), e);
}
checkInterfaceAndMethods(interfaceClass, getMethods());
}
初始化服务元数据:
serviceMetadata.setVersion(getVersion());
serviceMetadata.setGroup(getGroup());
serviceMetadata.setDefaultGroup(getGroup());
serviceMetadata.setServiceType(getActualInterface());
serviceMetadata.setServiceInterfaceName(interfaceName);
serviceMetadata.setServiceKey(URL.buildKey(interfaceName, group, version));
要消费的服务注册到本地ServiceRepository:
ServiceRepository repository = ApplicationModel.getServiceRepository();
ServiceDescriptor serviceDescriptor = repository.registerService(interfaceClass);
repository.registerConsumer(
serviceMetadata.getServiceKey(),
serviceDescriptor,
this,
null,
serviceMetadata);
resolveFile()方法会解析dubbo.resolve.file文件,判断是否需要点对点直连Provider。然后就是对自身进行一个Check。
// 解析dubbo.resolve.file,直连Provider
resolveFile();
ConfigValidationUtils.validateReferenceConfig(this);
postProcessConfig();
2.2 createProxy()
上述操作完成后,接下来会进行Stub、Local和Mock的校验,再然后就是开始构建引用服务所需的参数,使用HashMap存储,最终调用createProxy()开始创建代理对象。
Map<String, String> map = new HashMap<String, String>();
// 端点 表明是消费端
map.put(SIDE_KEY, CONSUMER_SIDE);
// 运行时参数
ReferenceConfigBase.appendRuntimeParameters(map);
map.put(INTERFACE_KEY, interfaceName);
AbstractConfig.appendParameters(map, getMetrics());
AbstractConfig.appendParameters(map, getApplication());
AbstractConfig.appendParameters(map, getModule());
......
// 创建代理对象
ref = createProxy(map);
如果在同一个JVM里,Consumer要引用的服务自身已经提供了,那么Dubbo会尝试直接引用同一个JVM内的服务,这样可以避免网络传输带来的性能损耗,shouldJvmRefer()方法判断是否应该这么做。
if (shouldJvmRefer(map)) {
// 优先引用同一个JVM内的Provider
URL url = new URL(LOCAL_PROTOCOL, LOCALHOST_VALUE, 0, interfaceClass.getName()).addParameters(map);
invoker = REF_PROTOCOL.refer(interfaceClass, url);
if (logger.isInfoEnabled()) {
logger.info("Using injvm service " + interfaceClass.getName());
}
}
大多数情况下,引用的还是远程服务,所以接下来要分析的才是重点。 如果配置了点对点直连的url,则会开始解析它:
if (url != null && url.length() > 0) {
// ,分割 点对点直连url
String[] us = SEMICOLON_SPLIT_PATTERN.split(url);
if (us != null && us.length > 0) {
for (String u : us) {
URL url = URL.valueOf(u);
if (StringUtils.isEmpty(url.getPath())) {
url = url.setPath(interfaceName);
}
// 解析,保存到urls集合
if (UrlUtils.isRegistry(url)) {
urls.add(url.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
} else {
urls.add(ClusterUtils.mergeUrl(url, map));
}
}
}
}
一般很少配置直连url,没有配置的话Dubbo就会基于注册中心去引用服务。
if (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())) {// 非injvm协议
checkRegistry();
// 加载注册中心URL
List<URL> us = ConfigValidationUtils.loadRegistries(this, false);
if (CollectionUtils.isNotEmpty(us)) {
for (URL u : us) {
URL monitorUrl = ConfigValidationUtils.loadMonitor(this, u);
if (monitorUrl != null) {
map.put(MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
}
// 给注册中心URL添加要引用的服务参数:refer
urls.add(u.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
}
}
if (urls.isEmpty()) {
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.");
}
}
不论如何,最终解析出的urls就是要引用的服务URL列表,可能一个也可能多个,我们目前只看一个url的情况。
if (urls.size() == 1) {
// 通过协议引用服务
invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(0));
}
2.2.1 构建Invoker
Protocol#refer()方法可以引用远程服务,得到Invoker对象,Consumer可以对Invoker发起服务调用,这个调用可能是远程调用,也可能是集群调用,取决于子类如何实现。
如果是点对点直连Provider,那就是根据协议创建对应的Invoker,例如DubboInvoker,这种就是简单的远程调用,没有服务重试、负载均衡的功能。
如果配置了多个点对点的直连url,相当于有多个Provider可以调用,Dubbo会创建StaticDirectory,它代表一个静态的服务目录,Provider的数量是明确的,此时基于Cluster#join()方法创建ClusterInvoker对象,它拥有集群容错的功能。
不过一般来说,上面两种方法都很少用,更多的是基于注册中心来实现自动的服务注册与发现。这种情况下,URL的协议就是registry,对应的方法是RegistryProtocol#refer()。
Protocol接口有两个包装类:ProtocolFilterWrapper和ProtocolListenerWrapper,在根据URL协议调用具体的refer()方法前会先经过这两个包装类,这是由Dubbo的SPI机制决定的。前者的作用是为Invoker构建FilterChian,执行过滤器逻辑;后者的作用是在服务引用和销毁时触发相应的事件。
基于注册中心引用服务的流程是这样子的,先解析注册中心URL,利用SPI加载对应的Registry实现类,例如NacosRegistry。然后根据cluster参数加载对应的集群容错策略,默认是FailoverCluster。再创建RegistryDirectory,它是基于注册中心的服务调用目录,通过它可以知道都有哪些Provider可以调用。最终调用Cluster#join()创建对应的ClusterInvoker实现。
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
// 重写URL,获取注册中心URL
url = getRegistryUrl(url);
Registry registry = registryFactory.getRegistry(url);
if (RegistryService.class.equals(type)) {
return proxyFactory.getInvoker((T) registry, type, url);
}
// SPI加载Cluster
Cluster cluster = Cluster.getCluster(qs.get(CLUSTER_KEY));
// 引用服务
return doRefer(cluster, registry, type, url);
}
关于RegistryDirectory,这里有两个方法需要关注。buildRouterChain()方法用于构建RouterChain,每个Directory都有一条Router路由链,Dubbo的路由机制可以根据路由规则对Provider进行筛选。subscribe()方法会订阅服务,当Provider服务发生变更时会收到通知,触发notify()方法,notify()方法里会将ProviderUrls转换成对应的Invoker,这个待会细说。
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
directory.setRegistry(registry);
directory.setProtocol(protocol);
Map<String, String> parameters = new HashMap<String, String>(directory.getConsumerUrl().getParameters());
URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
if (directory.isShouldRegister()) {
directory.setRegisteredConsumerUrl(subscribeUrl);
// 注册Consumer
registry.register(directory.getRegisteredConsumerUrl());
}
directory.buildRouterChain(subscribeUrl);
// 订阅服务,服务变更时触发notify()
directory.subscribe(toSubscribeUrl(subscribeUrl));
// 创建Invoker对象
Invoker<T> invoker = cluster.join(directory);
List<RegistryProtocolListener> listeners = findRegistryProtocolListeners(url);
if (CollectionUtils.isEmpty(listeners)) {
return invoker;
}
RegistryInvokerWrapper<T> registryInvokerWrapper = new RegistryInvokerWrapper<>(directory, cluster, invoker);
for (RegistryProtocolListener listener : listeners) {
listener.onRefer(this, registryInvokerWrapper);
}
return registryInvokerWrapper;
}
Cluster#join()方法用于创建ClusterInvoker,它拥有集群容错的能力,我们看下创建过程。doJoin()是子类实现的创建对应ClusterInvoker的方法,buildClusterInterceptors()方法会加载ClusterInterceptor拦截器,构建拦截器链,拦截ClusterInvoker的invoke()方法。
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
/**
* doJoin:创建ClusterInvoker
* buildClusterInterceptors:构建Cluter拦截器链
*/
return buildClusterInterceptors(doJoin(directory), directory.getUrl().getParameter(REFERENCE_INTERCEPTOR_KEY));
}
2.2.2 订阅服务
Cluster创建ClusterInvoker是依赖Directory的,ClusterInvoker不做具体的服务调用,只是在Invoker的基础上增加了集群容错的能力。ClusterInvoker会通过Directory过滤出可调用的服务列表,然后通过LoadBalance最终选出一个服务进行调用,一旦调用失败就要进行容错处理,例如FailoverClusterInvoker会进行重试。
public Result invoke(final Invocation invocation) throws RpcException {
checkWhetherDestroyed();
Map<String, Object> contextAttachments = RpcContext.getContext().getObjectAttachments();
if (contextAttachments != null && contextAttachments.size() != 0) {
((RpcInvocation) invocation).addObjectAttachments(contextAttachments);
}
// 通过Directory过滤服务列表
List<Invoker<T>> invokers = list(invocation);
// 初始化 负载均衡
LoadBalance loadbalance = initLoadBalance(invokers, invocation);
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
return doInvoke(invocation, invokers, loadbalance);
}
Directory是如何获取invokers的呢?Directory接口有两个实现类:StaticDirectory和RegistryDirectory,前者用于点对点直连时已经明确Provider的场景,后者是基于注册中心的,适用于Provider不断变化的场景。
还记得之前说过的吗?RegistryDirectory创建完,会调用subscribe()方法订阅服务,这样当Provider发生变化时会收到通知并触发notify()方法,这里就会自动将ProviderUrl转换成对应的Invoker,方法是RegistryDirectory#toInvokers(),方法较长,下面是精简后的大致意思:
// 协议引用获得Invoker
invoker = new InvokerDelegate<>(protocol.refer(serviceType, url), url, providerUrl);
// 存储到Map容器
newUrlInvokerMap.put(key, invoker);
例如,当Provider使用dubbo协议时,就会调用DubboProtocol#protocolBindingRefer()方法,创建DubboInvoker并返回。在创建DubboInvoker的过程中,就会去和服务端建立连接。
public <T> Invoker<T> protocolBindingRefer(Class<T> serviceType, URL url) throws RpcException {
optimizeSerialization(url);
DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
invokers.add(invoker);
return invoker;
}
getClients()方法会获取客户端ExchangeClient对象,和服务端建立连接。可以看到它是一个数组,这意味着Consumer可以跟同一个Provider同时建立多个连接,通过参数connections进行配置。默认情况下,该值为0,Consumer只会建立一条连接,所有人都共用这条连接。一般不建议修改该值,除非是一对一压测时,可以通过调大它来增强网络传输的能力。
private ExchangeClient[] getClients(URL url) {
// 使用共享连接
boolean useShareConnect = false;
// 连接数,=0代表使用共享连接,否则创建独占连接
int connections = url.getParameter(CONNECTIONS_KEY, 0);
List<ReferenceCountExchangeClient> shareClients = null;
// if not configured, connection is shared, otherwise, one connection for one service
if (connections == 0) {
useShareConnect = true; String shareConnectionsStr = url.getParameter(SHARE_CONNECTIONS_KEY, (String) null);
connections = Integer.parseInt(StringUtils.isBlank(shareConnectionsStr) ? ConfigUtils.getProperty(SHARE_CONNECTIONS_KEY,
DEFAULT_SHARE_CONNECTIONS) : shareConnectionsStr);
shareClients = getSharedClient(url, connections);
}
ExchangeClient[] clients = new ExchangeClient[connections];
for (int i = 0; i < clients.length; i++) {
if (useShareConnect) {
clients[i] = shareClients.get(i);
} else {
// 初始化客户端
clients[i] = initClient(url);
}
}
return clients;
}
initClient()方法会创建ExchangeClient并和服务端建立连接,这涉及到网络传输层,Dubbo会根据client或transporter参数加载对应的实现,默认是netty,即创建NettyClient。在NettyClient的构造函数中,会创建Bootstrap并设置EventLoopGroup、编排ChannelHandlerPipeline等,最终调用Bootstrap#connect()连接服务端,这部分是Netty的代码,这里就不贴了。
DubboInvoker在执行invoke调用时,会从ExchangeClient数组中轮询一个出来,然后发送网络请求。
2.2.3 创建代理对象
通过Protocol#refer()得到的Invoker对象并不能直接使用,开发者面向接口编程,我们并不关心Invoker对象,我们需要的是接口实例。
所以,Dubbo还需要基于Invoker来创建接口的代理对象,这个并不难,通过JDK动态代理、字节码技术都能实现。
ProxyFactory#getProxy()方法用来获取Invoker的代理对象,它有两个实现类JdkProxyFactory和JavassistProxyFactory,前者使用JDK动态代理,后者使用javassist字节码技术,Dubbo默认使用后者。
// create service proxy
return (T) PROXY_FACTORY.getProxy(invoker, ProtocolUtils.isGeneric(generic));
public class JavassistProxyFactory extends AbstractProxyFactory {
@Override
@SuppressWarnings("unchecked")
public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
}
}
我们对代理对象发起的调用,最终会调用InvokerInvocationHandler#invoke(),非Object继承来的方法,会发起RPC调用。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
......
RpcInvocation rpcInvocation = new RpcInvocation(method, invoker.getInterface().getName(), args);
String serviceKey = invoker.getUrl().getServiceKey();
rpcInvocation.setTargetServiceUniqueName(serviceKey);
if (consumerModel != null) {
rpcInvocation.put(Constants.CONSUMER_MODEL, consumerModel);
rpcInvocation.put(Constants.METHOD_MODEL, consumerModel.getMethodModel(method));
}
return invoker.invoke(rpcInvocation).recreate();
}
3. 总结
Dubbo引用服务,首先会对相关的配置进行检查和更新,然后引用服务创建Invoker,最终基于Invoker创建代理对象,这是大致的流程。
在引用服务时,会判断自身是否已经提供了该服务,如果是会优先引用自身,跳过网络传输,提高效率。
引用远程服务时,如果是基于注册中心的,那么会创建RegistryDirectory,它会去订阅相关服务,然后将ProviderUrls转换成对应的Invoker,例如DubboInvoker,DubboInvoker创建时会自动创建NettyClient和服务端建立连接,DubboInvoker在执行invoke调用时会轮训出NettyClient发送网络请求。
invokers转换完毕,开始构建RouterChain执行路由逻辑,list()方法会进行路由过滤。
最后根据配置的集群容错策略加载对应的Cluster实现,Cluster会基于RegistryDirectory创建ClusterInvoker,ClusterInvoker本身不实现服务调用的逻辑,它只是在Invoker的基础上增加了集群容错的能力。