SPI 在 Dubbo中 的应用

Stella981
• 阅读 636

通过本文的学习,可以了解 Dubbo SPI 的特性及实现原理,希望对大家的开发设计有一定的启发性。

一、概述

SPI 全称为 Service Provider Interface,是一种模块间组件相互引用的机制。其方案通常是提供方将接口实现类的全名配置在classPath下的指定文件中,由调用方读取并加载。这样需要替换某个组件时,只需要引入新的JAR包并在其中包含新的实现类和配置文件即可,调用方的代码无需任何调整。优秀的SPI框架能够提供单接口多实现类时的优先级选择,由用户指定选择哪个实现。

得益于这些能力,SPI对模块间的可插拔机制和动态扩展提供了非常好的支撑。

本文将简单介绍JDK自带的SPI,分析SPI和双亲委派的关系,进而重点分析DUBBO的SPI机制;比较两者有何不同,DUBBO的SPI带来了哪些额外的能力。

二、JDK自带SPI

提供者在classPath或者jar包的META-INF/services/目录创建以服务接口命名的文件,调用者通过java.util.ServiceLoader加载文件内容中指定的实现类。

1. 代码示例

  • 首先定义一个接口Search

search示例接口

package com.example.studydemo.spi;
public interface Search {
    void search();
}
  • 实现类FileSearchImpl实现该接口

文件搜索实现类

package com.example.studydemo.spi;
public class FileSearchImpl implements Search {
    @Override
    public void search() {
        System.out.println("文件搜索");
    }
}
  • 实现类DataBaseSearchImpl实现该接口

数据库搜索实现类

package com.example.studydemo.spi;
public class DataBaseSearchImpl implements Search {
    @Override
    public void search() {
        System.out.println("数据库搜索");
    }
}
  • 在项目的META-INF/services文件夹下,创建Search文件

SPI 在 Dubbo中 的应用

文件内容为:

com.example.studydemo.spi.DataBaseSearchImpl
com.example.studydemo.spi.FileSearchImpl

测试:

import java.util.ServiceLoader;
public class JavaSpiTest {
    public static void main(String[] args) {
        ServiceLoader<Search> searches = ServiceLoader.load(Search.class);
        searches.forEach(Search::search);
    }
}

结果为:

SPI 在 Dubbo中 的应用

2. 简单分析

ServiceLoader作为JDK提供的一个服务实现查找工具类,调用自身load方法加载Search接口的所有实现类,然后可以使用for循环遍历实现类进行方法调用。

有一个疑问:META-INF/services/目录是硬编码的吗,其它路径行不行?答案是不行。

跟进到ServiceLoader类中,第一行代码就是private static final String PREFIX = “META-INF/services/”,所以SPI配置文件只能放在classPath或者jar包的这个指定目录下面。

ServiceLoader的文件载入路径

public final class ServiceLoader<S>
    implements Iterable<S>
{
    //硬编码写死了文件路径
    private static final String PREFIX = "META-INF/services/";
 
    // The class or interface representing the service being loaded
    private final Class<S> service;
 
    // The class loader used to locate, load, and instantiate providers
    private final ClassLoader loader;

JDK SPI的使用比较简单,做到了基本的加载扩展组件的功能,但有以下几点不足:

  • 需要遍历所有的实现并实例化,想要找到某一个实现只能循环遍历,一个一个匹配;
  • 配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名,导致在程序中很难去准确的引用它们;
  • 扩展之间彼此存在依赖,做不到自动注入和装配,不提供上下文内的IOC和AOP功能;
  • 扩展很难和其他的容器框架集成,比如扩展依赖了一个外部spring容器中的bean,原生的JDK SPI并不支持。

三、SPI与双亲委派

1. SPI加载到何处

基于类加载的双亲委派原则,由JDK内部加载的class默认应该归属于bootstrap类加载器,那么SPI机制加载的class是否也属于bootstrap呢 ?

答案是否定的,原生SPI机制通过ServiceLoader.load方法由外部指定类加载器,或者默认取Thread.currentThread().getContextClassLoader()线程上下文的类加载器,从而避免了class被载入bootstrap加载器。

2.SPI是否破坏了双亲委派

双亲委派的本质涵义是在rt.jar包和外部class之间建立一道classLoader的鸿沟,即rt.jar内的class不应由外部classLoader加载,外部class不应由bootstrap加载。

SPI仅是提供了一种在JDK代码内部干预外部class文件加载的机制,并未强制指定加载到何处;外部的class还是由外部的classLoader加载,未跨越这道鸿沟,也就谈不上破坏双亲委派。

原生ServiceLoader的类加载器

//指定类加载器
public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader)
//默认取前线程上下文的类加载器
public static <S> ServiceLoader<S> load(Class<S> service)

