Dubbo的SPI机制分析

1. 前言

Dubbo服务框架采用了「微内核+插件」的设计原则,Dubbo自身的核心功能点也是通过扩展点实现的,这意味着Dubbo几乎所有的功能点都可以由用户自定义的扩展和替换,这也大大的提高了Dubbo框架本身的高度可扩展性。举例来说,如果你觉得Dubbo内置的对象序列化方式不好用,你完全可以自定义一个;如果你觉得Netty网络传输不好用,你也完全可以自定义一个。 ​

SPI的全称是「Service Provider Interface」,在介绍Dubbo SPI前,先来看看Java自带的SPI机制。 ​

Java SPI起初是提供给厂商做插件开发用的,例如数据库驱动java.sql.Driver,市面上各种各样的数据库,不同的数据库底层协议都不一样,为了方便开发者调用数据库而不用关心它们之间的差异,因此必须提供一个统一的接口来规范和约束这些数据库。有了统一的接口,数据库厂商就可以按照规范去开发自己的数据库驱动了。 ​

厂商开发好数据库驱动了,应用如何使用呢?该使用哪个驱动呢?以MySQL为例,早期手写JDBC时,开发者需要手动注册驱动,现在已经不需要了,就是利用了SPI机制。

2. Java SPI

Java SPI使用了策略模式,一个接口多种实现,开发者面向接口编程,具体的实现并不在程序中直接硬编码,而是通过外部文件进行配置。 ​

Java SPI约定了一个规范,使用步骤如下:

  1. 编写一个接口。
  2. 编写具体实现类。
  3. 在ClassPath下的META-INF/services目录创建以接口全限定名命名的文件,文件内容为实现类的全限定名,多个实现用换行符分割。
  4. 通过ServiceLoader类获取具体实现。

这里以MySQL为例,查看其Jar包,会发现配置文件如下: image.png 获取接口具体实现,代码如下:

Iterator<Driver> iterator = ServiceLoader.load(Driver.class).iterator();
while (iterator.hasNext()) {
	System.out.println(iterator.next().getClass());
}
输出:
class com.mysql.jdbc.Driver
class com.mysql.fabric.jdbc.FabricMySQLDriver
复制代码

Java SPI的缺点:

  1. 不支持按需加载,迭代器遍历会实例化所有的实现类,即使它没有被用到,太浪费资源了。
  2. 获取实现类的方式不灵活,只能通过迭代器遍历。
  3. 没有缓存,实现类会被多次创建。
  4. 扩展加载失败,失败原因丢失,不方便排查。
  5. 不支持AOP和IOC。

3. Dubbo SPI

Dubbo SPI定义了一套自己的规范,同时对Java SPI存在的问题进行了改进,优点如下:

  1. 扩展类按需加载,节约资源。
  2. SPI文件采用Key=Value形式,可以根据扩展名灵活获取实现类。
  3. 扩展类对象做了缓存,避免重复创建。
  4. 扩展类加载失败有详细日志,方便排查。
  5. 支持AOP和IOC。

Dubbo SPI使用规范:

  1. 编写接口,接口必须加@SPI注解,代表它是一个可扩展的接口。
  2. 编写实现类。
  3. 在ClassPath下的META-INF/dubbo目录创建以接口全限定名命名的文件,文件内容为Key=Value格式,Key是扩展点的名称,Value是扩展点实现类的全限定名。
  4. 通过ExtensionLoader类获取扩展点实现。

Dubbo默认会扫描META-INF/servicesMETA-INF/dubboMETA-INF/dubbo/internal三个目录下的配置,第一个是为了兼容Java SPI,第三个是Dubbo内部使用的扩展点。 ​

Dubbo SPI支持四种特性:自动包装、自动注入、自适应、自动激活。

3.1 自动包装

Dubbo SPI的AOP就是利用「自动包装」来实现的。在扩展类的实现中,可能存在部分逻辑是通用的,应该把它们提取出来,而不是每个实现类都写一份重复的代码。此时,应该创建一个Wrapper包装类,编写通用逻辑,它内部应该持有一个原对象Origin,个性化的业务逻辑交给Origin自己处理,通用逻辑由Wrapper统一处理。 ​

自动包装的规范是:Wrapper类应该提供一个构造函数,该函数只有一个参数:扩展点接口。

public class SayWrapper implements Say {

    private final Say origin;

    public SayWrapper(Say origin) {
        this.origin = origin;
    }

    @Override
    public void say() {
        System.err.println("before...");
        origin.say();
        System.err.println("after...");
    }
}
复制代码

SPI文件配置

impl=demo.spi.wrapper.SayImpl
wrapper=demo.spi.wrapper.SayWrapper
复制代码

