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

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

SPI 全称为 Service Provider Interface,是一种服务发现机制。当程序运行挪用接口时,会凭据设置文件或默认规则信息加载对应的实现类。以是在程序中并没有直接指定使用接口的哪个实现,而是在外部举行装配。
要想领会 Dubbo 的设计与实现,其中 Dubbo SPI 加载机制是必须领会的,在 Dubbo 中有大量功效的实现都是基于 Dubbo SPI 实现解耦,同时也使得 Dubbo 获得云云好的可扩展性。

Java SPI

通过完成一个 Java SPI 的操作来领会它的机制。

  • 建立一个 AnimalService 接口及 category 方式
  • 建立一个实现类 Cat
  • 建立 META-INF/services 目录,并在该目录下建立一个文件,文件名为 AnimalService 的全限制名作为文件名
  • 在文件中添加实现类 Cat 的全限制名

Animal 接口

public interface AnimalService {
    void category();
}

Cat 实现类

public class Cat implements AnimalService {

    @Override
    public void category() {
        System.out.println("cat: Meow ~");
    }
}

在 META-INF/services 目录下的 top.ytao.demo.spi.AnimalService 文件中添加:

top.ytao.demo.spi.Cat

加载 SPI 的实现:

public class JavaSPITest {

    @Test
    public void javaSPI() throws Exception {
        ServiceLoader<AnimalService> serviceLoader = ServiceLoader.load(AnimalService.class);
        // 遍历在设置文件中已设置的 AnimalService 的所有实现类
        for (AnimalService animalService : serviceLoader) {
            animalService.category();
        }
    }

}

执行效果:

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

就这样,一个 Java SPI 就实现完成了,通过 ServiceLoader.load 获取加载所有接口已设置的接口实现类,然后可以遍历找出需要的实现。

Dubbo SPI

本文 Dubbo 版本为2.7.5
Dubbo SPI 相较于 Java SPI 更为壮大,而且都是由自己实现的一套 SPI 机制。其中主要的改善和优化:

  • 相对于 Java SPI 一次性加载所有实现,Dubbo SPI 是按需加载,只加载需要使用的实现类。同时带有缓存支持。
  • 更为详细的扩展加载失败信息。
  • 增添了对扩展 IOC 和 AOP的支持。

Dubbo SPI 示例

Dubbo SPI 的设置文件放在 META-INF/dubbo 下面,而且实现类的设置方式接纳 K-V 的方式,key 为实例化工具传入的参数,value 为扩展点实现类全限制名。例如 Cat 的设置文件内容:

cat = top.ytao.demo.spi.Cat

Dubbo SPI 加载历程中,对 Java SPI 的目录也是可以被兼容的。

同时需要在接口上增添 @SPI 注解,@SPI 中可以指定 key 值,加载 SPI 如下:

public class DubboSPITest {

    @Test
    public void dubboSPI(){
        ExtensionLoader<AnimalService> extensionLoader = ExtensionLoader.getExtensionLoader(AnimalService.class);
        // 获取扩展类实现
        AnimalService cat = extensionLoader.getExtension("cat");
        System.out.println("Dubbo SPI");
        cat.category();
    }

}

执行效果如下:

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

获取 ExtensionLoader 实例

获取 ExtensionLoader 实例是通过上面 getExtensionLoader 方式,详细实现代码:

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    if (type == null) {
        throw new IllegalArgumentException("Extension type == null");
    }
    // 检查 type 必须为接口
    if (!type.isInterface()) {
        throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
    }
    // 检查接口是否有 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<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    if (loader == null) {
        // 加载 ExtensionLoader 实例到缓存中
        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    }
    return loader;
}

上面获取扩展类加载器历程主要是检查传入的 type 是否正当,以及从扩展类加载器缓存中是否存在当前类型的接口,若是不存在则添加当前接口至缓存中。
ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS 是扩展类加载器的缓存,它是以接口作为 key, 扩展类加载器作为 value 举行缓存。

获取扩展类工具

获取扩展类工具的方式ExtensionLoader#getExtension,在这里完成扩展工具的缓存及建立事情:

public T getExtension(String name) {
    if (StringUtils.isEmpty(name)) {
        throw new IllegalArgumentException("Extension name == null");
    }
    // 若是传入的参数为 true ,则获取默认扩展类工具操作
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    // 获取扩展工具,Holder 里的 value 属性保存着扩展工具实例
    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.set(instance);
            }
        }
    }
    return (T) instance;
}

获取 holder 工具是从缓存ConcurrentMap<String, Holder<Object>> cachedInstances中获取,若是不存在,则以扩展名 key,建立一个 Holder 工具作为 value,设置到扩展工具缓存。
若是是新建立的扩展工具实例,那么 holder.get() 一定是 null ,扩展工具为空时,经由双重检查锁,建立扩展工具。