四、Dubbo SPI

Dubbo借鉴了Java SPI的思想,与JDK的ServiceLoader相对应的,Dubbo设计了ExtensionLoader类,其提供的功能比JDK更为强大。

1. 基本概念

首先介绍一些基本概念,让大家有一个初步的认知。

  • 扩展点(Extension Point):是一个Java的接口。
  • 扩展(Extension):扩展点的实现类
  • 扩展实例(Extension Instance):扩展点实现类的实例。
  • 自适应扩展实例(Extension Adaptive Instance)

自适应扩展实例其实就是一个扩展类的代理对象,它实现了扩展点接口。在调用扩展点的接口方法时,会根据实际的参数来决定要使用哪个扩展。

比如一个Search的扩展点,有一个search方法。有两个实现FileSearchImpl和DataBaseSearchImpl。Search的自适应实例在调用接口方法的时候,会根据search方法中的参数,来决定要调用哪个Search的实现。

如果方法参数中有name=FileSearchImpl,那么就调用FileSearchImpl的search方法。如果name=DataBaseSearchImpl,就调用DataBaseSearchImpl的search方法。 自适应扩展实例在Dubbo中的使用非常广泛。

在Dubbo中每一个扩展点都可以有自适应的实例,如果我们没有使用@Adaptive人工指定,Dubbo会使用字节码工具自动生成一个。

  • SPI Annotation

    作用于扩展点的接口上,表明该接口是一个扩展点,可以被Dubbo的ExtentionLoader加载

  • Adaptive

@Adaptive注解可以使用在类或方法上。用在方法上表示这是一个自适应方法,Dubbo生成自适应实例时会在方法中植入动态代理的代码。方法内部会根据方法的参数来决定使用哪个扩展。

@Adaptive注解用在类上代表该实现类是一个自适应类,属于人为指定的场景,Dubbo就不会为该SPI接口生成代理类,最典型的应用如AdaptiveCompiler、AdaptiveExtensionFactory等。

@Adaptive注解的值为字符串数组,数组中的字符串是key值,代码中要根据key值来获取对应的Value值,进而加载相应的extension实例。比如new String[]{“key1”,”key2”},表示会先在URL中寻找key1的值,

如果找到则使用此值加载extension,如果key1没有,则寻找key2的值,如果key2也没有,则使用SPI注解的默认值,如果SPI注解没有默认值,则将接口名按照首字母大写分成多个部分,

然后以’.’分隔,例如org.apache.dubbo.xxx.YyyInvokerWrapper接口名会变成yyy.invoker.wrapper,然后以此名称做为key到URL寻找,如果仍没有找到则抛出IllegalStateException异常。

  • ExtensionLoader
    类似于Java SPI的ServiceLoader,负责扩展的加载和生命周期维护。ExtensionLoader的作用包括:解析配置文件加载extension类、生成extension实例并实现IOC和AOP、创建自适应的extension等,下文会重点分析。

  • 扩展名
    和Java SPI不同,Dubbo中的扩展都有一个名称,用于在应用中引用它们。比如
    registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
    dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol

  • 加载路径
    Java SPI从/META-INF/services目录加载扩展配置,Dubbo从以下路径去加载扩展配置文件:
    META-INF/dubbo/internal
    META-INF/dubbo
    META-INF/services
    其中META-INF/dubbo对开发者发放,META-INF/dubbo/internal 这个路径是用来加载Dubbo内部的拓展点的。

2. 代码示例

定义一个接口,标注上dubbo的SPI注解,赋予默认值,并提供两个extension实现类

package com.example.studydemo.spi;
@SPI("dataBase")
public interface Search {
    void search();
}