获取扩展实现

// 默认获取的就是包装类
Say say = ExtensionLoader.getExtensionLoader(Say.class).getDefaultExtension();
say.say();

输出:
before...
say...
after...
复制代码

3.2 自动注入

Dubbo SPI是支持自动注入的,它类似于Spring的IOC,当扩展类的属性是另一个扩展点类型,且提供了Setter方法时,Dubbo会自动帮我们注入依赖依赖的扩展类成员对象。

假设现在有一个Eat扩展接口。

@SPI
public interface Eat {
    @Adaptive("key")
    void eat(URL url);
}

public class EatImpl implements Eat {
    @Override
    public void eat(URL url) {
        System.err.println("eat meat...");
    }
}
复制代码

SayA依赖了Eat扩展。

public class SayA implements Say {
    public Eat eat;

    public void setEat(Eat eat) {
        this.eat = eat;
    }
}
复制代码

当我们获取SayA实现时,Dubbo会自动帮我们注入Eat扩展点对象。**Eat扩展点实现类可能有很多,该注入哪一个呢?**这就和下面要说的「自适应」有关了,其实Dubbo注入的始终是一个自适应扩展,它会根据参数中的URL去判断具体调用哪个实现。

3.3 自适应

SPI扩展点可能存在这种情况:扩展点实现类有很多,无法硬编码指定,需要运行时动态根据参数来确定具体实现类。为了实现该需求,Dubbo SPI实现了自适应调用。 ​

自适应需要用到@Adaptive注解,它可以加在类或方法上。加在类上,该类就是自适应类;加在方法上,会自动生成代理类,通过URL对象里的参数进行匹配,以确定具体实现。

自适应调用的实现原理并不复杂,Dubbo利用Javassist技术给扩展接口动态的生成了自适应代理类,类名的规则是XXX$Adaptive,在代理类中,根据URL对象中的参数,去匹配具体的扩展点实现类。

@SPI
public interface Say {

    // 匹配URL中的参数key
    @Adaptive({"key"})
    void say(URL url);
}
复制代码

假设有a、b两个实现,自适应调用如下:

Say say = ExtensionLoader.getExtensionLoader(Say.class).getAdaptiveExtension();
say.say(URL.valueOf("http://127.0.0.1?key=a"));
say.say(URL.valueOf("http://127.0.0.1?key=b"));

输出:
sayA...
sayB...
复制代码

3.4 自动激活

场景:某个扩展点的多个实现类需要根据规则同时启用,例如Filter过滤器。 ​

自动激活需要使用@Activate注解,一旦加上该注解表示该实现类需要根据条件自动激活,注解属性含义如下:

属性 说明
group Group匹配成功则激活
value URL中存在该Key则激活
order 扩展点执行顺序

假设有Filter接口:

@SPI
public interface Filter {
    void invoke();
}
复制代码

FilterA代表在consumer组、且URL中存在xxx参数时自动激活,顺序为1。

@Activate(group = {"consumer"}, value = {"xxx"}, order = 1)
public class FilterA implements Filter {

    @Override
    public void invoke() {
        System.err.println("FilterA...");
    }
}
复制代码

FilterB代表在provider组、且URL中存在ooo参数时自动激活,顺序为2。

@Activate(group = {"provider"}, value = {"ooo"}, order = 2)
public class FilterB implements Filter {

    @Override
    public void invoke() {
        System.err.println("FilterB...");
    }
}
复制代码

获取激活的扩展点实现类对象集合,如下仅会输出FilterA,FilterB的Group匹配失败了。

ExtensionLoader<Filter> extensionLoader = ExtensionLoader.getExtensionLoader(Filter.class);
URL url = URL.valueOf("http://127.0.0.1?key=xxx,ooo");
List<Filter> filters = extensionLoader.getActivateExtension(url, "key","consumer");
filters.stream().forEach(System.out::println);

输出:
demo.spi.activate.FilterA
复制代码

4. 源码分析

Dubbo SPI的核心类是ExtensionLoader,它的主要职责就是加载扩展点实现类,以及根据各种条件获取扩展点实例。

属性说明如何:

public class ExtensionLoader<T> {

