系列文章目录
【收藏向】从用法到源码,一篇文章让你精通Dubbo的SPI机制
面试Dubbo ,却问我和Springcloud有什么区别?
超简单,手把手教你搭建Dubbo工程(内附源码)
Dubbo最核心功能——服务暴露的配置、使用及原理
不满足于RPC,Dubbo是如何做服务引用的
- 系列文章目录
- 前言
- 一、服务引用的介绍
- 二、配置与模型
- 1. 服务引用配置
- 2. 整体实现模型
- 三. 引用的具体实现
- 1. 触发创建代理对象
- 2. 创建代理的实现
- 3. 代理的获取
- 四、小结
前言
我们在Dubbo与SpringCloud对比的那一期就说过,Dubbo并不满足于作为RPC框架,除了RPC所需要的内容,还提供了如服务治理等额外的支持。但是无奈,很多人对Dubbo的印象依旧停留在RPC框架。所以,我们在学习的初期,我们先不急着学习那些分支内容,还是专注其核心,即围绕RPC循序渐进的学习。那么上一篇我们讲了服务暴露,今天就一起来学习RPC桥梁的另一端:消费者,即服务的引用。当然,在此之前,还是希望大家带着问题来学习,比如说:
服务引用在底层替我们做了什么?
Spring怎么和服务引用融合的?
📕作者简介:战斧,多年开发及管理经验,爱好广泛,致力于创作更多高质量内容
📗本文收录于 Dubbo专栏,有需要者,可直接订阅专栏实时获取更新
📘高质量专栏 RabbitMQ、Spring全家桶 等仍在更新,欢迎指导
📙Zookeeper Redis kafka docker netty等诸多框架,以及架构与分布式专题即将上线,敬请期待
一、服务引用的介绍
在JAVA中,引用的概念是一直贯穿的。大部分情况,我们都是进行着对象引用。但是通过类比,我们不难得出服务引用的概念:服务引用是指在程序中使用另一个程序或服务提供的功能或数据的方式。通过服务引用,程序可以调用其他程序或服务提供的功能,获取返回值并将其用于自己的业务逻辑中
二、配置与模型
1. 服务引用配置
同服务暴露一样,实现服务引用的可以通过XML文件和注解两种途径来实现
XML 文件:
<dubbo:reference interface="com.zhanfu.dubbo.demo.dubbo.api.DemoService" timeout="2000"/>
或者 @DubboReference
注解
@Component
public class Consumer implements CommandLineRunner {
@DubboReference(timeout = 2000)
private DemoService demoService;
@Override
public void run(String... args) throws Exception {
String result = demoService.sayHello("战斧");
System.out.println("收到消息:" + result);
}
}
2. 整体实现模型
如果我们做了上面的配置,那么它是如何实现调用远方或本地的服务的呢?我们先简单概括一下:Dubbo 为我们的引用创建了一个代理对象
,这个代理对象如同一个电话筒,当我们对电话筒说话的时候,它会将我们的话传到指定的人那里。同理,当我们调用这个代理对象时,它会把我们的调用请求传递到服务提供方那里,获得结果后,再反馈给我们,当然,这里仅仅是将其简单化成一个代理对象,实际其结合了容错策略后,实现还是很复杂的
三. 引用的具体实现
如同前几期的一样,我们今天仍然以与Spring框架结合下的Dubbo进行分析
1. 触发创建代理对象
服务引用的操作其实和服务暴露
一样,因此把上次服务暴露的图稍微改下各位就明白了
与服务暴露仅有后置处理器不同:
服务暴露的工厂后置处理器为ServiceAnnotationPostProcessor ,而现在服务引用的工厂后置处理器为ReferenceAnnotationBeanPostProcessor,当然,其扫描的注解也有不同,服务引用扫描的注解为
- org.apache.dubbo.config.annotation.DubboReference
- org.apache.dubbo.config.annotation.Reference
- com.alibaba.dubbo.config.annotation.Reference
至于容器刷新监听,其实都是由 DubboDeployApplicationListener 进行监听,最终也都会落在在同一个方法里
onContextRefreshedEvent -> deployer.start() -> startSync()
// DefaultModuleDeployer.class
private synchronized Future startSync() throws IllegalStateException {
if (isStopping() || isStopped() || isFailed()) {
throw new IllegalStateException(getIdentifier() + " is stopping or stopped, can not start again");
}
try {
......
// 服务暴露
exportServices();
......
// 引用服务
referServices();
......
} catch (Throwable e) {
onModuleFailed(getIdentifier() + " start failed: " + e, e);
throw e;
}
return startFuture;
}
接下来,就让我们专注于真正的引用服务
方法 DefaultModuleDeployer.referServices()
// DefaultModuleDeployer
private void referServices() {
configManager.getReferences().forEach(rc -> {
try {
ReferenceConfig<?> referenceConfig = (ReferenceConfig<?>) rc;
if (!referenceConfig.isRefreshed()) {
referenceConfig.refresh();
}
// 每一个rc 其实就是一个引用配置,比如我们在DemoService上加了@DubboReference注解,就会被解析成一个引用配置,形如
// <dubbo:reference sticky="false" timeout="2000" id="demoService" interface="com.zhanfu.dubbo.demo.dubbo.api.DemoService" />
if (rc.shouldInit()) {
if (referAsync || rc.shouldReferAsync()) {
ExecutorService executor = executorRepository.getServiceReferExecutor();
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
referenceCache.get(rc);
} catch (Throwable t) {
logger.error(CONFIG_FAILED_EXPORT_SERVICE, "", "", "Failed to async export service config: " + getIdentifier() + " , catch error : " + t.getMessage(), t);
}
}, executor);
asyncReferringFutures.add(future);
} else {
// 从引用缓存中尝试获取该引用配置的代理对象,注意此处仅仅执行该方法,并没有获取其返回值
referenceCache.get(rc);
}
}
} catch (Throwable t) {
logger.error(CONFIG_FAILED_REFERENCE_MODEL, "", "", "Model reference failed: " + getIdentifier() + " , catch error : " + t.getMessage(), t);
referenceCache.destroy(rc);
throw t;
}
});
}
上文中的 referenceCache 其实是一个标注了 final 的 SimpleReferenceCache 对象,其储存着所有的引用代理的情况,需要注意的是,真正的代理对象并不存储在它这里。
public <T> T get(ReferenceConfigBase<T> rc) {
// 为该配置生成key, 即接口的全限定名,此处就是com.zhanfu.dubbo.demo.dubbo.api.DemoService
String key = generator.generateKey(rc);
// 接口的类型 com.zhanfu.dubbo.demo.dubbo.api.DemoService.calss
Class<?> type = rc.getInterfaceClass();
boolean singleton = rc.getSingleton() == null || rc.getSingleton();
T proxy = null;
// 单例类才需要从缓存取,不然每次都得新取一个
if (singleton) {
proxy = get(key, (Class<T>) type);
} else {
logger.warn(CONFIG_API_WRONG_USE, "", "", "Using non-singleton ReferenceConfig and ReferenceCache at the same time may cause memory leak. " +
"Call ReferenceConfig#get() directly for non-singleton ReferenceConfig instead of using ReferenceCache#get(ReferenceConfig)");
}
// 第一次或者不是单例,创建代理信息
if (proxy == null) {
// 代理对象并不存储,存储的是引用与引用配置的映射
List<ReferenceConfigBase<?>> referencesOfType = referenceTypeMap.computeIfAbsent(type, _t -> Collections.synchronizedList(new ArrayList<>()));
referencesOfType.add(rc);
List<ReferenceConfigBase<?>> referenceConfigList = referenceKeyMap.computeIfAbsent(key, _k -> Collections.synchronizedList(new ArrayList<>()));
referenceConfigList.add(rc);
// 真正创建代理的地方,还是由引用配置对象自己来实现的
proxy = rc.get();
}
return proxy;
}
至此,结合Spring的项目,我们对服务引用的准备就完成了,接下来就是其核心内容——代理的创建
2. 创建代理的实现
紧接上文,我们看到了 rc.get() ,其实这里就是真正创建代理的地方
// ReferenceConfig.class
public T get() {
if (destroyed) {
throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");
}
if (ref == null) {
// 确保模块已启动,兼容老版本
getScopeModel().getDeployer().start();
synchronized (this) {
if (ref == null) {
init();
}
}
}
return ref;
}
init() 方法内容较多,我们仅看关键的一行,此处的referenceParameters包含了引用所需要的各种信息
ref = createProxy(referenceParameters);
那么我们再进入 createProxy 一探究竟
ProxyFactory proxyFactory = this.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
private T createProxy(Map<String, String> referenceParameters) {
// 创建本地调用器
if (shouldJvmRefer(referenceParameters)) {
createInvokerForLocal(referenceParameters);
} else {
urls.clear();
meshModeHandleUrl(referenceParameters);
if (StringUtils.isNotEmpty(url)) {
// user specified URL, could be peer-to-peer address, or register center's address.
parseUrl(referenceParameters);
} else {
// if protocols not in jvm checkRegistry
if (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())) {
aggregateUrlFromRegistry(referenceParameters);
}
}
// 创建远程调用器
createInvokerForRemote();
}
URL consumerUrl = new ServiceConfigURL(CONSUMER_PROTOCOL, referenceParameters.get(REGISTER_IP_KEY), 0,
referenceParameters.get(INTERFACE_KEY), referenceParameters);
consumerUrl = consumerUrl.setScopeModel(getScopeModel());
consumerUrl = consumerUrl.setServiceModel(consumerModel);
// 发布订消费者元信息,注册到注册中心上
MetadataUtils.publishServiceDefinition(consumerUrl, consumerModel.getServiceModel(), getApplicationModel());
// 创建引用代理,此处的 proxyFactory 是Dubbo 的SPI接口,不是Spring提供的那个类
return (T) proxyFactory.getProxy(invoker, ProtocolUtils.isGeneric(generic));
}
可以看到这又是一个自适应的代理工厂类,那么其具体采用那个实现类,就要看入参了,此刻我们采用的远程服务,使用的MigrationInvoker ,最终使用的是默认的 ”javassist“ ,即 org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory 实现类,并最终拼接成操作一个代理类,并将其实例化
这中间通过javaassit生成的过程十分繁杂,我们直接将生成的代理进行反编译,其代码如下
class DemoServiceDubboProxy0
{
public DemoServiceDubboProxy0();
public DemoServiceDubboProxy0(java.lang.reflect.InvocationHandler);
public void $destroy();
public java.lang.String sayHello(java.lang.String);
public java.lang.Object $echo(java.lang.Object);
public static [Ljava.lang.reflect.Method; methods;
private java.lang.reflect.InvocationHandler handler;
}
因此,我们知道了,代理内其实还是一个 MigrationInvoker 在执行操作,但是我们现在并不看其内部在干什么,留待讲调用的章节再来详谈,因此点到为止,完成了一个代理对象的创建。
3. 代理的获取
完成了代理对象的创建,工作并没有结束,我们在业务代码中,还需要获取代理。当然在Spring框架下,更贴切的叫法叫做注入。把代理对象注入我们业务代码中。如下图
我们首先要知道的是通过xml 或@DubboReference
注解标志的接口,最终会被解析成一个 ReferenceBean 对象。
public class ReferenceBean<T> implements FactoryBean<T>,
ApplicationContextAware, BeanClassLoaderAware, BeanNameAware, InitializingBean, DisposableBean
因为其实现了FactoryBean 接口,所以我们应当能猜到重点并不是其本身,而是通过它的 getObject() 方法能获得什么东西。 (不了解FactoryBean 的看这里: BeanFactory 和 FactoryBean 的关联与区别)
public T getObject() {
if (lazyProxy == null) {
createLazyProxy();
}
return (T) lazyProxy;
}
果不其然,我们想要的东西很快就出现了,那就是代理对象的创建,我们关注到,这里其实提供的是一个惰性代理
,这种设计是为了防止一些Bean过早产生,因为当时它的配置和环境未必已经准备完成,此时直接初始化该Bean可能会出现各种问题。(其实我们在开发中,也有很多时候会使用@Lazy
注解来达到类似的目的)那么这个代理是如何创建的呢?
private void createLazyProxy() {
// 使用的是Spring提供的代理工厂
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTargetSource(new DubboReferenceLazyInitTargetSource());
proxyFactory.addInterface(interfaceClass);
// 代理还需要实现一些额外的接口,此处是 EchoService.class, Destroyable.class
Class<?>[] internalInterfaces = AbstractProxyFactory.getInternalInterfaces();
for (Class<?> anInterface : internalInterfaces) {
proxyFactory.addInterface(anInterface);
}
if (!StringUtils.isEquals(interfaceClass.getName(), interfaceName)) {
//add service interface
try {
Class<?> serviceInterface = ClassUtils.forName(interfaceName, beanClassLoader);
proxyFactory.addInterface(serviceInterface);
} catch (ClassNotFoundException e) {
// generic call maybe without service interface class locally
}
}
// 创建代理
this.lazyProxy = proxyFactory.getProxy(this.beanClassLoader);
}
我们这里要注意一个重点,我们为 proxyFactory 代理工厂设置了一个目标对象 DubboReferenceLazyInitTargetSource,熟悉 SpringAop 的朋友应该知道 proxyFactory 的目标对象就是真正执行方法的对象,代理只是在调用该对象的前后加入了一些操作。那么我们来看看 DubboReferenceLazyInitTargetSource 有什么特殊之处
// ReferenceBean 的内部类
private class DubboReferenceLazyInitTargetSource extends AbstractLazyCreationTargetSource {
@Override
protected Object createObject() throws Exception {
return getCallProxy();
}
@Override
public synchronized Class<?> getTargetClass() {
return getInterfaceClass();
}
}
我们可以看到该对象最大的特点是其实现了 AbstractLazyCreationTargetSource ,两个方法也是重写的父类的。限于主题与篇幅,我们此处不针对该父类详细展开,仅对其作用进行描述:
AbstractLazyCreationTargetSource是 Spring Framework 中的一个抽象类,它实现了
TargetSource 接口。它的主要作用是作为一个懒加载的代理对象,在第一次调用它时,通过回调方法创建并缓存目标对象。
换而言之,它就是一个临时工,正主有事它先占着位置,等到真正需要工作时它再去叫正主,换句话说,如果我们仅是获取代理,里面的目标对象就是这个,但是如果我们调用了代理的方法,它就会通过createObject方法来叫真正的对象,我们可以看到,它是通过getCallProxy来叫真正的对象
private Object getCallProxy() throws Exception {
// 惰性代理的作用,只有当环境与配置都准备完毕,才会真正产生可用的代理目标
if (referenceConfig == null) {
throw new IllegalStateException("ReferenceBean is not ready yet, please make sure to call reference interface method after dubbo is started.");
}
// 对象的创建需要给容器上锁,放置死锁
synchronized (((DefaultSingletonBeanRegistry)getBeanFactory()).getSingletonMutex()) {
return referenceConfig.get();
}
}
// ReferenceConfig ,即引用配置
public T get() {
if (destroyed) {
throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");
}
if (ref == null) {
// ensure start module, compatible with old api usage
getScopeModel().getDeployer().start();
synchronized (this) {
if (ref == null) {
init();
}
}
}
return ref;
}
到这里,我们终于看到了返回的代理对象,即上文里的ref,而ref的诞生,我们在上一个小节,创建代理对象里已经讲过了。至此,业务代码如何获取的代理我们也看完了。
四、小结
通篇下来,我们看了大量的源码,并辅以文字解释,总算讲完了Dubbo是如何做服务引用的,一句话概括就是使用了代理,但是这个代理的设计与实现却很复杂,总结有三个原因:
全面的SPI自适应设计,使得代理工厂有好几个可选择
本地或者远程,会创建不同的invoker,即调用器
为了和Spring配合,使用了惰性代理避免过早的初始化而出错
目前我们已经把服务引用和服务暴露都讲了,虽然还有大量的细节有待补充,但其核心流程已经说完,相当于一座大桥的两头我们已经搭好,下一次,我们将进行这座大桥的合拢————即看看服务调用