public class FileSearchImpl implements Search {
    @Override
    public void search() {
        System.out.println("文件搜索");
    }
}

public class DataBaseSearchImpl implements Search {
    @Override
    public void search() {
        System.out.println("数据库搜索");
    }
}

在META-INF/dubbo 路径下创建Search文件

SPI 在 Dubbo中 的应用 SPI 在 Dubbo中 的应用

文件内容如下:

dataBase=com.example.studydemo.spi.DataBaseSearchImpl
file=com.example.studydemo.spi.FileSearchImpl

编写测试类进行测试,内容如下:

public class DubboSpiTest {
    public static void main(String[] args) {
        ExtensionLoader<Search> extensionLoader = ExtensionLoader.getExtensionLoader(Search.class);
        Search fileSearch = extensionLoader.getExtension("file");
        fileSearch.search();
        Search dataBaseSearch = extensionLoader.getExtension("dataBase");
        dataBaseSearch.search();
        System.out.println(extensionLoader.getDefaultExtensionName());
        Search defaultSearch = extensionLoader.getDefaultExtension();
        defaultSearch.search();
    }
}

结果为:

SPI 在 Dubbo中 的应用 SPI 在 Dubbo中 的应用

从代码示例上来看,Dubbo SPI与Java SPI在这几方面是类似的:

  • 接口及相应的实现
  • 配置文件
  • 加载类及加载具体实现

3源码分析

下面深入到源码看看SPI在Dubbo中是怎样工作的,以Protocol接口为例进行分析。

//1、得到Protocol的扩展加载对象extensionLoader,由这个加载对象获得对应的自适应扩展类
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
//2、根据扩展名获取对应的扩展类
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("dubbo");

在获取扩展实例前要先获取Protocol接口的ExtensionLoader组件,通过ExtensionLoader来获取相应的Protocol实例Dubbo实际是为每个SPI接口都创建了一个对应的ExtensionLoader。

ExtensionLoader组件

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    if (type == null)
        throw new IllegalArgumentException("Extension type == null");
    if(!type.isInterface()) {
        throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
    }
    if(!withExtensionAnnotation(type)) {
        throw new IllegalArgumentException("Extension type(" + type +
                ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
    }
    //EXTENSION_LOADERS为ConcurrentMap,存储Class对应的ExtensionLoader
    ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    if (loader == null) {
        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    }
    return loader;
}

EXTENSION_LOADERS是一个 ConcurrentMap,以接口Protocol为key,以ExtensionLoader对象为value;保存的是Protocol扩展的加载类,第一次加载的时候Protocol还没有自己的接口加载类,需要实例化一个。

再看new ExtensionLoader(type) 这个操作,下面为ExtensionLoader的构造方法:

rivate ExtensionLoader(Class<?> type) {
    this.type = type;
    objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}

每一个ExtensionLoader都包含2个值:type和objectFactory,此例中type就是Protocol,objectFactory就是ExtensionFactory。

对于ExtensionFactory接口来说,它的加载类中objectFactory值为null。

对于其他的接口来说,objectFactory都是通过ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()来获取;objectFactory的作用就是为dubbo的IOC提供依赖注入的对象,可以认为是进程内多个组件容器的一个上层引用,

随着这个方法的调用次数越来越多,EXTENSION_LOADERS 中存储的 loader 也会越来越多。

自适应扩展类与IOC

得到ExtensionLoader组件之后,再看如何获得自适应扩展实例。

public T getAdaptiveExtension() {
    //cachedAdaptiveInstance为缓存的自适应对象,第一次调用时还没有创建自适应类,所以instance为null
    Object instance = cachedAdaptiveInstance.get();
    if (instance == null) {
        if(createAdaptiveInstanceError == null) {
            synchronized (cachedAdaptiveInstance) {
                instance = cachedAdaptiveInstance.get();
                if (instance == null) {
                    try {
                        //创建自适应对象实例
                        instance = createAdaptiveExtension();
                        //将自适应对象放到缓存中
                        cachedAdaptiveInstance.set(instance);
                    } catch (Throwable t) {
                        createAdaptiveInstanceError = t;
                        throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
                    }
                }
            }
        }
        else {
            throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
        }
    }
 
    return (T) instance;
}

