dubbo的SPI应用与原理

Posted 一叶飘舟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了dubbo的SPI应用与原理相关的知识,希望对你有一定的参考价值。

dubbo

SPI(Service Provider Interface)

  • 本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。

  • 在Java中SPI是被用来设计给服务提供商做插件使用的。基于策略模式 来实现动态加载的机制 。我们在程序只定义一个接口,具体的实现交个不同的服务提供者;在程序启动的时候,读取配置文件,由配置确定要调用哪一个实现;

  • 通过 SPI 机制为我们的程序提供拓展功能,在dubbo中,基于 SPI,我们可以很容易的对 Dubbo 进行拓展。例如dubbo当中的protocol,LoadBalance等都是通过SPI机制扩展。

想要学习 Dubbo 的源码,SPI 机制务必弄懂。接下来,我们了解下JAVA SPI与dubbo SPI的用法,再分析DUBBO SPI的源码,本文的dubbo源码是基于2.7.5版本。

JAVA 原生SPI 示例

  • 先简单介绍JAVA SPI的应用。首先,我们定义一个Car接口

public interface Car 
    String getBrand();
  • 定义该接口的两个实现类。

public class BM implements Car 
    public String getBrand() 
        System.out.println("BM car");
        return "BM";
    

 
public class Benz implements Car 
    public String getBrand() 
        System.out.println("benz car");
        return "Benz";
    

 
  • 再在resources下创建META-INF/services 文件夹,并创建一个文件,文件名称为Car接口的全限定名com.dubbo.dp.spi.Car。内容为接口实现类的全限定类名:

com.dubbo.dp.spi.Benz 
com.dubbo.dp.spi.BM
  • 使用如下,调用Car接口在配置文件中的所有实现类。

public class JavaSPITest 
    @Test
public void  sayHello() throws Exception 
        ServiceLoader serviceLoader = ServiceLoader.load(Car.class);
        serviceLoader.forEach(car -> System.out.println(car.getBrand()));
    

JAVA SPI实现了接口的定义与具体业务实现解耦,应用进程可以根据实际业务情况启用或替换具体组件。

举例:JAVA的java.sql包中就定义一个接口Driver,各个服务提供商实现该接口。当我们需要使用某个数据库时就导入相应的jar包。

缺点

  • 不能按需加载。Java SPI在加载扩展点的时候,会一次性加载所有可用的扩展点,很多是不需要的,会浪费系统资源;

  • 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。

  • 不支持AOP与依赖注入。

  • JAVA SPI可能会丢失加载扩展点异常信息,导致追踪问题很困难;

dubbo SPI示例

  • dubbo重新实现了一套功能更强的 SPI 机制,支持了AOP与依赖注入,并且利用缓存提高加载实现类的性能,同时支持实现类的灵活获取,文中接下来将讲述SPI的应用与原理。

Dubbo的SPI接口都会使用@SPI注解标识,该注解的主要作用就是标记这个接口是一个SPI接口。源码如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SPI 
/**
* default extension name
* 设置默认拓展类
*/
String value() default “”;
该注解只作用在接口上,value用来设置默认拓展类
  • 首先讲解下dubbo SPI的使用。在上述Car接口加上@SPI注解,它的实现类暂时不变。

@SPI
public interface Car 
    String getBrand();

META-INF/services 文件夹的配置文件的内容调整如下:

benz=com.dubbo.dp.spi.Benz 
bm=com.dubbo.dp.spi.BM

配置文件通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。使用如下:

public class JavaSPITest 
    @Test
    public void sayHello() throws Exception 
        ExtensionLoader<Car> carExtensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
        //按需获取实现类对象
        Car car = carExtensionLoader.getExtension("benz");
        System.out.println(car.getBrand());
    

输出结果为

benz car
Benz

Dubbo SPI 源码分析

