Apache Dubbo系列:增强SPI

Dubbo良好的扩展性与两个方面是密不可分的,一是Dubbo整体架构中,在合适的场景中巧妙的使用了设计模式,二是使用Dubbo SPI机制,使Dubbo的接口与实现完全解耦。

在本次分享中,您可以了解如下知识点

  • Java SPI机制

  • Java SPI机制的缺点

  • Dubbo SPI配置规范

  • Dubbo SPI的分类与缓存

  • Dubbo SPI的特点

  • Dubbo SPI源码分析

  • Dubbo对IOC的支持

Java SPI机制

SPI,全称Service Provider Interface,起初是提供给厂商做定制化插件开发的。Java SPI使用策略模式,一个接口多种实现,我们只提供接口,具体实现并不在程序中直接确定,而是写在配置文件里。具体使用步骤如下:

1、定义一个接口和对应的方法。

package com.wb.service;
public interface OrderService {
    void doOrder();
}

2、编写该接口的实现类,可以有多种实现。


package com.wb.service.impl;
import com.wb.service.OrderService;
public class OrderServiceImpl01 implements OrderService {
    @Override
    public void doOrder() {
        System.out.println("OrderServiceImpl01 run");
    }
public class OrderServiceImpl02 implements OrderService {
    @Override
    public void doOrder() {
        System.out.println("OrderServiceImpl02 run");
    }
}

3、在META-INF/services目录下新建一个文件,文件名是接口的全路径,如com.xxx.OrderService。

4、文件的具体内容是接口的实现类的全路径名称,如果接口有多个实现类,则换行编写。

com.wb.service.impl.OrderServiceImpl01
com.wb.service.impl.OrderServiceImpl02

5、在业务代码中使用java.util.ServiceLoader加载具体的实现类。


import com.wb.service.OrderService;
import java.util.ServiceLoader;
public class Test {
    public static void main(String[] args)  {
        ServiceLoader<OrderService> loader = ServiceLoader.load(OrderService.class);
        for (OrderService orderService : loader) {
            orderService.doOrder();
        }
    }
}
输出结果:
OrderServiceImpl01 run
OrderServiceImpl02 run

Java SPI机制的缺点

Dubbo为什么不使用Java提供的SPI机制,而使用自己的SP?相对于Java SPI,Dubbo SPI做了一定的优化和改进。

1、Java SPI会一次性加载所有的扩展类,如果扩展类没有被使用到也会加载,很耗时。

2、如果扩展类加载失败,则连扩展的名称都获取不到,排查错误困难。

3、增加了对IOC和AOP的支持,一个扩展类可以通过setter方法注入到其他扩展类中。

Dubbo SPI机制

Dubbo SPI配置规范

Dubbo SPI和Java SPI类似,需要在META-INF/dubbo目录下放置相应的SPI配置文件,文件名是接口的全路径名,文件内容为key=扩展点实现类的全路径名,如果有多个实现类,则用换行符分割。其中,key是Dubbo SPI注解中传入的参数。另外,Dubbo SPI兼容了Java SPI,Dubbo在启动时会扫描META-INF/services/,META-INF/dubbo/、META-INF/dubbo/internal/三个路径下的SPI配置。详情可查看org.apache.dubbo.remoting.Transporter接口的实现类org.apache.dubbo.remoting.transport.netty4.NettyTransporter的配置,此处就不一一展开。

Dubbo SPI的分类与缓存

Dubbo SPI根据缓存类型可以分为以下两类:

  • Class缓存

    Dubbo SPI获取扩展类配置时,先从缓存中获取,如果缓存中没有,则加载配置文件,根据配置把Class缓存起来,但不会全部初始化。

  • 实例缓存

    基于性能考虑,Dubbo SPI不仅会缓存Class,也会缓存Class的实例对象。每次获取时先从缓存中获取,如果缓存中没有,则重新加载并缓存。这也是Dubbo SPI比Java SPI性能高的原因之一,按需加载。

也可以根据扩展类的种类分为以下类型:

  • 普通扩展类

    最基础的扩展类,在SPI中配置的扩展类。

  • 包装扩展类(Wrapper类)

    如果该类的构造方法里有普通扩展类作为参数,Dubbo会判定为扩展点 Wrapper 类。

  • 自适应扩展类(Adaptive类)

    这样的扩展接口有多种实现,具体使用哪个类不写死在配置中,而是通过URL参数动态确定(此处不展开解释)。

Dubbo SPI的特点

  • 自动包装特性

    如果该类的构造方法里有普通扩展类作为参数,Dubbo会判定为扩展点 Wrapper 类。

  • 自动加载特性

    如果该扩展类的成员变量有其他扩展类作为属性,并且拥有setter方法,那么Dubbo也会注入对应的扩展点实例(类似于Spring的依赖注入)。

  • 自适应