首先从cachedAdaptiveInstance缓存中获取,第一次调用时还没有相应的自适应扩展,需要创建自适应实例,创建后再将该实例放到cachedAdaptiveInstance缓存中。

创建自适应实例参考createAdaptiveExtension方法,该方法包含两部分内容:创建自适应扩展类并利用反射实例化、利用IOC机制为该实例注入属性。

private T createAdaptiveExtension() {
    try {
        //得到自适应扩展类并利用反射实例化,然后注入属性值
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    } catch (Exception e) {
        throw new IllegalStateException("Can not create adaptive extenstion " + type + ", cause: " + e.getMessage(), e);
    }
}

再来分析getAdaptiveExtensionClass方法,以Protocol接口为例,该方法会做以下事情:获取所有实现Protocol接口的扩展类、如果有自适应扩展类直接返回、如果没有则创建自适应扩展类。

//该动态代理生成的入口
private Class<?> getAdaptiveExtensionClass() {
    //1.获取所有实现Protocol接口的扩展类
    getExtensionClasses();
    //2.如果有自适应扩展类,则返回
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    //3.如果没有,则创建自适应扩展类
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

getExtensionClasses方法会加载所有实现Protocol接口的扩展类,首先从缓存中获取,缓存中没有则调用loadExtensionClasses方法进行加载并设置到缓存中,如下图所示:

private Map<String, Class<?>> getExtensionClasses() {
    //从缓存中获取
    Map<String, Class<?>> classes = cachedClasses.get();
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                //从SPI配置文件中解析
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

loadExtensionClasses方法如下:首先获取SPI注解中的value值,作为默认扩展名称,在Protocol接口中SPI注解的value为dubbo,因此DubboProtocol就是Protocol的默认实现扩展。其次加载三个配置路径下的所有的Protocol接口的扩展实现。

// 此方法已经getExtensionClasses方法同步过。
private Map<String, Class<?>> loadExtensionClasses() {
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if(defaultAnnotation != null) {
        String value = defaultAnnotation.value();
        if(value != null && (value = value.trim()).length() > 0) {
            String[] names = NAME_SEPARATOR.split(value);
            if(names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
                        + ": " + Arrays.toString(names));
            }
            if(names.length == 1) cachedDefaultName = names[0];
        }
    }
     
    Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    //分别从三个路径加载
    loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    loadFile(extensionClasses, DUBBO_DIRECTORY);
    loadFile(extensionClasses, SERVICES_DIRECTORY);
    return extensionClasses;
}
 
 
private static final String SERVICES_DIRECTORY = "META-INF/services/";
private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";

在加载配置路径下的实现中,其中有一个需要关注的点,如果其中某个实现类上有Adaptive注解,说明用户指定了自适应扩展类,那么该实现类就会被赋给cachedAdaptiveClass,在getAdaptiveExtensionClass方法中会被直接返回。

如果该变量为空,则需要通过字节码工具来创建自适应扩展类。

private Class<?> createAdaptiveExtensionClass() {
    //生成类代码
    String code = createAdaptiveExtensionClassCode();
    //找到类加载器
    ClassLoader classLoader = findClassLoader();
    //获取编译器实现类,此处为AdaptiveCompiler,此类上有Adaptive注解
    com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    //将类代码编译为Class
    return compiler.compile(code, classLoader);
}

createAdaptiveExtensionClass方法生成的类代码如下:

package com.alibaba.dubbo.rpc;
 
import com.alibaba.dubbo.common.extension.ExtensionLoader;
 
public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
    public void destroy() {
        throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }
 
    public int getDefaultPort() {
        throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }
 
    public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) throws com.alibaba.dubbo.rpc.RpcException {
        if (arg1 == null) throw new IllegalArgumentException("url == null");
        com.alibaba.dubbo.common.URL url = arg1;
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.refer(arg0, arg1);
    }
 
    public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.RpcException {
        if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null)
            throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
        com.alibaba.dubbo.common.URL url = arg0.getUrl();
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }
}

由字节码工具生成的类Protocol$Adpative在方法末尾调用了ExtensionLoader.getExtensionLoader(xxx).getExtension(extName)来满足adaptive的自适应动态特性。

传入的extName就是从url中获取的动态参数,用户只需要在代表DUBBO全局上下文信息的URL中指定protocol参数的取值,adaptiveExtentionClass就可以去动态适配不同的扩展实例。