在dubbo SPI示例方法中,我们首先通过ExtensionLoader的 getExtensionLoader 方法获取一个接口的 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象,源码如下,首先是getExtensionLoader 方法:

    /**
     * 扩展类加载器缓存,就是扩展点ExtendsLoader实例缓存; key=扩展接口 value=扩展类加载器
     */
    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();
 
 
    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) 
        //校验传进的type类是否为空
        if (type == null) 
            throw new IllegalArgumentException("Extension type == null");
        
        //校验传进的type类是否为接口
        if (!type.isInterface()) 
            throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
        
        //校验传进的type类是否有@SPI注解
        if (!withExtensionAnnotation(type)) 
            throw new IllegalArgumentException("Extension type (" + type +
                    ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
        
        //从ExtensionLoader缓存中查询是否已经存在对应类型的ExtensionLoader实例
        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) 
            //没有就new一个ExtensionLoader实例,并存入本地缓存
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        
        return loader;
    

getExtensionLoader会对传进的接口进行校验,其中包括是否有@SPI注解校验,这也是在接口上需加@SPI的原因。然后从EXTENSION_LOADERS缓存中获取该接口类型的ExtensionLoader,如果获取不到,则创建一个该接口类型的ExtensionLoader放入到缓存中,并返回该ExtensionLoader。

注意:

这里创建ExtensionLoader对象的构造方法如下:ExtensionLoader.getExtensionLoader获取ExtensionFactory接口的拓展类,再通过getAdaptiveExtension从拓展类中获取目标拓展类。它会设置该接口对应的 objectFactory常量为AdaptiveExtensionFactory。因为AdaptiveExtensionFactory类上加了@Adaptive注解,为什么是AdaptiveExtensionFactory原因在之后的文章会解释,且objectFactory也会在后面用到。

private ExtensionLoader(Class<?> type) 
        this.type = type;
        //type通常不为ExtensionFactory类,则objectFactory为ExtensionFactory接口的默认扩展类AdaptiveExtensionFactory
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    

当通过ExtensionLoader.getExtensionLoader取到接口的加载器Loader之后,在通过getExtension方法获取需要拓展类对象。该方法的整个执行流程如下图所示

参照执行流程图,拓展类对象的获取源码如下:

/**
    * 扩展点实例缓存 key=扩展点名称,value=扩展实例的Holder实例
    */
    private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
 
    /**
     * 获取接口拓展类实例
     * 1.检查缓存中是否存在
     * 2.创建并返回拓展类实例
     * @param name  需要获取的配置文件中拓展类的key
     * @return
     */
 public T getExtension(String name) 
        if (StringUtils.isEmpty(name)) 
            throw new IllegalArgumentException("Extension name == null");
        
        if ("true".equals(name)) 
            // 获取默认的拓展实现类,即@SPI注解上的默认实现类, 如@SPI("benz")
            return getDefaultExtension();
        
        // Holder,顾名思义,用于持有目标对象,从缓存中拿,没有则创建
        final Holder<Object> holder = getOrCreateHolder(name);
        Object instance = holder.get();
        // 双重检查
        if (instance == null) 
            synchronized (holder) 
                instance = holder.get();
                if (instance == null) 
                    // 创建拓展实例
                    instance = createExtension(name);
                    // 设置实例到 holder 中
                    holder.set(instance);
                
            
        
        return (T) instance;
    
 
    /**
     * 获取或者创建一个Holder对象
     */
    private Holder<Object> getOrCreateHolder(String name) 
        // 首先通过扩展名从扩展实例缓存中获取Holder对象
        Holder<Object> holder = cachedInstances.get(name);
        if (holder == null) 
            //如果没有获取到就new一个空的Holder实例存入缓存
            cachedInstances.putIfAbsent(name, new Holder<>());
            holder = cachedInstances.get(name);
        
        return holder;
    

上面代码的逻辑比较简单,首先检查缓存,缓存未命中则创建拓展对象。dubbo中包含了大量的扩展点缓存。这个就是典型的使用空间换时间的做法。也是Dubbo性能强劲的原因之一,包括

  1. 扩展点Class缓存 ,Dubbo SPI在获取扩展点时,会优先从缓存中读取,如果缓存中不存在则加载配置文件,根据配置将Class缓存到内存中,并不会直接初始化。

  1. 扩展点实例缓存 ,Dubbo不仅会缓存Class,还会缓存Class的实例。每次取实例的时候会优先从缓存中取,取不到则从配置中加载,实例化并缓存到内存中。