    在Dubbo中使用@Adaptive注解,我们可以在URL中动态的传入参数来确定具体使用哪个实现类。

Dubbo SPI源码分析

与Java SPI代码类似,Dubbo官网中有这么一段示例


public class DubboSPITest {
    @Test
    public void sayHello() throws Exception {
        ExtensionLoader<Robot> extensionLoader = 
            ExtensionLoader.getExtensionLoader(Robot.class);
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
        optimusPrime.sayHello();
        Robot bumblebee = extensionLoader.getExtension("bumblebee");
        bumblebee.sayHello();
    }
}

所以我们阅读源码的切入点是org.apache.dubbo.common.extension.ExtensionLoader#getExtension方法。


public T getExtension(String name) {
    if (StringUtils.isEmpty(name)) {
        throw new IllegalArgumentException("Extension name == null");
    }
    // 获取默认的拓展实现类
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    // Holder,顾名思义,用于持有目标对象。通过name从缓存中获取
    final Holder<Object> holder = getOrCreateHolder(name);
    Object instance = holder.get();
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            // 如果缓存为空,则创建实例并设置到Holder中
            if (instance == null) {
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

上面代码的逻辑比较简单,首先检查缓存,缓存未命中则创建拓展对象。下面我们来看一下创建扩展对象的过程是怎样的。

org.apache.dubbo.common.extension.ExtensionLoader#createExtension


private T createExtension(String name) {
    // 从配置文件中加载所有的拓展类
    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方法,向实例中注入依赖,体现了Dubbo SPI的自动加载特性
        injectExtension(instance);
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (CollectionUtils.isNotEmpty(wrapperClasses)) {
            // 循环创建 Wrapper 实例,体现出Dubbo SPI的自动包装特性
            for (Class<?> wrapperClass : wrapperClasses) {
                // 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。
                // 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        initExtension(instance);
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                type + ") couldn't be instantiated: " + t.getMessage(), t);
    }
}

createExtension 方法的逻辑稍复杂一下,包含了如下的步骤:

  • 通过 getExtensionClasses 获取所有的拓展类

  • 通过反射创建拓展对象

  • 向拓展对象中注入依赖

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

以上步骤中,第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。接下来,将会重点分析 getExtensionClasses 方法的逻辑,以及简单介绍 Dubbo IOC 的具体实现。

org.apache.dubbo.common.extension.ExtensionLoader#getExtensionClasses

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 方法的逻辑

org.apache.dubbo.common.extension.ExtensionLoader#loadExtensionClasses

private Map<String, Class<?>> loadExtensionClasses() {
    // 获取SPI注解
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    String value = defaultAnnotation.value();
    if ((value = value.trim()).length() > 0) {
        // 检测 SPI 注解内容是否合法,不合法则抛出异常
        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<>();
    // 加载指定文件夹下的配置文件
    for (LoadingStrategy strategy : strategies) {
        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.excludedPackages());
        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.excludedPackages());
    }
    return extensionClasses;
}

loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 loadDirectory 做了哪些事情。


private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type,
                           boolean extensionLoaderClassLoaderFirst, String... excludedPackages) {
    // 拼接文件名
    String fileName = dir + type;
    try {
        Enumeration<java.net.URL> urls = null;
        ClassLoader classLoader = findClassLoader();

        // 根据文件名加载所有的同名文件
        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, excludedPackages);
            }
        }
    } catch (Throwable t) {
        logger.error("...");
    }
}

loadDirectory 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
                          java.net.URL resourceURL, String... excludedPackages) {
    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;
                        // 通过=分割key和value
                        int i = line.indexOf('=');
                        if (i > 0) {
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0 && !isExcluded(line, excludedPackages)) {
                            // 加载类,并通过 loadClass 方法对类进行缓存
                            loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
                        }
                    } catch (Throwable t) {
                        IllegalStateException e = new IllegalStateException("...");
                        exceptions.put(line, e);
                    }
                }
            }
        }
    } catch (Throwable t) {
        logger.error("...");
    }
}

loadResource方法用于读取配置文件,最后通过loadClass方法对结果进行缓存,loadClass方法代码如下:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("Error occurred when loading extension class (interface: " +
                type + ", class line: " + clazz.getName() + "), class "
                + clazz.getName() + " is not subtype of interface.");
    }
    // 检查类是否有@Adaptive注解
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        cacheAdaptiveClass(clazz);
    } else if (isWrapperClass(clazz)) {
        // 检查类是否是包装类
        cacheWrapperClass(clazz);
    } else {
        // 走到这一分支,是普通扩展剋
        clazz.getConstructor();
        if (StringUtils.isEmpty(name)) {
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
            }
        }

        String[] names = NAME_SEPARATOR.split(name);
        if (ArrayUtils.isNotEmpty(names)) {
            // 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键,
            // 存储 name 到 Activate 注解对象的映射关系
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                // 存储 Class 到名称的映射关系
                cacheName(clazz, n);
                // 存储名称到 Class 的映射关系
                saveInExtensionClass(extensionClasses, clazz, n);
            }
        }
    }
}

Dubbo对IOC的支持

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

org.apache.dubbo.common.extension.ExtensionLoader#injectExtension

private T injectExtension(T instance) {
    if (objectFactory == null) {
        return instance;
    }
    try {
        // 获取所有setter方法
        for (Method method : instance.getClass().getMethods()) {
            if (!isSetter(method)) {
                continue;
            }
            if (method.getAnnotation(DisableInject.class) != null) {
                continue;
            }
            // 获取 setter 方法参数类型
            Class<?> pt = method.getParameterTypes()[0];
            if (ReflectUtils.isPrimitives(pt)) {
                continue;
            }
            try {
                String property = getSetterProperty(method);
                // 从 ObjectFactory 中获取依赖对象
                Object object = objectFactory.getExtension(pt, property);
                if (object != null) {
                    // 通过反射调用 setter 方法设置依赖
                    method.invoke(instance, object);
                }
            } catch (Exception e) {
                logger.error("...");
            }
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;
}

好了,这是我读Dubbo官网所理解的知识点,今天的分享就到这里,下期再见。

扫描二维码,加作者微信,更多精彩

猜你喜欢

转载自blog.csdn.net/nuoWei_SenLin/article/details/107601612