    private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);
    // 多个扩展点用逗号分割
    private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*[,]+\\s*");
    // 扩展点实例缓存
    private final ConcurrentMap<Class<?>, Object> extensionInstances = new ConcurrentHashMap<>(64);
    // 接口
    private final Class<?> type;
    // 扩展依赖注入器
    private final ExtensionInjector injector;
    // 扩展类名称缓存
    private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<>();
    // 扩展类缓存
    private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
    // 自动激活扩展实例缓存
    private final Map<String, Object> cachedActivates = Collections.synchronizedMap(new LinkedHashMap<>());
    // 扩展类激活的Group缓存
    private final Map<String, Set<String>> cachedActivateGroups = Collections.synchronizedMap(new LinkedHashMap<>());
    // 扩展类激活的Value缓存
    private final Map<String, String[]> cachedActivateValues = Collections.synchronizedMap(new LinkedHashMap<>());
    // 扩展实例缓存
    private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
    // 动态生成的自适应实例缓存
    private final Holder<Object> cachedAdaptiveInstance = new Holder<>();
    // 动态生成的自适应类
    private volatile Class<?> cachedAdaptiveClass = null;
    // 默认扩展名称
    private String cachedDefaultName;
    // 动态创建自适应实例发生的异常
    private volatile Throwable createAdaptiveInstanceError;
    // 包装类缓存
    private Set<Class<?>> cachedWrapperClasses;
    // 异常缓存
    private Map<String, IllegalStateException> exceptions = new ConcurrentHashMap<>();
    /**
     * 扩展类Class加载策略:默认从三个路径加载
     * 1. META-INF/dubbo/internal/
     * 2. META-INF/dubbo/
     * 3. META-INF/services/
     */
    private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();
    /**
     * Record all unacceptable exceptions when using SPI
     * 记录加载扩展点时出现的异常
     */
    private Set<String> unacceptableExceptions = new ConcurrentHashSet<>();
    //
    private ExtensionDirector extensionDirector;
    // 扩展点后置处理
    private List<ExtensionPostProcessor> extensionPostProcessors;
    // 扩展类实例化策略
    private InstantiationStrategy instantiationStrategy;
    private Environment environment;
    // 自动激活扩展点排序
    private ActivateComparator activateComparator;
    private ScopeModel scopeModel;
}
复制代码

ExtensionLoader是和接口绑定的,一个接口对应一个ExtensionLoader实例,获取接口对应的实例也很简单:

ExtensionLoader<Say> extensionLoader = ExtensionLoader.getExtensionLoader(Say.class);
复制代码

ExtensionLoader有三个常用方法,下面分别分析:

方法名 备注
getDefaultExtension() 获取默认扩展点实现类实例
getAdaptiveExtension() 获取自适应实例
getActivateExtension() 获取自动激活实例集合

4.1 默认扩展点

在这里插入图片描述

getDefaultExtension()方法为入口获取默认的扩展点实现类实例,默认情况下,如果有Wrapper类,会进行自动包装。

public T getDefaultExtension() {
    // 加载实现类
    getExtensionClasses();
    if (StringUtils.isBlank(cachedDefaultName) || "true".equals(cachedDefaultName)) {
        return null;
    }
    // 获取默认扩展点实现类实例
    return getExtension(cachedDefaultName);
}
复制代码

getExtensionClasses()方法会获取扩展点下的所有实现类,只会加载一次,加载完会进行缓存。

private Map<String, Class<?>> getExtensionClasses() {
    // 优先从缓存中取
    Map<String, Class<?>> classes = cachedClasses.get();
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // 没有缓存,从指定路径下加载类
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}
复制代码

loadExtensionClasses()方法默认会从三个路径去加载Class,不同的加载路径被定义成一个加载策略,对应的类是LoadingStrategy。

private Map<String, Class<?>> loadExtensionClasses() {
    // 缓存@SPI注解指定的默认扩展名
    cacheDefaultExtensionName();

    Map<String, Class<?>> extensionClasses = new HashMap<>();

    /**
     * 依次从不同目录下加载
     * 1. META-INF/dubbo/internal/
     * 2. META-INF/dubbo/
     * 3. META-INF/services/
     */
    for (LoadingStrategy strategy : strategies) {
        // 从指定目录加载Class
        loadDirectory(extensionClasses, strategy, type.getName());

        // compatible with old ExtensionFactory
        if (this.type == ExtensionInjector.class) {
            loadDirectory(extensionClasses, strategy, ExtensionFactory.class.getName());
        }
    }

    return extensionClasses;
}
复制代码

扩展类加载完毕后,就可以根据默认的扩展名去创建实例了,默认是会进行自动包装的。