下面我们来看一下创建拓展对象的过程

/**
     * 扩展实例存入内存中缓存起来; key=扩展类 ; value=扩展类实例
     */
    private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
 
    /**
     * 创建拓展类实例,包含如下步骤
     * 1. 通过 getExtensionClasses 获取所有的拓展类,从配置文件加载获取拓展类的map映射
     * 2. 通过反射创建拓展对象
     * 3. 向拓展对象中注入依赖(IOC)
     * 4. 将拓展对象包裹在相应的 Wrapper 对象中(AOP)
     * @param name 需要获取的配置文件中拓展类的key
     * @return 拓展类实例
     */
    private T createExtension(String name) 
        // 从配置文件中加载所有的拓展类,可得到“配置项名称”到“配置类”的map,再根据拓展项名称从map中取出相应的拓展类即可
        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, clazz.newInstance());
                //然后从缓存中获取对应实例
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            
            // 向实例中注入依赖,通过setter方法自动注入对应的属性实例
            injectExtension(instance);
            //从缓存中取出所有的包装类,形成包装链
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
            if (CollectionUtils.isNotEmpty(wrapperClasses)) 
                // 循环创建 Wrapper 实例,形成Wrapper包装链
                for (Class<?> wrapperClass : wrapperClasses) 
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                
            
            //初始化实例并返回
            initExtension(instance);
            return instance;
         catch (Throwable t) 
            throw new IllegalStateException(".....");
        
    

创建拓展类对象步骤分别为:

  1. 通过 getExtensionClasses 从配置文件中加载所有的拓展类,再通过名称获取目标拓展类

  1. 通过反射创建拓展对象

  1. 向拓展对象中注入依赖

  1. 将拓展对象包裹在相应的 Wrapper 对象中

第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。我们先重点分析getExtensionClasses 方法的逻辑。

从配置文件中加载所有的拓展类

  • 在通过name获取拓展类之前,首先需要根据配置文件解析出拓展项名称与拓展类的映射map,之后再根据拓展项名称从map中取出相应的拓展类即可。getExtensionClasses 方法源码如下

    /**
     * 扩展点Class缓存 key=扩展名 ,value=对应的class对象
     */
    private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();

  /**
     * 解析配置文件中接口的拓展项名称与拓展类的映射表map    * @return
     */
    private Map<String, Class<?>> getExtensionClasses() 
        // 从缓存中获取已加载的拓展点class
        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 加载拓展类,缓存避免了多次读取配置文件的耗时。

下面分析loadExtensionClasses方法加载配置文件的逻辑

private Map<String, Class<?>> loadExtensionClasses() 
    //获取并缓存接口的@SPI注解上的默认实现类,@SPI("value")中的value
    cacheDefaultExtensionName();

    Map<String, Class<?>> extensionClasses = new HashMap<>();
    // 加载指定文件夹下的配置文件,常量包含META-INF/dubbo/internal/,META-INF/dubbo/,META-INF/services/三个文件夹
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName(), true);
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"), true);

    // 加载 META-INF/dubbo/ 目录下当前扩展点对应的具体实现类列表
    loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
    loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));

    // 加载 META-INF/services/ 目录下当前扩展点对应的具体实现类列表
    loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
    loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));

    // 返回别名-扩展点实现类Map,这里如果别名一致会覆盖,后来的会覆盖前面的
    return extensionClasses;

loadExtensionClasses 方法总共做了两件事情。

首先该方法调用cacheDefaultExtensionName对 SPI注解进行解析,获取并缓存接口的@SPI注解上的默认拓展类在cachedDefaultName。

再调用 loadDirectory方法加载指定文件夹配置文件。