告辞炼丹,Google Brain提出强化学习助力Neural Architecture Search | ICLR2017

建立扩展工具

建立扩展工具历程:

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);
        }
        // 向当前实例注入依赖
        injectExtension(instance);
        // 获取包装扩展类缓存
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (CollectionUtils.isNotEmpty(wrapperClasses)) {
            for (Class<?> wrapperClass : wrapperClasses) {
                // 建立包装扩展类实例,并向其注入依赖
                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);
    }
}

上面建立扩展历程中,内里有个 Wrapper 类,这里使用到装饰器模式,该类是没有详细的实现,而是把通用逻辑举行抽象。
建立这个历程是从所有扩展类中获取当前扩展名对应映射关系的扩展类,以及向当前扩展工具注入依赖。

获取所有扩展类:

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中:


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/";

private Map<String, Class<?>> loadExtensionClasses() {
    // 获取 @SPI 上的默认扩展名
    cacheDefaultExtensionName();

    Map<String, Class<?>> extensionClasses = new HashMap<>();
    // 先加载 Dubbo 内部的扩展类, 通过 Boolean 值控制
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName(), true);
    // 由于 Dubbo 迁到 apache ,以是包名有转变,会替换之前的 alibaba 为 apache
    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;
}

上面获取 @SPI 扩展名,以及指定要加载的文件。从上面静态常量中,我们可以看到,Dubbo SPI 也是支持加载 Java SPI 的目录,同时还加载 META-INF/dubbo/internal (该目录为 Dubbo 的内部扩展类目录),在 loadDirectory 加载目录设置文件。

    private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst) {
        // 获取文件在项目中的路径,如:META-INF/dubbo/top.ytao.demo.spi.AnimalService
        String fileName = dir + type;
        try {
            Enumeration<java.net.URL> urls = null;
            ClassLoader classLoader = findClassLoader();
            
            // 加载内部扩展类
            if (extensionLoaderClassLoaderFirst) {
                ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
                if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
                    urls = extensionLoaderClassLoader.getResources(fileName);
                }
            }
            
            // 加载当前 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);
        }
    }

这里获取文件名后加载所有同名文件,然后迭代各个文件,逐个加载文件内容。

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('=');
                        // 若是当前行存在 "=",将 "=" 左右的值离开复制给 name 和 line
                        if (i > 0) {
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0) {
                            // 加载扩展类
                            loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
                        }
                    } catch (Throwable t) {
                        IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
                        exceptions.put(line, e);
                    }
                }
            }
        }
    } catch (Throwable t) {
        logger.error("Exception occurred when loading extension class (interface: " +
                type + ", class file: " + resourceURL + ") in " + resourceURL, t);
    }
}

上面代码完成文件内容加载和剖析,接下来通过 loadClass 加载扩展类。

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("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);
    // 当前类是否为 Wrapper 包装扩展类 
    } else if (isWrapperClass(clazz)) {
        cacheWrapperClass(clazz);
    } else {
        // 实验当前类是否有无参组织方式
        clazz.getConstructor();
        
        if (StringUtils.isEmpty(name)) {
            // 若是 name 为空,则获取 clazz 的 @Extension 注解的值,若是注解值也没有,则使用小写类名
            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的缓存
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                // 缓存 扩展类和扩展名的缓存
                cacheName(clazz, n);
                // 将 扩展类和扩展名 保存到extensionClasses 扩展名->扩展类 关系映射中
                saveInExtensionClass(extensionClasses, clazz, n);
            }
        }
    }
}