private T createExtension(String name, boolean wrap) {
    // 获取扩展名对应的Class
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null || unacceptableExceptions.contains(name)) {
        // Class实例创建失败过,抛出异常
        throw findException(name);
    }
    try {
        T instance = (T) extensionInstances.get(clazz);
        if (instance == null) {
            // 创建实例并缓存
            extensionInstances.putIfAbsent(clazz, createExtensionInstance(clazz));
            instance = (T) extensionInstances.get(clazz);
            // 前置处理
            instance = postProcessBeforeInitialization(instance, name);
            // Setter方法注入
            injectExtension(instance);
            // 后置处理
            instance = postProcessAfterInitialization(instance, name);
        }

        if (wrap) {// 自动包装
            List<Class<?>> wrapperClassesList = new ArrayList<>();
            if (cachedWrapperClasses != null) {
                // 包装类排序
                wrapperClassesList.addAll(cachedWrapperClasses);
                wrapperClassesList.sort(WrapperComparator.COMPARATOR);
                Collections.reverse(wrapperClassesList);
            }

            if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
                for (Class<?> wrapperClass : wrapperClassesList) {
                    Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
                    // @Wrapper注解匹配,判断是否需要包装
                    if (wrapper == null
                        || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
                        // 反射创建包装类实例
                        instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                        // 包装类的后置处理
                        instance = postProcessAfterInitialization(instance, name);
                    }
                }
            }
        }

        initExtension(instance);
        return instance;
    }
}
复制代码

injectExtension()方法会进行依赖注入,它会查找Class的Setter方法,然后判断它的参数是否也是扩展点,如果是就会从ExtensionAccessor中获取扩展点对应的自适应实例,然后反射赋值。 注:Dubbo SPI只能注入Adaptive实例,因此必须保证注入的扩展点是自适应的。

4.2 自适应

在这里插入图片描述

getAdaptiveExtension()方法用来获取扩展点的自适应实例,它的原理并不复杂,无非就是生成扩展点的代理类,然后解析参数中URL的属性和@Adaptive注解的值做匹配,再去调用指定的扩展点实现。 ​

自适应类会在程序运行时动态生成,可以用JDK的动态代理,也可以用类似CGLIB等字节码技术生成,Dubbo默认用的是Javassist。 ​

自适应对象也有缓存,只会创建一次,对应的属性是cachedAdaptiveInstance,创建自适应对象的方法是createAdaptiveExtension()

private T createAdaptiveExtension() {
    try {
        // 获取自适应类Class,创建实例
        T instance = (T) getAdaptiveExtensionClass().newInstance();
        // 前后置处理、Setter注入
        instance = postProcessBeforeInitialization(instance, null);
        instance = injectExtension(instance);
        instance = postProcessAfterInitialization(instance, null);
        initExtension(instance);
        return instance;
    } catch (Exception e) {
        throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
    }
}
复制代码

创建自适应对象之前,首先得生成自适应Class,对应的方法是createAdaptiveExtensionClass()