SPI 注解解析过程比较简单,源码如下。只允许一个默认拓展类。

private void cacheDefaultExtensionName() 
        // 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入,代表接口类
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if (defaultAnnotation == null) 
            return;
        
 
        String value = defaultAnnotation.value();
        if ((value = value.trim()).length() > 0) 
            String[] names = NAME_SEPARATOR.split(value);
            // 检测 SPI 注解内容是否合法(至多一个默认实现类),不合法则抛出异常
            if (names.length > 1) 
                throw new IllegalStateException("...");
            
            // 设置默认拓展类名称
            if (names.length == 1) 
                cachedDefaultName = names[0];
            
        
    
 

从源码中可以看出loadExtensionClasses方法加载配置文件的路径有3个,分别为META-INF/dubbo/internal/,META-INF/dubbo/,META-INF/services/三个文件夹。方法源码如下:

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) 
        loadDirectory(extensionClasses, dir, type, false);
    
    /**
     * 加载配置文件内容
     * @param extensionClasses 拓展类map
     * @param dir 文件夹路径
     * @param type 接口名称
     * @param extensionLoaderClassLoaderFirst 是否先加载ExtensionLoader的ClassLoader
     */
    private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst) 
        // fileName = 文件夹路径 + type 全限定名
        String fileName = dir + type;
        try 
            Enumeration<java.net.URL> urls = null;
            //获取当前线程的类加载器
            ClassLoader classLoader = findClassLoader();
 
            // try to load from ExtensionLoader's ClassLoader first
            if (extensionLoaderClassLoaderFirst) 
                //获取加载ExtensionLoader.class这个类的类加载器
                ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
                //如果extensionLoaderClassLoaderFirst=true时,且这两个类加载器不同,就优先使用 extensionLoaderClassLoader
                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();
                    // 解析并加载配置文件中配置的实现类到extensionClasses中去
                    loadResource(extensionClasses, classLoader, resourceURL);
                
            
         catch (Throwable t) 
            logger.error("").", t);
        
    

首先找到文件夹下的配置文件,文件名需为接口全限定名。利用类加载器获取文件资源链接,再解析配置文件中配置的实现类添加到extensionClasses中。我们继续看loadResource是如何加载资源的。

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) 
        try 
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) 
                String line;
                // 按行读取配置内容
                while ((line = reader.readLine()) != null) 
                    final int ci = line.indexOf('#');
                    if (ci >= 0) 
                        // 截取 # 之前的字符串,# 之后的内容为注释,需要忽略
                        line = line.substring(0, ci);
                    
                    line = line.trim();
                    if (line.length() > 0) 
                        try 
                            String name = null;
                            int i = line.indexOf('=');
                            if (i > 0) 
                                // 以等于号 = 为界,截取键与值
                                name = line.substring(0, i).trim();
                                line = line.substring(i + 1).trim();
                            
                            if (line.length() > 0) 
                                // 通过反射加载类,并通过 loadClass 方法对类进行缓存
                                loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
                            
                         catch (Throwable t) 
                            .....
                        
                    
                
            
         catch (Throwable t) 
            logger.error(....);
        
    

loadResource 方法用于读取和解析配置文件,按行读取配置文件,每行以等于号 = 为界,截取键与值,并通过反射加载类,最后通过loadClass方法加载扩展点实现类的class到map中,并对加载到的class进行分类缓存。loadClass方法实现如下

