不积跬步,无以至千里;不积小流,无以成江河。
干了四五年Java开发,总想写一些自己感觉上档次的文章,越是这样越是无处下手,到现在一遍自己的博客都没写过。所以从现在开始,从技术最基础的地方出发,记录下自己的学习经历和一点点心得。本文将通过官方资料和阅读源码来讲述我自己对dubbo的理解。一则记录自己学习的成果,再则帮助更多初学者少走弯路。有错误之处,还望各位指正 。
本文使用目前dubbo的最新版本2.7.5为基础,Java版本是1.8.212。后续关于dubbo相关的文章也全部是使用这个版本。
Apache Dubbo 学习总结
- 1 基础架构
- 2 SPI原理
- 2.1 JAVA SPI
- 2.2 Dubbo SPI
- 2.2.1 SPI扩展点的配置
- 2.2.2 SPI扩展点的分类
- 2.2.3 SPI 扩展点的四大特性
- 2.2.4 SPI扩展点的缓存
- 2.3 ExtensionLoader原理解析
- 2.4 ExtensionLoader相关技术源码解析
- 3 整体设计原理
- 3.1 总体设计
Apache Dubbo™是基于Java的高性能开源RPC框架。Dubbo提供了三个关键功能,基于接口的远程调用,容错和负载平衡以及自动服务注册和发现。
1 基础架构
第一节是根据官方介绍翻译而来,熟悉的朋友直接跳过!
也可以直接看Dubbo中文网址:http://dubbo.apache.org/zh-cn/docs/user/preface/architecture.html
这是Dubbo官方的架构图:
先对这几个角色进行简要说明:
- Provider 服务提供者;
- Consumer 服务消费者,消费provider提供的服务;
- Registry 注册中心,服务的注册和发现以及配置;
- Monitor 监视器,负责监控服务的调用的次数和时间等;
- Container 服务容器,负责服务的生命周期;
架构图中角色之间的调用流程
- 先启动注册中心 ;
- 通过Container或者单独启动Provider, 启动后会把自己的元数据信息注册到Register上 ;
- 启动Consumer ,启动后会从Register上订阅所有的服务,然后把自己的元数据也注册到Register ;
- 启动Monitor ,启动后会从Register上读取Consumer和Provider的元数据信息 ;
- Register将提供者列表返回给消费者,当Provider有更改时,将通过长连接将更改后的数据推送到消费者;
- 当Consumer调用Provider的服务之前,会使用负载均衡算法确定调用哪一个Provider的服务,如果调用失败会选择其他的可用的Provider ;
- Consumer和Provider都将计算服务调用的数量和在内存中所花费的时间,并将统计信息发送给Monitor;
这是未来的架构图
2 SPI原理
SPI是dubbo实现的基石,只有完全理解SPI的工作原理,才能更好的掌握Dubbo内部工作原理,这一节我将花大力气研究这个原理。
2.1 JAVA SPI
SPI 全称是Service Provider Interface。在Java中SPI是被用来设计给服务提供商做插件使用的
SPI是基于策略模式 来实现动态加载的机制 。我们在程序只定义一个接口,具体的实现交个不同的服务提供者;在程序启动的时候,读取配置文件,由配置确定要调用哪一个实现;
举个例子:在Java程序中想操作数据库,由于数据库有关系型数据和非关系型数据库,在关系型的数据库中又有Oracle,mysql,sqlServer等。那在程序中如何与他们建立连接呢?
在java.sql包中就定义了一个总的接口:Driver.java 假如要使用mysql数据库, 就需要导入MySQL的驱动包mysql-connector-java-5.1.46.jar
以下为Java中Driver这个SPI的源码
package java.sql;
import java.util.logging.Logger;
/**
* The interface that every driver class must implement.
*/
public interface Driver {
Connection connect(String url, java.util.Properties info)
throws SQLException;
boolean acceptsURL(String url) throws SQLException;
DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
throws SQLException;
int getMajorVersion();
int getMinorVersion();
boolean jdbcCompliant();
public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}
在mysql-connector-java-5.1.46.jar包中会有MySQL提供的实现com.mysql.jdbc.Driver,这个在目录中的配置文件中指定java.sql.Driver连接MySQL的实现:如果有多个实现,就用分行符分开,如下所示:
这个是java.mysql.jdbc.Driver的源码,具体实现逻辑在其父类NonRegisteringDriver中:
package com.mysql.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
在Java中实现这个核心类为:java.util.ServiceLoader ;
这是部ServiceLoader的部分代码,这里配置文件默认的路径前缀是:META-INF/services/
这是ServiceLoader中加载配置的地方,需要详细源码可以自己去JDK源码中查看,这里不做赘述。
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//配置文件的全名为:默认前缀+SPI接口的全限定名,如:META-INF/services/java.sql.Driver
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
使用ServiceLoader加载个服务提供商的实现的简单实现如下:
public class TestM {
public static void main(String[] args) {
//获取所有Driver的扩展点,这个在实际中有点浪费资源
ServiceLoader<Driver> driverServiceLoader = ServiceLoader.load(Driver.class);
driverServiceLoader.forEach(driver ->{
//打印当前我的项目中可用的所有服务商的Driver实现
System.out.println(driver.getClass().getName());
});
}
}
//输出结果,这是我的项目,具体会有多少个Driver,要看自己导入了多少个服务商jar包
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
com.alibaba.druid.proxy.DruidDriver
com.alibaba.druid.mock.MockDriver
SPI主要用于框架扩展和替换组件。一个好的开源框架,必须要留一些扩展点,让使用者可以自定义某一些组件,而这个过程要做到黑盒扩展。
2.2 Dubbo SPI
Dubbo SPI和Java SPI其实很类似,但是Dubbo SPI做出了很多改进和优化,并且Dubbo的SPI接口都会使用@SPI注解标识,这是Dubbo自己的注解。
源码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {
/**
* default extension name
* 默认实现类的名字。配置在去路径接口名称文件中的key
*/
String value() default "";
}
在Dubbo框架中,@SPI只是用在接口上,它的主要作用就是标记这个接口是一个SPI接口,可以有多个不同的内置的或者用户自己的实现,然后在运行的时候,@SPI中的参数作为实现类的名称来查找具体的实现类。
举个例子:
以下是dubbo序列化的SPI接口,首先在接口上使用@SPI标记,然后注解中传入了一个参数hessian2 。使用这个参数来确定Serialization接口的默认实现的名称是hessian2。用户也可以通过dubbo.protocol.serialization=xxx来指定自己想要的实现,如果系统中没有你指定的实现,你可以自行实现一个。非常灵活!
/**
* Serialization strategy interface that specifies a serializer. (SPI, Singleton, ThreadSafe)
*
* The default extension is hessian2 and the default serialization implementation of the dubbo protocol.
* <pre>
* e.g. <dubbo:protocol serialization="xxx" />
* </pre>
*/
@SPI("hessian2")
public interface Serialization {
/**
* Get content type unique id, recommended that custom implementations use values different with
* any value of {@link Constants} and don't greater than ExchangeCodec.SERIALIZATION_MASK (31)
* because dubbo protocol use 5 bits to record serialization ID in header.
*
* @return content type id
*/
byte getContentTypeId();
/**
* Get content type
*
* @return content type
*/
String getContentType();
/**
* Get a serialization implementation instance
*
* @param url URL address for the remote service
* @param output the underlying output stream
* @return serializer
* @throws IOException
*/
@Adaptive
ObjectOutput serialize(URL url, OutputStream output) throws IOException;
/**
* Get a deserialization implementation instance
*
* @param url URL address for the remote service
* @param input the underlying input stream
* @return deserializer
* @throws IOException
*/
@Adaptive
ObjectInput deserialize(URL url, InputStream input) throws IOException;
}
这是hessian2的里面的具体配置,这也是dubbo的默认的内置实现:
Dubbo SPI相对于JAVA SPI的新特性:
- 扩展点的加载
Java SPI在加载扩展点的时候,会一次性加载所有可用的扩展点,如上1.2.1中所示,我只需要MySQL的jdbc扩展,结果会查询很多我不需要的。因此,会浪费系统资源; - 加载扩展点异常信息
Java SPI加载失败是,可以能会导致异常信息丢失,导致追踪问题很困难;Dubbo SPI在加载扩展点失败时会先抛出真是异常,并打印日志,某一个扩展点加载失败也不会影响其他扩展点和整个框架的使用。 - 对扩展点IoC和AOP的支持
Dubbo SPI加载扩展点的时候,会为扩展点中所有的setter属性(非基础类型属性)注入响应的实例。然后,Dubbo SPI只是加载配置文件中的类到系统的缓存中,不会立即初始化所有扩展点,实现懒加载。这个会更节约系统资源,提高系统的性能。
现在来敲重点,Dubbo SPI的核心实现是:org.apache.dubbo.common.extension.ExtensionLoader
2.2.1 SPI扩展点的配置
/**
* synchronized in getExtensionClasses
* */
private Map<String, Class<?>> loadExtensionClasses() {
cacheDefaultExtensionName();
Map<String, Class<?>> extensionClasses = new HashMap<>();
// internal extension load from ExtensionLoader's ClassLoader first
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName(), true);
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"), true);
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
return extensionClasses;
}
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {
loadDirectory(extensionClasses, dir, type, false);
}
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst) {
//配置文件的全称:路径前缀+接口的全限定名 ,这个与Java SPI是一直的算法
String fileName = dir + type;
try {
Enumeration<java.net.URL> urls = null;
ClassLoader classLoader = findClassLoader();
// try to load from ExtensionLoader's ClassLoader first
if (extensionLoaderClassLoaderFirst) {
ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
urls = extensionLoaderClassLoader.getResources(fileName);
}
}
if(urls == null || !urls.hasMoreElements()) {
if (classLoader != null) {
urls = classLoader.getResources(fileName);
} else {
urls = ClassLoader.getSystemResources(fileName);
}
}
if (urls != null) {
while (urls.hasMoreElements()) {
java.net.URL resourceURL = urls.nextElement();
loadResource(extensionClasses, classLoader, resourceURL);
}
}
} catch (Throwable t) {
logger.error("Exception occurred when loading extension class (interface: " +
type + ", description file: " + fileName + ").", t);
}
}
由源码可以看出:
- Dubbo SPI配置文件存放的路径是: META-INF/services/ , META-INF/dubbo/,META-INF/dubbo/internal/
- 配置文件的名称为接口的全路径名
- 配置文件使用key=value的方式,多个就用换行符分割,key是扩展点在Dubbo/spring Ioc容器Ioc容器中的名称,value是扩展点类的全称
2.2.2 SPI扩展点的分类
①. 普通扩展类
在SPI配置文件中配置的扩展类
②. 包装扩展类
这个wrapper类没有具体的实现,它的核心功能就是包装扩展点,实现扩展点的通用逻辑;这个wrapper类的必须在构造发放中传入一个响应的扩展点的实现;
③. 自适应扩展类
一个SPI接口会有多个扩展点,具体使用哪一扩展点可以不用在配置或代码中写死。可以在程序运行时,通过URL[^1]中的某些参数动态来确定。这个也是dubbo最棒的特性之一!
[^1] :在源码org.apache.dubbo.common.URL中 ;官方解释Uniform Resource Locator (Immutable, ThreadSafe) 。
2.2.3 SPI 扩展点的四大特性
- 自动包装类
自动包装类使用装饰器模式,通过对原始类包装或者说是增强,在包装类中抽象出通用的逻辑,让被包装的类能更专注于业务具体的实现。
如何判断一个类是否是Wrapper类,在ExtensionLoader#isWrapperClass 的实现如下:
/**
* test if clazz is a wrapper class
* <p>
* which has Constructor with given class type as its only argument
*
* 如果扩展点实现的构造函数中只有一个参数,且这个参数的类型为它本身时,这个扩展类会被认为是wrapper类
*/
private boolean isWrapperClass(Class<?> clazz) {
try {
clazz.getConstructor(type);
return true;
} catch (NoSuchMethodException e) {
return false;
}
}
在Dubbo中包装类都是以Wrapper结尾,如下随即列一个源码:
/**
* ListenerProtocol
*/
public class ProtocolFilterWrapper implements Protocol {
private final Protocol protocol;
//在构造参数中传入一个自己的实例
public ProtocolFilterWrapper(Protocol protocol) {
if (protocol == null) {
throw new IllegalArgumentException("protocol == null");
}
this.protocol = protocol;
}
private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
...
return last;
}
@Override
public int getDefaultPort() {
return protocol.getDefaultPort();
}
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
if (UrlUtils.isRegistry(invoker.getUrl())) {
return protocol.export(invoker);
}
return protocol.export(buildInvokerChain(invoker, SERVICE_FILTER_KEY, CommonConstants.PROVIDER));
}
@Override
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
if (UrlUtils.isRegistry(url)) {
return protocol.refer(type, url);
}
return buildInvokerChain(protocol.refer(type, url), REFERENCE_FILTER_KEY, CommonConstants.CONSUMER);
}
@Override
public void destroy() {
protocol.destroy();
}
@Override
public List<ProtocolServer> getServers() {
return protocol.getServers();
}
}
这就是一个典型的Wrapper类,它本身实现 Protocol 接口,然后它的构造函数中又必须传入一个Protocol 实例;它里面实现的Protocol 的方法里面实际调用的是构造函数中传如的Protocol 实例的实现;
- 自动加载
假如一个扩展点里面的成员属性是另外一个扩展点,并且这个属性有setter方法;那么Dubbo会自动注入对应的扩展点的实例;这个实现在ExtensionLoader#injectExtension(T instance)方法中实现,详细实现如下:
private T injectExtension(T instance) {
if (objectFactory == null) {
return instance;
}
try {
/**
* 实现IOC和AOP的机制,获取到类中所有的setter方法。
* 1 如果setter方法不是私有的和基础数据类型,
* 并且没有注解{@link DisableInject}注解,就自动给相关属性注入默认的实现类
* 2 如果setter方法属性是一个接口,并且次接口有多个实现类,则会更加{@link Adaptive}注解
* 自适应加载配置的默认实现
*/
for (Method method : instance.getClass().getMethods()) {
if (!isSetter(method)) {
continue;
}
/**
* Check {@link DisableInject} to see if we need auto injection for this property
*/
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
Class<?> pt = method.getParameterTypes()[0];
if (ReflectUtils.isPrimitives(pt)) {
continue;
}
try {
String property = getSetterProperty(method);
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("Failed to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
到这里还会有一个问题,假如这个扩展点的属性存在多个实现怎么办? 这个就由下面这个自适应特性来实现。
- 自适应
在Dubbo中,自适应的接口方法或接口实现会使用@Adaptive标识。
- 标注接口方法时, 通过URL中的参数来动态的决定要使用哪一个具体的实现(方法中本身就有URL对象,或者调用的时候获取调用的URL对象来进行匹配)。
- 标注扩展点实现时,会默认这个扩展点实现就是这个接口的实现
直接上源码
注解@Adaptive的源码(注意到,只有一个参数,是一个字符串数据),如果英文水平好,直接看注解就知道了其中的实现原理,具体的实现在ExtensionLoader#injectExtension中有体现:
/**
* Provide helpful information for {@link ExtensionLoader} to inject dependency extension instance.
*
* @see ExtensionLoader
* @see URL
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
/**
* Decide which target extension to be injected. The name of the target extension is decided by the parameter passed
* in the URL, and the parameter names are given by this method.
* <p>
* If the specified parameters are not found from {@link URL}, then the default extension will be used for
* dependency injection (specified in its interface's {@link SPI}).
* <p>
* For example, given <code>String[] {"key1", "key2"}</code>:
* <ol>
* <li>find parameter 'key1' in URL, use its value as the extension's name</li>
* <li>try 'key2' for extension's name if 'key1' is not found (or its value is empty) in URL</li>
* <li>use default extension if 'key2' doesn't exist either</li>
* <li>otherwise, throw {@link IllegalStateException}</li>
* </ol>
* If the parameter names are empty, then a default parameter name is generated from interface's
* class name with the rule: divide classname from capital char into several parts, and separate the parts with
* dot '.', for example, for {@code org.apache.dubbo.xxx.YyyInvokerWrapper}, the generated name is
* <code>String[] {"yyy.invoker.wrapper"}</code>.
*
* @return parameter names in URL
*/
String[] value() default {};
}
使用样例代码,这里使用Codec2接口,这是Dubbo消息编码解码的核心接口。
package org.apache.dubbo.remoting;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;
import org.apache.dubbo.remoting.buffer.ChannelBuffer;
import java.io.IOException;
/**
* Dubbo编解码器顶层抽象接口
*/
@SPI
public interface Codec2 {
@Adaptive({Constants.CODEC_KEY})
void encode(Channel channel, ChannelBuffer buffer, Object message) throws IOException;
@Adaptive({Constants.CODEC_KEY})
Object decode(Channel channel, ChannelBuffer buffer) throws IOException;
enum DecodeResult {
NEED_MORE_INPUT, SKIP_SOME_INPUT
}
}
SPI接口使用@SPI注解,然后接口方法使用@Adaptive注解,这里面只传了一个参数;
当要获取Codec2的扩展点时,
首先会从URL的参数中找到key为这参数(Constants.CODEC_KEY)的值value,然后以这个value作为扩展点的实例的名称去加载这个实例。
具体实现在org.apache.dubbo.remoting.transport.AbstractEndpoint#getChannelCodec(URL url)中有体现:
protected static Codec2 getChannelCodec(URL url) {
//从URL参数中,以Constants.CODEC_KEY去找对应的值,最为默认实现类的名称
String codecName = url.getParameter(Constants.CODEC_KEY, "telnet");
if (ExtensionLoader.getExtensionLoader(Codec2.class).hasExtension(codecName)) {
return ExtensionLoader.getExtensionLoader(Codec2.class).getExtension(codecName);
} else {
return new CodecAdapter(ExtensionLoader.getExtensionLoader(Codec.class).getExtension(codecName));
}
}
假如@Adaptive中有多个参数时,首先会以第一个参数作为key去查找,如果没有找到对应的实现类,就以第二个参数作为key去找,以此类推…
这个特性虽然在使用的过程中非常灵活,但是,对于一个接口,只能激活一个实现。假如一个接口需要同时激活多个扩展点,这个就显得有点不够用了!因此dubbo还提供了下面这个特性自动激活来实现这个业务场景
- 自动激活
在dubbo中,使用@Activate注解来标识扩展点的实现类。先看注解源码:
/**
* @Activate,是一个用于自动激活给定条件的某些扩展点实现的注解
* 例子: 当Filter有多个实现时,@Activate可以用来加载某些Filter扩展
* <ol>
* <li>{@link Activate#group()} specifies group criteria. Framework SPI defines the valid group values.
* <li>{@link Activate#value()} specifies parameter key in {@link URL} criteria.
* </ol>
* SPI provider can call {@link ExtensionLoader#getActivateExtension(URL, String, String)} to find out all activated
* extensions with the given criteria.
*
* @see SPI
* @see URL
* @see ExtensionLoader
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Activate {
/**
* Activate the current extension when one of the groups matches. The group passed into
* {@link ExtensionLoader#getActivateExtension(URL, String, String)} will be used for matching.
*
* @return group names to match
* @see ExtensionLoader#getActivateExtension(URL, String, String)
*/
String[] group() default {};
/**
* Activate the current extension when the specified keys appear in the URL's parameters.
* <p>
* For example, given <code>@Activate("cache, validation")</code>, the current extension will be return only when
* there's either <code>cache</code> or <code>validation</code> key appeared in the URL's parameters.
* </p>
*
* @return URL parameter keys
* @see ExtensionLoader#getActivateExtension(URL, String)
* @see ExtensionLoader#getActivateExtension(URL, String, String)
*/
String[] value() default {};
/**
* Relative ordering info, optional
* Deprecated since 2.7.0
*
* @return extension list which should be put before the current one
*/
@Deprecated
String[] before() default {};
/**
* Relative ordering info, optional
* Deprecated since 2.7.0
*
* @return extension list which should be put after the current one
*/
@Deprecated
String[] after() default {};
/**
* Absolute ordering info, optional
*
* @return absolute ordering info
*/
int order() default 0;
}
通过源码注解可以知道,实现这个功能核心代码在ExtensionLoader#getActivateExtension(URL, String, String)中,那么下面我们就来看一下这一块代码:
/**
* This is equivalent to {@code getActivateExtension(url, url.getParameter(key).split(","), null)}
*
* @param url url
* @param key url parameter key which used to get extension point names
* @param group group
* @return extension list which are activated.
* @see #getActivateExtension(org.apache.dubbo.common.URL, String[], String)
*/
public List<T> getActivateExtension(URL url, String key, String group) {
String value = url.getParameter(key);
return getActivateExtension(url, StringUtils.isEmpty(value) ? null : COMMA_SPLIT_PATTERN.split(value), group);
}
/**
* Get activate extensions.
* 获取所有激活的扩展点
* @param url url
* @param values extension point names
* @param group group
* @return extension list which are activated
* @see org.apache.dubbo.common.extension.Activate
*/
public List<T> getActivateExtension(URL url, String[] values, String group) {
//需要激活的扩展点
List<T> exts = new ArrayList<>();
//扩展点的名称,即注解中第二个参数中指定的值
List<String> names = values == null ? new ArrayList<>(0) : Arrays.asList(values);
/**
*在dubbo的扩展点的配置,如果配置的扩展点的名称以{@link REMOVE_VALUE_PREFIX}开头,就不会被激活
*/
if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
/**
* 将扩展点类加载到缓存中{@link cachedClasses}
*/
getExtensionClasses();
//遍历缓存中的激活的扩展点
for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
String name = entry.getKey();
Object activate = entry.getValue();
String[] activateGroup, activateValue;
if (activate instanceof Activate) {
//获取@Activate的group参数
activateGroup = ((Activate) activate).group();
//获取@Activate的value参数
activateValue = ((Activate) activate).value();
} else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
//这个主要是为了兼容Dubbo前期的版本
activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
} else {
continue;
}
//匹配group ,value ,来决定是否激活这个扩展点
if (isMatchGroup(group, activateGroup)
&& !names.contains(name)
&& !names.contains(REMOVE_VALUE_PREFIX + name)
&& isActive(activateValue, url)) {
exts.add(getExtension(name));
}
}
exts.sort(ActivateComparator.COMPARATOR);
}
List<T> usrs = new ArrayList<>();
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
if (!name.startsWith(REMOVE_VALUE_PREFIX)
&& !names.contains(REMOVE_VALUE_PREFIX + name)) {
if (DEFAULT_KEY.equals(name)) {
if (!usrs.isEmpty()) {
exts.addAll(0, usrs);
usrs.clear();
}
} else {
usrs.add(getExtension(name));
}
}
}
if (!usrs.isEmpty()) {
exts.addAll(usrs);
}
return exts;
}
/**
* 匹配group
* @param group 服务的group
* @param groups 注解@Activate中的group字符串数组
* @return true 匹配成功,false失败
*/
private boolean isMatchGroup(String group, String[] groups) {
//如果group是空,默认为要激活
if (StringUtils.isEmpty(group)) {
return true;
}
if (groups != null && groups.length > 0) {
for (String g : groups) {
if (group.equals(g)) {
return true;
}
}
}
return false;
}
/**
* 匹配key
* @param keys {@link @Active}注解中配置的参数,一个或者多个
* @param url SPI实现的核心对象URL
* @return true 表示是自动激活扩展点,false不是
*/
private boolean isActive(String[] keys, URL url) {
//如果keys没有指定,默认表示要激活
if (keys.length == 0) {
return true;
}
for (String key : keys) {
// @Active(value="key1:value1, key2:value2")
String keyValue = null;
if (key.contains(":")) {
String[] arr = key.split(":");
key = arr[0];
keyValue = arr[1];
}
for (Map.Entry<String, String> entry : url.getParameters().entrySet()) {
String k = entry.getKey();
String v = entry.getValue();
if ((k.equals(key) || k.endsWith("." + key))
&& ((keyValue != null && keyValue.equals(v)) || (keyValue == null && ConfigUtils.isNotEmpty(v)))) {
return true;
}
}
}
return false;
}
2.2.4 SPI扩展点的缓存
①. 扩展点Class缓存 ,Dubbo SPI在获取扩展点时,会优先从缓存中读取,如果缓存中不存在则加载配置文件,根据配置将Class缓存到内存中,并不会直接初始化。
②. 扩展点实例缓存 ,Dubbo不仅会缓存Class,还会缓存Class的实例。每次取实例的时候会优先从缓存中取,取不到则从配置中加载并缓存到内存中。这个就是典型的使用空间换时间的做法。也是Dubbo性能强劲的原因之一
以上只列举了主要的,这里直接上源码:
/**
* 扩展类加载器缓存,就是扩展点ExtendsLoader实例缓存; key=扩展接口 value=扩展类加载器
*/
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();
/**
* 扩展实例存入内存中缓存起来; key=扩展类 ; value=扩展类实例
*/
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
/**
* 扩展名缓存 key=扩展类 ; value=扩展名
*/
private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<>();
/**
* 扩展点Class缓存
*/
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
/**
* 默认激活的扩展点的缓存
*/
private final Map<String, Object> cachedActivates = new ConcurrentHashMap<>();
/**
* 扩展点实例缓存 key=扩展点名 value=扩展对象
*/
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
/**
* 自适应的扩展点实例缓存
*/
private final Holder<Object> cachedAdaptiveInstance = new Holder<>();
/**
* 配置有自动激活的接口的Class缓存
*/
private volatile Class<?> cachedAdaptiveClass = null;
/**
*默认缓存的名称
*/
private String cachedDefaultName;
/**
* Wrapper类缓存
*/
private Set<Class<?>> cachedWrapperClasses;
这个将在下一节研读ExtensionLoader源码的时候会做详细介绍
2.3 ExtensionLoader原理解析
文章链接
2.4 ExtensionLoader相关技术源码解析
文章链接
3 整体设计原理
前面两章讲了Dubbo框架的架构设计和核心的实现技术细节,从这一章开始将深入解读Dubbo各个模块的实现原理和关系。
3.1 总体设计
这是dubbo项目中Consumer调用Providers接口服务的整体流程分析