private Class<?> createAdaptiveExtensionClass() {
    ClassLoader classLoader = type.getClassLoader();
    try {
        if (NativeUtils.isNative()) {
            return classLoader.loadClass(type.getName() + "$Adaptive");
        }
    } catch (Throwable ignore) {

    }
    // 根据Class和默认扩展名,生成Class源码
    String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
    // 获取默认编译器:JavassistCompiler
    org.apache.dubbo.common.compiler.Compiler compiler = extensionDirector.getExtensionLoader(
        org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    // 动态编译成Class
    return compiler.compile(code, classLoader);
}
复制代码

这里给出一个Dubbo 生成的自适应类代码示例:

public class Say$Adaptive implements demo.spi.adaptive.Say {
    public void say(org.apache.dubbo.common.URL arg0) {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg0;
        // 获取URL中的参数,key是@Adaptive注解指定的
        String extName = url.getParameter("key");
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (demo.spi.adaptive.Say) name from url (" + url.toString() + ") use keys([key])");
        ScopeModel scopeModel = ScopeModelUtil.getOrDefault(url.getScopeModel(), demo.spi.adaptive.Say.class);
        // 获取key指定的扩展名对应的实现类
        demo.spi.adaptive.Say extension = (demo.spi.adaptive.Say) scopeModel.getExtensionLoader(demo.spi.adaptive.Say.class).getExtension(extName);
        extension.say(arg0);
    }
}
复制代码

4.3 自动激活

在这里插入图片描述

getActivateExtension()方法用来获取自动激活的扩展点实例集合,如果希望某个扩展点实现自动激活,只需要在类上加@Activate即可,还可以配置Group和Value来设置自动激活的条件。例如某些扩展点只会在Provider端激活,而有些只会在Consumer端激活。

public @interface Activate {
    
    // 自动激活时匹配的Group
	String[] group() default {};
    
    // 自动激活时匹配的Value
    String[] value() default {};
    
    // 扩展点顺序
    int order() default 0;
}
复制代码

首先,需要从URL中解析出Key对应的Value,多个扩展点名称用逗号分割。

public List<T> getActivateExtension(URL url, String key, String group) {
    // 获取Key对应的Value
    String value = url.getParameter(key);
    // Value使用,拆分
    return getActivateExtension(url, StringUtils.isEmpty(value) ? null : COMMA_SPLIT_PATTERN.split(value), group);
}
复制代码

激活的扩展点实例使用TreeMap存储,Key会按照注解里的order属性进行排序。

Map<Class<?>, T> activateExtensionsMap = new TreeMap<>(activateComparator);
List<String> names = values == null ? new ArrayList<>(0) : asList(values);
Set<String> namesSet = new HashSet<>(names);
复制代码

Value如果配置了-default,则会排除默认的自动激活类,反之会先加载默认的激活类,此时并不会加载Value指定的扩展类

// 没有-default,先加载默认扩展点(Value和Group匹配成功的)
if (!namesSet.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
    if (cachedActivateGroups.size() == 0) {
        synchronized (cachedActivateGroups) {
            // cache all extensions
            if (cachedActivateGroups.size() == 0) {
                // 加载配置的扩展类
                getExtensionClasses();
                // 遍历@Activate类,缓存类的Group和Value配置
                for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
                    String name = entry.getKey();
                    Object activate = entry.getValue();
                    String[] activateGroup, activateValue;
                    if (activate instanceof Activate) {
                        activateGroup = ((Activate) activate).group();
                        activateValue = ((Activate) activate).value();
                    } else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
                        // 兼容旧注解
                        activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
                        activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
                    } else {
                        continue;
                    }
                    cachedActivateGroups.put(name, new HashSet<>(Arrays.asList(activateGroup)));
                    cachedActivateValues.put(name, activateValue);
                }
            }
        }
    }

    cachedActivateGroups.forEach((name, activateGroup) -> {
        if (isMatchGroup(group, activateGroup)// Group匹配
            && !namesSet.contains(name)// 被Key指定的扩展点后面会加载
            && !namesSet.contains(REMOVE_VALUE_PREFIX + name)
            // Value匹配
            && isActive(cachedActivateValues.get(name), url)) {
            // Group和Value均匹配成功,且没有被Key指定的默认扩展点,这里会加载。
            activateExtensionsMap.put(getExtensionClass(name), getExtension(name));
        }
    });
}
复制代码

如果Value指定了default,会影响扩展点的顺序,default内的扩展点依然是有序的,但是default前后的扩展点将不会根据order排序,例如:

`extA,default,extB`
extA的顺序将在所有默认扩展点之前,extB的顺序将在所有默认扩展点之后
复制代码

代码如下:

if (namesSet.contains(DEFAULT_KEY)) {
    ArrayList<T> extensionsResult = new ArrayList<>(activateExtensionsMap.size() + names.size());
    for (int i = 0; i < names.size(); i++) {
        String name = names.get(i);
        if (!name.startsWith(REMOVE_VALUE_PREFIX)
            && !namesSet.contains(REMOVE_VALUE_PREFIX + name)) {
            if (!DEFAULT_KEY.equals(name)) {
                if (containsExtension(name)) {
                    extensionsResult.add(getExtension(name));
                }
            } else {
                extensionsResult.addAll(activateExtensionsMap.values());
            }
        }
    }
    return extensionsResult;
}
复制代码

如果Value没有指定default,那么所有扩展点实例将全部存储在TreeMap中,全部都是有序的。

for (int i = 0; i < names.size(); i++) {
    String name = names.get(i);
    if (!name.startsWith(REMOVE_VALUE_PREFIX)
        && !namesSet.contains(REMOVE_VALUE_PREFIX + name)) {
        if (!DEFAULT_KEY.equals(name)) {
            if (containsExtension(name)) {
                activateExtensionsMap.put(getExtensionClass(name), getExtension(name));
            }
        }
    }
}
return new ArrayList<>(activateExtensionsMap.values());
复制代码

5. 总结

SPI机制使用了策略模式,一个接口多种实现,开发者面向接口编程,具体实现并不在程序中硬编码指定,而是通过配置文件的方式在外部指定。Java内置了SPI机制,但是存在一些缺陷,例如:不支持按需加载,浪费资源,排查困难等等,因此Dubbo自己定义了一套规范,开发了自己的SPI功能。 ​

Dubbo SPI进行了大量的优化和功能增强,它支持按需加载,并且对扩展对象做了缓存,不会重复创建对象。获取扩展对象的方式更加灵活,还增加了诸如自动包装、IOC和AOP、自动激活、自适应调用等多重高级特性。

猜你喜欢

转载自juejin.im/post/7040443722101850148