文章目录
1 Dubbo consumer启动过程
Dubbo provider启动伴随的重要动作是dubbo服务的导出,也叫服务暴露。而Dubbo consumer启动框架则会进行服务引用,让consumer应用获取到自己关注的服务信息。
在dubbo中,引用服务有两种方式。第一种是使用服务直连的方式引用服务,第二种方式是基于注册中心进行引用。其中服务直连方式又包含远程服务直连引用和本地JVM服务引用。
服务直连方式引用服务适用于测试环境,实际应用场景都会使用到注册中心,基于注册中心进行引用。引用服务过程不仅包括从注册服务获取服务元数据,还包括consumer应用生成Invoker,创建代理类等步骤。
1.1 服务引用原理与过程
1.1.1 服务引用触发时机
Dubbo 服务引用的时机有两个:
- 饿汉式:第一个是在 Spring 容器初始化ReferenceBean,会调用 ReferenceBean 的
afterPropertiesSet
方法时引用服务 - 懒汉式:第二个是在 ReferenceBean 对应的服务被注入到其他类中时引用。
Dubbo 使用懒汉式引用服务。如果需要使用饿汉式,可通过配置 <dubbo:reference>
的 init 属性开启。
饿汉式的开始服务引用的时机很清楚, 调用ReferenceBean的afterPropertiesSet
方法时触发,对于懒汉式,当我们的服务被注入到其他类中时,Spring 会第一时间调用 getObject 方法,并由该方法执行服务引用逻辑。(getObject 方法定义在 Spring 的 FactoryBean 接口中)下面看看服务引用的流程。
1.1.2 服务引用过程
服务引用的过程大概是以下几个步骤
- 参数收集和配置检查。
- 解析配置信息,根据URL头决定服务用的方式。有三种,本地JVM服务引用、远程服务直连、通过注册中心引用。
- 生成Invoker。如果有多个注册中心,多个服务提供者,这个时候会得到一组 Invoker 实例,此时需要通过集群管理类 Cluster 将多个 Invoker 合并成一个实例。
- 通过代理工厂类 (ProxyFactory) 为服务接口生成代理类,让代理类去调用 Invoker 逻辑。避免了 Dubbo 框架代码对业务代码的侵入。
下面用官网一张图表示服务引用的总体过程:
2 服务引用过程源码分析
服务引用的入口方法为 ReferenceBean 的 getObject 方法,该方法在上面说的两个时机被调用。
2.1 入口代码
getObject()
@Override
public Object getObject() throws Exception {
return get();
}
get方法在ReferenceBean的父类ReferenceConfig中
get()
public synchronized T get() {
if (destroyed) {
throw new IllegalStateException("Already destroyed!");
}
// 检测 ref 是否为空,为空则通过 init 方法创建
if (ref == null) {
// init 方法主要用于处理配置,以及调用 createProxy 生成代理类
init();
}
return ref;
}
2.2 配置收集与检查
Dubbo 在引用或导出服务时,首先会对这些配置进行检查和处理,以保证配置的正确性。服务引用过程的配置解析逻辑封装在 ReferenceConfig 的 init 方法中
init()
private void init() {
//省略。。。
// 创建代理类
ref = createProxy(map);
// 根据服务名,ReferenceConfig,代理类构建 ConsumerModel,
// 并将 ConsumerModel 存入到 ApplicationModel 中
ConsumerModel consumerModel = new ConsumerModel(getUniqueServiceName(), this, ref, interfaceClass.getMethods());
ApplicationModel.initConsumerModel(getUniqueServiceName(), consumerModel);
}
init方法主要做的事情就是处理配置,原理与服务暴露时的doExport方法大同小异。由于init方法代码较长,这里具体代码就不贴出来了,但是它所做的事情需要弄清楚。
init方法做的事情比较多,按代码顺序详细分为以下几个步骤:
- 首先做一些检查,包括是否已经初始化、接口名合法性校验、检测 ConsumerConfig 实例是否存在,如不存在则创建一个新的实例,然后通过系统变量或 dubbo.properties 配置文件填充 ConsumerConfig 的字段、检测泛化配置,并根据配置设置 interfaceClass 的值。
- 从系统属性或配置文件中加载与接口名相对应的配置,并将解析结果赋值给 url 字段。当然,系统属性优先级高于配置文件。
- 检测几个核心配置类是否为空,为空则尝试从其他配置类中获取。比如reference里的application、registries配置为空则从consumer中获取。类似service从provider里获取。
- 然后收集各种配置,并将配置存储到 map 中。其中用到一个
appendParameters
方法,这个方法时父类abstractConfig提供的,服务暴露时也用这个方法来将配置存储到map中。 - 然后一段代码是处理 MethodConfig 实例。该实例包含了事件通知配置,比如 onreturn、onthrow、oninvoke 等
- 最后解析服务消费者 ip,以及调用 createProxy 创建代理对象。
init方法处理并保存完配置信息,就可以创建Invoker代理了
2.3 判断服务引用方式
createProxy()
private T createProxy(Map<String, String> map) {
URL tmpUrl = new URL("temp", "localhost", 0, map);
//===============1.判断服务引用方式,组装url================
final boolean isJvmRefer;
if (isInjvm() == null) {
// if a url is specified, don't do local reference
if (url != null && url.length() > 0) {
isJvmRefer = false;
// 根据 url 的协议、scope 以及 injvm 等参数检测是否需要本地引用
// 比如如果用户显式配置了 scope=local,此时 isInjvmRefer 返回 true
} else if (InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl)) {
// by default, reference local service if there is
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 {// 远程引用
// url 不为空,表明用户可能想进行点对点调用,或者是注册中心地址
if (url != null && url.length() > 0) {
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.isEmpty()) {
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.isEmpty()) {
throw new IllegalStateException("...");
}
}
//===============2.调用对应Protocol实例的refer方法创建Invoker================
// 单个注册中心或服务提供者(服务直连,下同)
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) {
// 通过 refprotocol 调用 refer 构建 Invoker,refprotocol 会在运行时
// 根据 url 协议头加载指定的 Protocol 实例,并调用实例的 refer 方法
invokers.add(refprotocol.refer(interfaceClass, url));
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
registryURL = url;
}
}
if (registryURL != null) { // registry url is available
// 如果注册中心链接不为空,则将使用 AvailableCluster
URL u = registryURL.addParameter(Constants.CLUSTER_KEY, AvailableCluster.NAME);
// 创建 StaticDirectory 实例,并由 Cluster 对多个 Invoker 进行合并
invoker = cluster.join(new StaticDirectory(u, invokers));
} else { // not a registry 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("...");
}
// invoker 可用性检查
if (logger.isInfoEnabled()) {
logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());
}
// 创建服务代理
return (T) proxyFactory.getProxy(invoker);
}
上面代码很多,不过逻辑比较清晰。首先根据配置判断服务引用方式,是本地JVM引用、还是远程直连、还是注册中心。若是本地调用,则调用 InjvmProtocol 的 refer 方法生成 InjvmInvoker 实例。若不是,则读取直连配置项,或注册中心 url,并将读取到的 url 存储到 urls 中。然后根据 urls 元素数量进行后续操作。若 urls 元素数量为1,则直接通过 Protocol 自适应拓展类构建 Invoker 实例接口。若 urls 元素数量大于1,即存在多个注册中心或服务直连 url,此时先根据 url 构建 Invoker。然后再通过 Cluster 合并多个 Invoker,最后调用 ProxyFactory 生成代理类。
生成服务代理类,用户就可以通过该类进行远程服务调用了。服务引用的流程就算完成了。
上面的流程还有两个重要的过程需要细看,一个是调用Protocol实例的refer方法生成Invoker,一个是proxyFactory.getProxy(invoker)生成服务代理对象。
2.4 创建Invoker
Invoker 是 Dubbo 的核心模型,代表一个可执行体。
在服务提供方,Invoker 由ProxyFactory的以下方法创建,用于调用服务提供类。
@Adaptive({Constants.PROXY_KEY})
<T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) throws RpcException;
在服务消费方,Invoker由Protocol的以下方法创建, 用于执行远程调用。
@Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
Protocol的实现分为两类,一种是RegistryProtocol,会先处理注册中中心相关逻辑,一种是各种PRC协议的实现,如InjvmProtocol、ThriftProtocol、以及默认使用的DubboProtocol。下面先介绍DubboProtocol的refer方法现实流程,再介绍RegistProtocol的refer方法。
2.4.1 DubboProtocol#refer()创建Invoker
refer()
public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {
optimizeSerialization(url);
// create rpc invoker.
DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
invokers.add(invoker);
return invoker;
}
上面代码主要是通过服务类型,url,网络通信客户端生成Invoker实例,并保存在invokers集合中。
getClients(url)获取客户端实例为ExchangeClient类型,ExchangeClient 实际上并不具备通信能力,它需要基于更底层的客户端实例进行通信。比如 NettyClient、MinaClient 等,默认情况下,Dubbo 使用 NettyClient 进行通信。
下面看看getClients的代码:
getClients()
private ExchangeClient[] getClients(URL url) {
// 是否共享连接
boolean service_share_connect = false;
// 获取连接数,默认为0,表示未配置
int connections = url.getParameter(Constants.CONNECTIONS_KEY, 0);
// 如果未配置 connections,则共享连接
if (connections == 0) {
service_share_connect = true;
connections = 1;
}
ExchangeClient[] clients = new ExchangeClient[connections];
for (int i = 0; i < clients.length; i++) {
if (service_share_connect) {
// 获取共享客户端
clients[i] = getSharedClient(url);
} else {
// 初始化新的客户端
clients[i] = initClient(url);
}
}
return clients;
}
这里根据 connections 数量决定是获取共享客户端还是创建新的客户端实例,默认情况下,使用共享客户端实例。getSharedClient 方法中也会调用 initClient 方法。
getSharedClient方法上先访问缓存,若缓存未命中,则通过 initClient 方法创建新的 ExchangeClient 实例,并将该实例传给 ReferenceCountExchangeClient 构造方法创建一个带有引用计数功能的 ExchangeClient 实例。
下面看看initClient()方法:
initClient()
private ExchangeClient initClient(URL url) {
// 获取客户端类型,默认为 netty
String str = url.getParameter(Constants.CLIENT_KEY, url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_CLIENT));
url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME);
// 添加编解码和心跳包参数到 url 中
url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));
// 检测客户端类型是否存在,不存在则抛出异常
// 不再支持BIO,因为它会带来性能问题
if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) {
throw new RpcException("..."));
}
ExchangeClient client;
try {
// 根据配置生成懒加载或普通客户端实例
if (url.getParameter(Constants.LAZY_CONNECT_KEY, false)) {
client = new LazyConnectExchangeClient(url, requestHandler);
} else {
client = Exchangers.connect(url, requestHandler);
}
} catch (RemotingException e) {
throw new RpcException("Fail to create remoting client for service...");
}
return client;
}
initClient 方法首先获取用户配置的客户端类型,默认为 netty,检查该类型是否存在,然后在connect方法里,通过SPI机制根据客户端类型名称获取对应客户端实例。比如new NettyClient(url, listener);
DubboProtocol默认使用单一长连接BIO异步通信。
然后来看看RegistyProtocol的refer方法,用于处理注册中心相关的服务引用。
2.4.1 RegistyProtocol#refer()创建Invoker
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
// 取 registry 参数值,并将其设置为协议头
url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY);
// 获取注册中心实例
Registry registry = registryFactory.getRegistry(url);
if (RegistryService.class.equals(type)) {
return proxyFactory.getInvoker((T) registry, type, url);
}
// 将 url 查询字符串转为 Map
Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(Constants.REFER_KEY));
// 获取 group 配置
String group = qs.get(Constants.GROUP_KEY);
if (group != null && group.length() > 0) {
if ((Constants.COMMA_SPLIT_PATTERN.split(group)).length > 1
|| "*".equals(group)) {
// 通过 SPI 加载 MergeableCluster 实例,并调用 doRefer 继续执行服务引用逻辑
return doRefer(getMergeableCluster(), registry, type, url);
}
}
// 调用 doRefer 继续执行服务引用逻辑
return doRefer(cluster, registry, type, url);
}
上面代码首先为 url 设置协议头,然后根据 url 参数加载注册中心实例。继续看doRefer()方法。
doRefer()
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
// 创建 RegistryDirectory 实例
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
// 设置注册中心和协议
directory.setRegistry(registry);
directory.setProtocol(protocol);
Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
// 生成服务消费者链接
URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, parameters.remove(Constants.REGISTER_IP_KEY), 0, type.getName(), parameters);
// 注册服务消费者,在 consumers 目录下新节点
if (!Constants.ANY_VALUE.equals(url.getServiceInterface())
&& url.getParameter(Constants.REGISTER_KEY, true)) {
registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY, Constants.CONSUMERS_CATEGORY,
Constants.CHECK_KEY, String.valueOf(false)));
}
// 订阅 providers、configurators、routers 等节点数据
directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY,
Constants.PROVIDERS_CATEGORY
+ "," + Constants.CONFIGURATORS_CATEGORY
+ "," + Constants.ROUTERS_CATEGORY));
// 一个注册中心可能有多个服务提供者,因此这里需要将多个服务提供者合并为一个
Invoker invoker = cluster.join(directory);
ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
return invoker;
}
doRefer 方法创建一个 RegistryDirectory 实例,然后生成服务者消费者链接,并向注册中心进行注册。注册完毕后,紧接着订阅 providers、configurators、routers 等节点下的数据。Cluster 将多个服务节点合并为一个,并生成一个 Invoker。
以上就是生成Invoker的逻辑,下面再看另一个服务引用的重要过程,生成代理。
2.5 创建服务代理
Invoker 创建完毕后,接下来要做的事情是为服务接口生成代理对象。有了代理对象,即可进行远程调用。
代理对象生成的入口方法为 ProxyFactory 的 getProxy方法
getProxy()
public <T> T getProxy(Invoker<T> invoker, boolean generic) throws RpcException {
Class<?>[] interfaces = null;
// 获取接口列表
String config = invoker.getUrl().getParameter("interfaces");
if (config != null && config.length() > 0) {
// 切分接口列表
String[] types = Constants.COMMA_SPLIT_PATTERN.split(config);
if (types != null && types.length > 0) {
interfaces = new Class<?>[types.length + 2];
// 设置服务接口类和 EchoService.class 到 interfaces 中
interfaces[0] = invoker.getInterface();
interfaces[1] = EchoService.class;
for (int i = 0; i < types.length; i++) {
// 加载接口类
interfaces[i + 1] = ReflectUtils.forName(types[i]);
}
}
}
if (interfaces == null) {
interfaces = new Class<?>[]{invoker.getInterface(), EchoService.class};
}
// 为 http 和 hessian 协议提供泛化调用支持,
if (!invoker.getInterface().equals(GenericService.class) && generic) {
int len = interfaces.length;
Class<?>[] temp = interfaces;
// 创建新的 interfaces 数组
interfaces = new Class<?>[len + 1];
System.arraycopy(temp, 0, interfaces, 0, len);
// 设置 GenericService.class 到数组中
interfaces[len] = GenericService.class;
}
// 调用重载方法
return getProxy(invoker, interfaces);
}
以上代码在AbstractProxyFactory中,用于获取interfaces列表,最后调用的getProxy重载方法由子类实现。
javaassistProxyFactory的实现:
public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
}
JdkProxyFactory的实现:
public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), interfaces, new InvokerInvocationHandler(invoker));
}
两种实现都会用到jdk动态代理,区别是javaassist实现方式是先生成Proxy.getProxy(interfaces)
生成Proxy的子类,再使用jdk动态代理生成服务代理。
3 总结
最后总结一下服务引用的具体过程:
- 应用启动,服务引用两大入口:第一,Spring容器初始化ReferenceBean会触发ReferenceConfig的get()方法。第二,其他类型引用ReferenceBean触发ReferenceBean的getObject()方法,进而调用ReferenceConfig的get()方法。
- 先进行一些检查,然后执行init方法,进行配置检查。
- 配置检查与参数获取,判断服务引用方式
<T> Invoker<T> refer(Class<T> type, URL url)
方法生成Invoker,这个过程可能包含注册订阅信息的注册中心的操作(T) proxyFactory.getProxy(invoker)
方法创建服务代理
有了服务代理,就可以进行远程服务调用了。服务引用完成。