/**
     * 加载扩展点实现类的class到map中,并对加载到的class进行分类缓存
     * 比如 cachedAdaptiveClass、cachedWrapperClasses 和 cachedNames 等等
     * @param extensionClasses 装载配置文件类的容器
     * @param resourceURL 配置文件资源URL
     * @param clazz 扩展点实现类的class
     * @param name  扩展点实现类的名称,配置文件一行中的key
     * @throws NoSuchMethodException
     */
    private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException 
        //判断配置的实现类是否是实现了type接口
        if (!type.isAssignableFrom(clazz)) 
            throw new IllegalStateException("...");
        
 
        //根据配置中实现类的类型来分类缓存起来
        // 检测目标类上是否有 Adaptive 注解,表示这个类就是一个自适应实现类,缓存到cachedAdaptiveClass
        if (clazz.isAnnotationPresent(Adaptive.class)) 
            cacheAdaptiveClass(clazz);
        // 检测 clazz 是否是 Wrapper 类型,判断依据是是否有参数为该接口类的构造方法,缓存到cachedWrapperClasses
         else if (isWrapperClass(clazz)) 
            cacheWrapperClass(clazz);
         else 
            // 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常
            clazz.getConstructor();
            // 如果配置文件中key的name 为空,则尝试从Extension注解中获取 name,或使用小写的类名作为name。
            // 已经弃用,就不在讨论这种方式
            if (StringUtils.isEmpty(name)) 
                name = findAnnotationName(clazz);
                if (name.length() == 0) 
                    throw new IllegalStateException("...");
                
            
            //使用逗号将name分割为字符串数组
            String[] names = NAME_SEPARATOR.split(name);
            if (ArrayUtils.isNotEmpty(names)) 
                //如果扩展点配置的实现类使用了@Activate注解,就将对应的注解信息缓存起来
                cacheActivateClass(clazz, names[0]);
                for (String n : names) 
                    //缓存扩展点实现类class和扩展点名称的对应关系
                    cacheName(clazz, n);
                    //最后将class存入extensionClasses
                    saveInExtensionClass(extensionClasses, clazz, n);
                
            
        
    

loadClass方法实现了扩展点的分类缓存功能,如包装类,自适应扩展点实现类,普通扩展点实现类等分别进行缓存。需要注意的是自适应扩展点实现类@Adaptive注解,该注解源码如下

*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>
     * @return
     */
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE, ElementType.METHOD)
    public @interface Adaptive 
        String[] value() default ;
    
 

该注解的作用是决定哪个自适应拓展类被注入,该目标拓展类是由URL中的参数决定,URL中参数key由该注解的value给出,该key的value作为目标拓展类名称。

  • 如果注解中有多个值,则根据下标从小到大去URL中查找有无对应的key,一旦找到就用该key的value作为目标拓展类名称。

  • 如果这些值在url中都没有对应的key,使用spi上的默认值。

@Adaptive注解可以作用的类上与方法上,绝大部分情况下,该注解是作用在方法上,当 Adaptive 注解在类上时,Dubbo 不会为该类生成代理类。注解在方法(接口方法)上时,Dubbo 则会为该方法生成代理类Adaptive 注解在接口方法上,表示拓展的加载逻辑需由框架自动生成。注解在类上,表示拓展的加载逻辑由人工编码完成。

上述的loadClass扫描的是作用在类上。在 Dubbo 中,仅有两个类被@Adaptive注解了,分别是 AdaptiveCompiler 和 AdaptiveExtensionFactory。

loadClass方法设置缓存cacheAdaptiveClass会导致接口的cacheAdaptiveClass不为空,后面都会默认用这个拓展类,优先级最高。

回到主线,当执行完loadClass方法,配置文件中的所有拓展类已经被加载到map中,到此,关于缓存类加载的过程就分析完了。

Dubbo IOC

当getExtensionClasses()方法执行流程完成后,再根据拓展项name从map中取出相应的拓展类即可获取扩展类Class,通过反射创建实例,并通过injectExtension(instance);方法向实例中注入依赖。

injectExtension()通过获取set方法参数与名称,获取对应注入类实例,反射注入依赖对象

private T injectExtension(T instance) 
    if (objectFactory == null) 
        return instance;
    

    try 
        // 遍历实现类中的所有方法
        for (Method method : instance.getClass().getMethods()) 
            // 如果不是set方法,则继续遍历
            if (!isSetter(method)) 
                continue;
            

            // 如果方法上存在@DisableInject注解,则继续遍历
            if (method.getAnnotation(DisableInject.class) != null) 
                continue;
            

            // 获取set方法的第一个参数的类型
            Class<?> pt = method.getParameterTypes()[0];
            // 如果该参数类型是基础数据类型,则继续遍历
            if (ReflectUtils.isPrimitives(pt)) 
                continue;
            

            try 
                // 解析set后的名称,setCarService =》 carService
                String property = getSetterProperty(method);
                // 根据解析后的名称去工厂中获取对应的扩展类对象
                Object object = objectFactory.getExtension(pt, property);
                if (object != null) 
                    // 通过反射调用set方法进行注入
                    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;