再看属性注入方法injectExtension,针对public的只有一个参数的set方法进行处理,利用反射进行方法调用来实现属性注入,此方法是Dubbo SPI实现IOC功能的关键。

private T injectExtension(T instance) {
    try {
        if (objectFactory != null) {
            for (Method method : instance.getClass().getMethods()) {
                if (method.getName().startsWith("set")
                        && method.getParameterTypes().length == 1
                        && Modifier.isPublic(method.getModifiers())) {
                    Class<?> pt = method.getParameterTypes()[0];
                    try {
                        String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                        Object object = objectFactory.getExtension(pt, property);
                        if (object != null) {
                            method.invoke(instance, object);
                        }
                    } catch (Exception e) {
                        logger.error("fail to inject via method " + method.getName()
                                + " of interface " + type.getName() + ": " + e.getMessage(), e);
                    }
                }
            }
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;

Dubbo IOC 是通过set方法注入依赖,Dubbo首先会通过反射获取到实例的所有方法,然后再遍历方法列表,检测方法名是否具有set方法特征。若有则通过ObjectFactory获取依赖对象。

最后通过反射调用set方法将依赖设置到目标对象中。objectFactory在创建加载类ExtensionLoader的时候已经创建了,因为@Adaptive是打在类AdaptiveExtensionFactory上,所以此处就是AdaptiveExtensionFactory。

AdaptiveExtensionFactory持有所有ExtensionFactory对象的集合,dubbo内部默认实现的对象工厂是SpiExtensionFactory和SpringExtensionFactory,他们经过TreeSet排好序,查找顺序是优先先从SpiExtensionFactory获取,如果返回空在从SpringExtensionFactory获取。

//有Adaptive注解说明该类是自适应类,不需要程序自己创建代理类
@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {
    //factories拥有所有ExtensionFactory接口的实现对象
    private final List<ExtensionFactory> factories;
     
    public AdaptiveExtensionFactory() {
        ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
        List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
        for (String name : loader.getSupportedExtensions()) {
            list.add(loader.getExtension(name));
        }
        factories = Collections.unmodifiableList(list);
    }
    //查找时会遍历factories,顺序优先从SpiExtensionFactory中获取,再从SpringExtensionFactory中获取,原因为初始化时getSupportedExtensions方法中使用TreeSet已经排序,见下图
    public <T> T getExtension(Class<T> type, String name) {
        for (ExtensionFactory factory : factories) {
            T extension = factory.getExtension(type, name);
            if (extension != null) {
                return extension;
            }
        }
        return null;
    }
}

public Set<String> getSupportedExtensions() {
    Map<String, Class<?>> clazzes = getExtensionClasses();
    return Collections.unmodifiableSet(new TreeSet<String>(clazzes.keySet()));
}

虽然有过度设计的嫌疑,但我们不得不佩服dubbo SPI设计的精巧。

  • 提供@Adaptive注解,既可以加在方法上通过参数动态适配到不同的扩展实例;又可以加在类上直接指定自适应扩展类。
  • 利用AdaptiveExtensionFactory统一了进程中的不同容器,将ExtensionLoader本身视为一个独立的容器,依赖注入时将会分别从Spring容器和ExtensionLoader容器中查找。

扩展实例和AOP

getExtension方法比较简单,重点在于createExtension方法,根据扩展名创建扩展实例。

public T getExtension(String name) {
   if (name == null || name.length() == 0)
       throw new IllegalArgumentException("Extension name == null");
   if ("true".equals(name)) {
       return getDefaultExtension();
   }
   Holder<Object> holder = cachedInstances.get(name);
   if (holder == null) {
       cachedInstances.putIfAbsent(name, new Holder<Object>());
       holder = cachedInstances.get(name);
   }
   Object instance = holder.get();
   if (instance == null) {
       synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                //根据扩展名创建扩展实例
                instance = createExtension(name);
                holder.set(instance);
            }
        }
   }
   return (T) instance;
}

createExtension方法中的部分内容上文已经分析过了,getExtensionClasses方法获取接口的所有实现类,然后通过name获取对应的Class。紧接着通过clazz.newInstance()来实例化该实现类,调用injectExtension为实例注入属性。

private T createExtension(String name) {
    //getExtensionClasses方法之前已经分析过,获取所有的扩展类,然后根据扩展名获取对应的扩展类
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            EXTENSION_INSTANCES.putIfAbsent(clazz, (T) clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        //属性注入
        injectExtension(instance);
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (wrapperClasses != null && wrapperClasses.size() > 0) {
            for (Class<?> wrapperClass : wrapperClasses) {
                //包装类的创建及属性注入
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                type + ")  could not be instantiated: " + t.getMessage(), t);
    }
}

在方法的最后有一段对于WrapperClass包装类的处理逻辑,如果接口存在包装类实现,那么就会返回包装类实例。实现AOP的关键就是WrapperClass机制,判断一个扩展类是否是WrapperClass的依据,是看其constructor函数中是否包含当前接口参数。

如果有就认为是一个wrapperClass,最终创建的实例是一个经过多个wrapperClass层层包装的结果;在每个wrapperClass中都可以编入面向切面的代码,从而就简单实现了AOP功能。

Activate活性扩展

对应ExtensionLoader的getActivateExtension方法,根据多个过滤条件从extension集合中智能筛选出您所需的那一部分。

getActivateExtension方法

public List<T> getActivateExtension(URL url, String[] names, String group);

首先这个方法只会返回带有Activate注解的扩展类,但并非带有注解的扩展类都会被返回。

names是明确指定所需要的那部分扩展类,非明确指定的扩展类需要满足group过滤条件和Activate注解本身指定的key过滤条件,非明确指定的会按照Activate注解中指定的排序规则进行排序;

getActivateExtension的返回结果是上述两种扩展类的总和。

Activate注解类

*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Activate {
    /**
     * Group过滤条件。
     */
    String[] group() default {};
 
    /**
     * Key过滤条件。包含{@link ExtensionLoader#getActivateExtension}的URL的参数Key中有,则返回扩展。
     */
    String[] value() default {};
 
    /**
     * 排序信息,可以不提供。
     */
    String[] before() default {};
 
    /**
     * 排序信息,可以不提供。
     */
    String[] after() default {};
 
    /**
     * 排序信息,可以不提供。
     */
    int order() default 0;
}

活性Extension最典型的应用是rpc invoke时获取filter链条,各种filter有明确的执行优先级,同时也可以人为增添某些filter,filter还可以根据服务提供者和消费者进行分组过滤。

Dubbo invoke获取filter链条

List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), Constants.SERVICE_FILTER_KEY, Constants.PROVIDER);