至此,getExtensionClasses() 加载扩展类方式剖析完成,接下剖析注入依赖 injectExtension() 方式。

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

    try {
        for (Method method : instance.getClass().getMethods()) {
            // 遍历当前扩展类的所有方式,若是当前方式不属于 setter 方式,
            // 即不是以 'set'开头的方式名,参数不是一个的,该方式接见级别不是 public 的,则不往下执行
            if (!isSetter(method)) {
                continue;
            }
            
            // 当前方式是否添加了不要注入依赖的注解
            if (method.getAnnotation(DisableInject.class) != null) {
                continue;
            }
            Class<?> pt = method.getParameterTypes()[0];
            // 判断当前参数是否属于 八个基本类型或void
            if (ReflectUtils.isPrimitives(pt)) {
                continue;
            }

            try {
                // 通过属性 setter 方式获取属性名
                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;
}

通过遍历扩展类所有方式,找到相对应的依赖,然后使用反射挪用 settter 方式来举行设置依赖。
objectFactory 工具如图:

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

其中找到响应依赖是在 SpiExtensionFactory 或 SpringExtensionFactory 中,同时,这两个 Factory 保存在 AdaptiveExtensionFactory 中举行维护。

@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {

    private final List<ExtensionFactory> factories;

    public AdaptiveExtensionFactory() {
        // ......
    }

    @Override
    public <T> T getExtension(Class<T> type, String name) {
        // 通过遍历匹配到 type->name 的映射
        for (ExtensionFactory factory : factories) {
            T extension = factory.getExtension(type, name);
            if (extension != null) {
                return extension;
            }
        }
        return null;
    }

}

以上是对 Dubbo SPI 扩展类简朴加载历程剖析完成。

自适应加载机制

为 Dubbo 加倍天真的使一个接口不通过硬编码加载扩展机制,而是通过使用历程中举行加载,Dubbo 的另一加载机制——自适应加载。
自适应加载机制使用 @Adaptive 标注:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
    String[] value() default {};
}

Adaptive 的值是一个数组,可以设置多个 key。初始化时,遍历所有 key 举行匹配,若是没有则匹配 @SPI 的值。
当 Adaptive 注解标注在类上时,则简朴对应该实现。若是注解标注在接口方式上时,则会凭据参数动态天生代码来获取扩展点的实现。
类上注解处置照样对照好明白,方式上的注解加载相对对照有研读性。通过挪用ExtensionLoader#getAdaptiveExtension来举行获取扩展实现。

public T getAdaptiveExtension() {
    // 获取实例化工具缓存
    Object instance = cachedAdaptiveInstance.get();
    if (instance == null) {
        if (createAdaptiveInstanceError != null) {
            throw new IllegalStateException("Failed to create adaptive instance: " +
                    createAdaptiveInstanceError.toString(),
                    createAdaptiveInstanceError);
        }
        // 双重检查锁后建立自适应扩展
        synchronized (cachedAdaptiveInstance) {
            instance = cachedAdaptiveInstance.get();
            if (instance == null) {
                try {
                    // 建立自适应扩展
                    instance = createAdaptiveExtension();
                    cachedAdaptiveInstance.set(instance);
                } catch (Throwable t) {
                    createAdaptiveInstanceError = t;
                    throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
                }
            }
        }
    }

    return (T) instance;
}

private T createAdaptiveExtension() {
    try {
        // 获取自适应扩展后,注入依赖
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    } catch (Exception e) {
        throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
    }
}

上面代码完成了扩展类工具是否存在缓存中,若是不存在,则通过建立自适应扩展,并将实例注入依赖后,设置在实例化后的自适应扩展工具中。
其中getAdaptiveExtensionClass是对照焦点的流程。

private Class<?> getAdaptiveExtensionClass() {
    // 加载所有扩展类
    getExtensionClasses();
    // 加载所有扩展类后,若是有 @Adaptive 标注的类,cachedAdaptiveClass 则一定不会为空
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    // 建立自适应扩展类
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

private Class<?> createAdaptiveExtensionClass() {
    // 天生自适应扩展代码
    String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
    // 获取扩展类加载器
    ClassLoader classLoader = findClassLoader();
    // 获取编译器类型的实现类
    org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    // 编译代码,返回该工具
    return compiler.compile(code, classLoader);
}

这里完成的事情主要是,加载所有扩展类,代表所有扩展接口类的实现类,在其加载历程中,若是有 @Adaptive 标注的类,会保存到 cachedAdaptiveClass 中。通过自动天生自适应扩展代码,并被编译后,获取扩展类实例化工具。
上面编译器类型是可以指定的,通过 compiler 举行指定,例如:<dubbo:application name="taomall-provider" compiler="jdk" />,该编译器默认使用 javassist 编译器。

在 generate 方式中动态天生代码:

public String generate() {
    // 检查当前扩展接口的方式上是否有 Adaptive 注解
    if (!hasAdaptiveMethod()) {
        throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
    }

    // 天生代码
    StringBuilder code = new StringBuilder();
    // 天生类的包名
    code.append(generatePackageInfo());
    // 天生类的依赖类
    code.append(generateImports());
    // 天生类的声明信息
    code.append(generateClassDeclaration());

    // 天生方式
    Method[] methods = type.getMethods();
    for (Method method : methods) {
        code.append(generateMethod(method));
    }
    code.append("}");

    if (logger.isDebugEnabled()) {
        logger.debug(code.toString());
    }
    return code.toString();
}

上面是天生类信息的方式,天生设计原理是根据已设置好的模板,举行替换操作,天生类。详细信息不代码许多,但阅读照样对照简朴。
自适应加载机制,已简朴剖析完,咋一眼看,非常复杂,然则领会整体结构和流程,再去细研的话,相对照样好明白。

总结

从 Dubbo 设计来看,其优越的扩展性,对照主要的一点是得益于 Dubbo SPI 加载机制。在学习它的设计理念,对可扩展性方面的编码思索也有一定的启发。

小我私家博客: https://ytao.top
关注民众号 【ytao】,更多原创好文
Dubbo 扩展点加载机制:从 Java SPI 到 Dubbo SPI

原创文章,作者:dddof新闻网,如若转载,请注明出处:https://www.dddof.com/archives/3077.html