下面是initExtension()方法的源码,调用实现类对象的初始化方法进行初始化

private void initExtension(T instance) 
    // 检测是否实现了Lifecycle接口
    if (instance instanceof Lifecycle) 
        Lifecycle lifecycle = (Lifecycle) instance;
        // 如果实现了,则调用其初始化方法
        lifecycle.initialize();
    

DUBBO AOP

当执行完injectExtension(T instance)方法,在createExtension(String name)就开始执行wrapper的包装,类似于spring中的AOP,dubbo运用了装饰器模式。

Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (CollectionUtils.isNotEmpty(wrapperClasses)) 
            // 循环创建 Wrapper 实例,形成Wrapper包装链
            for (Class<?> wrapperClass : wrapperClasses) 
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
            
        

这里的cachedWrapperClasses通过前面的分析已经知道,就是在解析配置文件时判断是否是Wrapper类型的拓展类,++判断依据为构造方法中是否有参数为该接口类++,则缓存到cachedWrapperClasses。

执行wrapperClass.getConstructor(type).newInstance(instance)将获取包装类的构造方法,方法的参数就是该接口类型,并通过反射生成一个包含该拓展类实例的包装对象,再通过injectExtension注入包装对象的依赖。如此循环,得到成Wrapper包装链。这里需注意的是,配置文件中内容靠后的包装类会包装在相对外层。下面是DUBBO AOP的例子,我们继续使用上面的Car接口与实现类,同时新增一个实现类,代码如下

public class CarWrapper implements Car
 
    private Car car;
 
    /**
     * 有一个包含该接口类参数的构造方法
     */
    public CarWrapper(Car car) 
        this.car = car;
    
 
    @Override
    public String getBrand() 
        System.out.println("校验");
        String result = car.getBrand();
        System.out.println("记录日志");
        return result;
    

该接口实现了Car,并且持有一个Car对象,同时拥有一个构造方法且该构造方法的参数为Car接口类型,那么该类会被识别为接口的Wrapper类。则可以在方法中做一些切面功能的扩展,再调用car对象执行其方法实现AOP功能。

将配置文件内容中添加wrapper实现类,如下

benz=com.xiaoju.automarket.energy.scm.rpc.Benz
bm=com.xiaoju.automarket.energy.scm.rpc.BM
com.xiaoju.automarket.energy.scm.rpc.CarWrapper #包装类

执行如下代码获取benz的拓展类实例后,调用其方法,将会被Wrapper包装类

public class TestAOP 
    public static void main(String[] args) 
        ExtensionLoader<Car> carExtensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
        Car car = carExtensionLoader.getExtension("benz");
        System.out.println(car.getBrand(null));
    

结果如下

校验
benz car
记录日志
Benz

与我们预想的一致,实现了Wrapper类的切面功能。

总结

本篇简单分别介绍了 Java SPI 与 Dubbo SPI 用法,并对 Dubbo SPI 的加载拓展类的过程进行了分析。同时分析了Dubbo AOP的实现原理。如果文章中有错误不妥之处,希望大家指正。

以上是关于dubbo的SPI应用与原理的主要内容,如果未能解决你的问题,请参考以下文章

java SPI 05-dubbo adaptive extension 自适应拓展

Dubbo面试

Java SPI 与 dubbo SPI

Dubbo 扩展点加载机制:从 Java SPI 到 Dubbo SPI

Dubbo-扩展机制-SPI

2. Dubbo原理解析-Dubbo内核实现之基于SPI思想Dubbo内核实现(转)