以TokenFilter为例,其注解为@Activate(group = Constants.PROVIDER, value = Constants.TOKEN_KEY),表示该过滤器只在服务提供方才会被加载,同时会验证注册地址url中是否带了token参数,如果有token表示服务端注册时指明了要做token验证,自然就需要加载该filter。

反之则不用加载;此filter加载后的执行逻辑则是从url中获取服务端注册时预设的token,再从rpc请求的attachments中获取消费方设置的remote token,比较两者是否一致,若不一致抛出RPCExeption异常阻止消费方的正常调用。

五、总结

Dubbo 所有的接口几乎都预留了扩展点,根据用户参数来适配不同的实现。如果想增加新的接口实现,只需要按照SPI的规范增加配置文件,并指向新的实现即可。

用户配置的Dubbo属性都会体现在URL全局上下文参数中,URL贯穿了整个Dubbo架构,是Dubbo各个layer组件间相互调用的纽带。

总结一下 Dubbo SPI 相对于 Java SPI 的优势:

  • Dubbo的扩展机制设计默认值,每个扩展类都有自己的名称,方便查找。
  • Dubbo的扩展机制支持IOC,AOP等高级功能。
  • Dubbo的扩展机制能和第三方IOC容器兼容,默认支持Spring Bean,也可扩展支持其他容器。
  • Dubbo的扩展类通过@Adaptive注解实现了动态代理功能,更强大的是它可以通过一个proxy映射多个不同的扩展类。
  • Dubbo的扩展类通过@Activate注解实现了不同扩展类的分组、过滤、排序功能,能够更好的适配较复杂的业务场景。

作者: Xie Xiaopeng

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
9个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这