Механизм загрузки точки расширения Dubbo: от Java SPI к Dubbo SPI

Dubbo

Полное название SPI — интерфейс поставщика услуг, представляющий собой механизм обнаружения услуг. Когда программа запускается и вызывает интерфейс, соответствующий класс реализации будет загружен в соответствии с файлом конфигурации или информацией о правиле по умолчанию. Поэтому в программе напрямую не указывается, какая реализация интерфейса используется, а собирается извне. Чтобы понять дизайн и реализацию Dubbo, необходимо понять механизм загрузки Dubbo SPI.Реализация большого количества функций в Dubbo основана на Dubbo SPI для достижения развязки, что также позволяет Dubbo получить такую ​​​​хорошую масштабируемость.

Java SPI

Узнайте, как это работает, работая через Java SPI.

  • Создайте интерфейс AnimalService и метод категории
  • Создайте класс реализации Cat
  • Создайте каталог META-INF/services и создайте файл в этом каталоге с полным именем AnimalService в качестве имени файла.
  • Добавьте в файл полное имя класса реализации Cat.

Интерфейс животных

public interface AnimalService {
    void category();
}

Класс реализации Cat

public class Cat implements AnimalService {

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

Добавьте файл top.ytao.demo.spi.AnimalService в каталог META-INF/services:

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();
        }
    }

}

Результаты:

Таким образом реализуется Java SPI черезServiceLoader.loadПолучите классы реализации интерфейса, которые загружают все настроенные интерфейсы, а затем вы сможете найти требуемую реализацию.

Dubbo SPI

Версия этой статьи в Dubbo2.7.5Dubbo SPI более мощный, чем Java SPI, и представляет собой набор механизмов SPI, реализованных сам по себе. Основные улучшения и оптимизации:

  • По сравнению с Java SPI, который загружает все реализации одновременно, Dubbo SPI загружается по запросу, и загружаются только те классы реализации, которые необходимо использовать. Также с поддержкой кэша.
  • Более подробная информация об ошибке загрузки расширения.
  • Добавлена ​​поддержка расширенного IOC и AOP.

Пример Дуббо SPI

Файл конфигурации Dubbo SPI находится в папке META-INF/dubbo, а конфигурация класса реализации использует метод KV.Ключом является параметр, передаваемый созданным объектом, а значением является полное имя расширения. класс реализации точки. Например, содержимое конфигурационного файла Cat:

cat = top.ytao.demo.spi.Cat

В процессе загрузки Dubbo SPI каталог Java SPI также может быть совместим.

В то же время аннотация @spi должна быть добавлена ​​к интерфейсу. Ключевое значение может быть указано в @spi. Загрузка 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();
    }

}

Результат выполнения следующий:

Получить экземпляр 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;
}

Вышеупомянутый процесс получения расширенного загрузчика классов в основном заключается в проверке того, является ли входящий тип допустимым, и есть ли интерфейс текущего типа из кэша расширенного загрузчика классов, если нет, добавьте текущий интерфейс в кэш.ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERSЭто кеш загрузчика класса расширения, который использует интерфейс в качестве ключа и загрузчик класса расширения в качестве значения для кэширования.

Получить объект класса расширения

Получить метод расширения класса расширения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;
}

Получение объекта держателя из кешаConcurrentMap<String, Holder<Object>> cachedInstancesЕсли он не существует, создайте объект Holder в качестве значения с ключом расширения и установите его в кэш объекта расширения. Если это вновь созданный экземпляр объекта расширения, тоholder.get() должен иметь значение null , Когда объект расширения пуст, объект расширения создается после двойной проверки блокировки.

Создать объект расширения

Процесс создания объекта расширения:

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;
}

Обходя все методы класса расширения, найдите соответствующие зависимости, а затем используйте отражение, чтобы вызвать метод установки для установки зависимостей. Объект objectFactory показан на рисунке:

Соответствующие зависимости находятся в SpiExtensionFactory или SpringExtensionFactory, и в то же время эти две фабрики сохраняются в 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. Путем автоматического создания кода адаптивного расширения и после компиляции получается объект создания экземпляра класса расширения. Вышеупомянутые типы компилятора могут быть указаны и указаны компилятором, например:<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. Изучив его концепцию дизайна, я также получил некоторое вдохновение для кодирования, думая о масштабируемости.


личный блог: ytao.top

Обратите внимание на паблик [ytao], больше оригинальных хороших статей

我的公众号