前言
在微服务架构流行的当下,高性能开源RPC框架Dubbo现已被广泛的使用在各类系统中,本文将以消费端的视角,通过Dubbo源码的实现来了解消费端获得代理对象Proxy的过程,有助于对Dubbo的底层实现原理与思想的理解,同时能够帮助使用者在实际系统中应用时更恰当的运用Dubbo的能力。
对于Java开发者而言,在刚开始接触使用Dubbo框架时,最大的感受莫过于其便利性,即调用远程的接口可以通过引入一个SDK(jar包)后即可像使用本地方法一样去进行调用接口。在这种便利使用的背后其实是Dubbo框架内部已经做了很多事情,核心的包括注册订阅、负载均衡、网络通信(通信协议、序列化、反序列化)等,当然还有其他更多保证其健壮性、扩展性的功能。
下面将以消费端的初始生成代理类的过程进行学习分析,全文引用Dubbo源码版本为2.7.8-release。
一、消费端代理类生成
如下图为Dubbo的架构图(图片引自Dubbo官方文档),本次内容涉及到的是步骤2、3,也就是消费端和注册中心之间的交互,了解完步骤2、3之后,对步骤4的调用的内容就会很清晰明了。
在使用Dubbo的时,可以采用不同的方式来配置你的Dubbo应用,有包括XML配置、属性配置、动态配置中心、自动加载环境变量、API配置、注解配置。早期最常见的方式是XML配置,而后较多的是注解配置,都会配合属性配置进行,不同方式的配置都会使用相同的底层代码。这里为了方便查看理解选用API配置进行分析说明。
如下测试代码所示,消费端获取接口的方法是在ReferenceConfig的get()方法:
@Test
public void test() {
// <dubbo:application name="consumer-demo" />
ApplicationConfig application = new ApplicationConfig();
application.setName("consumer-demo");
// <dubbo:registry address="zookeeper://xx.xx.x.xx:2183" />
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://xx.xx.x.xx:2183");
// <dubbo:reference interface="org.apache.dubbo.samples.basic.api.DemoService" />
ReferenceConfig<DemoService> rc = new ReferenceConfig<DemoService>();
rc.setApplication(application);
rc.setRegistry(registry);
rc.setInterface(DemoService.class.getName());
// 获取服务接口
DemoService demoService = rc.get();
// 获取接口实例后,可以进行调用
// 略
}
二、ReferenceConfig获取代理类
2.1 init()
(1)如下图,get方法比较简单,如果已经初始化过ref则直接返回,否则进入初始化方法init()。
// ReferenceConfig.java
/**
* The interface proxy reference
*/
private transient volatile T ref;
public synchronized T get() {
if (destroyed) {
throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");
}
if (ref == null) {
// 初始化
init();
}
return ref;
}
(2)如下个init()方法代码,省略了非重点的代码
// ReferenceConfig.java
public synchronized void init() {
// 省略一些初始化检查操作
// 省略参数map的构造过程
// 生成代理类
ref = createProxy(map);
serviceMetadata.setTarget(ref);
serviceMetadata.addAttribute(PROXY_CLASS_REF, ref);
ConsumerModel consumerModel = repository.lookupReferredService(serviceMetadata.getServiceKey());
consumerModel.setProxyObject(ref);
consumerModel.init(attributes);
initialized = true;
checkInvokerAvailable();
// dispatch a ReferenceConfigInitializedEvent since 2.7.4
dispatch(new ReferenceConfigInitializedEvent(this, invoker));
}
init()方法的代码较长,我们主要关注的是创建代理类的方法createProxy。
为方面后面的理解,通过dubug上面test的代码执行到createProxy(map)时的map内容如下:
2.2 createProxy
如上,从createProxy方法的代码实现中可以明确的看出有3种创建代理类的场景,也就是**injvm、端对端、注册中心**三种场景。在实际使用中主要的是用注册中心方式,上述test案例会通过单注册中心的方式创建。(1)injvm
一般在以下3种情况下,会选择injvm方式来创建代理引用类
- 通过为dubbo:reference配置injvm=true,该配置参数已建议删除
- 通过为dubbo:reference配置scope=local
- service export时在本地暴露接口
有一种场景要说明下,如果一个接口的提供者和消费者都在一个应用,消费者在引用是会默认走injvm方式,可以通过设置injvm=false或scope=remote来控制走注册中心的方式。
(2)端对端
端对端的应用场景是在联调测试阶段,可以通过配置指定的提供者地址来进行调用,无需通过注册中心来订阅提供者信息。
- 通过为dubbo:refenrece配置url,多个地址使用逗号分隔
参考如下配置:
<dubbo:reference id="xxxService" interface="com.alibaba.xxx.XxxService" url="dubbo://localhost:20890" />
(3)注册中心
在使用注册中心方式时,首先会检查注册中心配置的信息,然后使用一个list来保存注册中心的url,这里使用的是单注册中心,此处最终用于向注册中心订阅的url如下,refer的value会使用utf-8进行编码
-- 编码的url
registry://xx.xx.x.xx:2183/org.apache.dubbo.registry.RegistryService?application=consumer-demo&dubbo=2.0.2&pid=17120&refer=application%3Dconsumer-demo%26dubbo%3D2.0.2%26injvm%3Dfalse%26interface%3Dorg.apache.dubbo.config.api.DemoService%26methods%3DsayName%2CgetUsers%2Cecho%2CthrowDemoException%2CgetBox%26pid%3D17120%26register.ip%3D172.xx.xx.xx%26side%3Dconsumer%26sticky%3Dfalse%26timestamp%3D1661236826707®istry=zookeeper×tamp=1661236836715
-- 对应refer解码如下
refer=application=consumer-demo&dubbo=2.0.2&injvm=false&interface=org.apache.dubbo.config.api.DemoService&methods=sayName,getUsers,echo,throwDemoException,getBox&pid=17120®ister.ip=172.xx.xx.xx&side=consumer&sticky=false×tamp=1661236826707
经过分析可以将createProxy方法的代码简化如下,接下来就是看invoker的生成过程,最后生成代理代理类是以invoker为目标类。
// ReferenceConfig.java
private static final Protocol REF_PROTOCOL = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
private T createProxy(Map<String, String> map) {
if (shouldJvmRefer(map)) {
// (1)injvm
// 省略....
} else {
urls.clear();
if (url != null && url.length() > 0) {
// (2)端对端
// 省略...
} else {
// (3)注册中心
// 省略 构造注册中心urls...
if (urls.size() == 1) {
// 关键步骤
invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(0));
}else {
// 多注册中心
}
}
// ...
// create service proxy
return (T) PROXY_FACTORY.getProxy(invoker, ProtocolUtils.isGeneric(generic));
}
注意到在这里会使用dubbo的SPI扩展自适应获取Protocol的实现类,而refer方法是被注解@Adaptive修饰的。关于Dubbo的SPI可参见:dubbo.apache.org/zh/docs/v2.…。
根据Dubbo SPI实现的方式,此处的url为registry://...(完整数据见上面),所以在调用refer方法时会经过以下链路:
ProtocolFilterWrapper
ProtocolListenerWrapper
RegistryProtocol
其中,在ProtocolFilterWrapper和ProtocolListenerWrapper的refer方法中对注册中心的url都会直接跳过往下执行,所以最终执行的位置在RegistryProtocol中。
2.3 RegistryProtocol
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
url = getRegistryUrl(url);
// 选择注册中心
Registry registry = registryFactory.getRegistry(url);
if (RegistryService.class.equals(type)) {
return proxyFactory.getInvoker((T) registry, type, url);
}
// group="a,b" or group="*"
Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));
String group = qs.get(GROUP_KEY);
if (group != null && group.length() > 0) {
if ((COMMA_SPLIT_PATTERN.split(group)).length > 1 || "*".equals(group)) {
return doRefer(Cluster.getCluster(MergeableCluster.NAME), registry, type, url);
}
}
// 集群模式
Cluster cluster = Cluster.getCluster(qs.get(CLUSTER_KEY));
return doRefer(cluster, registry, type, url);
}
在RegistryProtocol.refer的过程中会根据注册中心url选择相应的注册中心,根据cluster配置选择集群模式,然后完成在注册中心的注册、订阅,并返回Invoker实例。
(1)注册中心
以此处测试的zookeeper注册中心为例,对应的url见上方。最终,对应的registry为ListenerRegistryWrapper包含ZookeeperRegistryFactory实例。
(2)集群模式
集群模式可通过配置cluster进行,可选failover/failfast/failsafe/failback/forking,默认是failover。此处对应的cluster为MockClusterWrapper包含FailoverCluster实例。
MockClusterWrapper
FailoverCluster
(3)doRefer
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);
// all attributes of REFER_KEY
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);
// 注册
registry.register(directory.getRegisteredConsumerUrl());
}
directory.buildRouterChain(subscribeUrl);
// 订阅
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;
}
如上doRefer代码所示,首先会创建RegisteyDerecroty,并将注册中心实例注入。以此处test为例,此时的subscribeUrl为
consumer://172.xx.xx.xx/org.apache.dubbo.config.api.DemoService?application=consumer-demo&dubbo=2.0.2&injvm=false&interface=org.apache.dubbo.config.api.DemoService&methods=sayName,getUsers,echo,throwDemoException,getBox&pid=15080&side=consumer&sticky=false×tamp=1661243789699
注册的过程是在zookeeper上创建一个临时节点
/dubbo
/org.apache.dubbo.config.api.DemoService
/consumers
/consumer://172.xx.xx.xx/org.apache.dubbo.config.api.DemoService?...
订阅的过程是在zookeeper上创建一个永久节点,并注册一个监听器,在此接口对应的节点数据发生变化时会notify到消费端。
/dubbo
/org.apache.dubbo.config.api.DemoService
/configurators
在订阅过程中,有一个很关键的过程是初始化RegistryDirectory的invokers属性,这个invokers在调用的过程中会真实用到。
2.4 RegistryDirectory的invokers生成
订阅过程在zookeeper上创建节点后会调用RegistryDirectory的notify方法(在zk节点变化时也会调用)。由于代码较多,下面给出调用的链路
RegistryDirectory#notify
RegistryDirectory#refreshOverrideAndInvoker
RegistryDirectory#refreshInvoker
RegistryDirectory#toInvokers
最终会定位到
invoker = new InvokerDelegate<>(protocol.refer(serviceType, url), url, providerUrl);
此处的serviceType
为接口org.apache.dubbo.config.api.DemoService
,url
为dubbo://172.xx.xx.xx:20880/org.apache.dubbo.config.api.DemoService?anyhost=true&application=consumer-demo&check=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&injvm=false&interface=org.apache.dubbo.config.api.DemoService&methods=sayName,getUsers,echo,throwDemoException,getBox&pid=26868®ister.ip=172.xx.xx.xx&release=&remote.application=consumer-demo&scope=local&side=consumer&sticky=false×tamp=1661246567095
。
InvokerDelegate为RegistryDirecroty的内部静态类,其会持有protocol.refer(serviceType, url)生成代理类,这个类就是最终调用时会用到的Invoker,其组成图大致如下:
ProtocolFilterWrapper
ListenerInvokerWrapper
AsyncToSyncInvoker
DubboInvoker
ProtocolFilterWrapper作为最外层会生成一个Filter调用链,具体代码在ProtocolFilterWrapper的buildInvokerChain中,最终生成的Invoker关系如下:
如上图所示,本质上是组成一个Invoker链,在消费端执行rpc调用时,首先会执行消费端的Filter链,然后再依次执行ListenerInvokerWrapper、AsyncToSyncInvoker、DubboInvoker。从源码中也可以看出,可通过配置ReferenceConfig的filter来配置消费端的Filter,像ConsumerContextFilter、FutureFilter、MonitorFilter都是默认激活的消费端的filter,如下为ConsumerContextFilter的注解配置信息:
@Activate(group = CONSUMER, order = -10000)
public class ConsumerContextFilter implements Filter {
}
注意,在这里的invokers是保存在RegistryDirectory实例中,同时invokers会保存在routerChain中,routerChain会保存在RegistryDirectory中,而RegistryDirectory的实例会最终传入到Invoker invoker = cluster.join(directory)中。
2.5 MockClusterWrapper
最终用于生成代理的Invoker的过程就在如下这行代码中:
Invoker<T> invoker = cluster.join(directory);
这里的cluster在上面的2.3中已说明,即为MockClusterWrapper包含了FailoverCluster的实例。
public class MockClusterWrapper implements Cluster {
private Cluster cluster;
public MockClusterWrapper(Cluster cluster) {
this.cluster = cluster;
}
@Override
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
return new MockClusterInvoker<T>(directory,
this.cluster.join(directory));
}
}
最终会返回MockClusterInvoker实例,而this.cluster.join(directory)可以继续跟进到FailoverCluster中,如下:
public abstract class AbstractCluster implements Cluster {
// 这一步生成Invoker很重要
private <T> Invoker<T> buildClusterInterceptors(AbstractClusterInvoker<T> clusterInvoker, String key) {
AbstractClusterInvoker<T> last = clusterInvoker;
List<ClusterInterceptor> interceptors = ExtensionLoader.getExtensionLoader(ClusterInterceptor.class).getActivateExtension(clusterInvoker.getUrl(), key);
if (!interceptors.isEmpty()) {
for (int i = interceptors.size() - 1; i >= 0; i--) {
final ClusterInterceptor interceptor = interceptors.get(i);
final AbstractClusterInvoker<T> next = last;
last = new InterceptorInvokerNode<>(clusterInvoker, interceptor, next);
}
}
return last;
}
@Override
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
return buildClusterInterceptors(doJoin(directory), directory.getUrl().getParameter(REFERENCE_INTERCEPTOR_KEY));
}
}
其中doJoin会返回FailoverClusterInvoker,然后通过buildClusterInterceptors,生成一个拦截器链,在此链中FailoverClusterInvoker处于最后一个节点。
2.6 生成代理类
最后,回到ReferenceConfig#createProxy方法的最后一行将对上面生成的MockClusterInvoker生成代理类
private static final ProxyFactory PROXY_FACTORY = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
private T createProxy(Map<String, String> map) {
// 省略
return (T) PROXY_FACTORY.getProxy(invoker, ProtocolUtils.isGeneric(generic));
}
通过ProxyFactory的实现类来完成动态代理类的生成,有javassist和jdk两种生成方式,可以通过proxy参数进行配置或设置,默认是使用javassist的方式生成,最终使用InvokerInvocationHandler来持有invoker。
三、总结
对代理类的生成过程进行了解后,才能更好的了解整个消费端在进行rpc调用过程细节,包括怎么去自定义扩展实现拦截器、过滤器;还能更好的了解路由选择、负载均衡、集群容错的方式。下图为消费端调用类链路图: