Mecanismo de extensión Dubbo SPI

Introducción

SPI se denomina Interfaz de proveedor de servicios, que es un mecanismo de descubrimiento de servicios. La esencia de SPI es configurar el nombre completo de la clase de implementación de la interfaz en un archivo, y el cargador de servicios lee el archivo de configuración y carga la clase de implementación. Esto puede reemplazar dinámicamente la clase de implementación de la interfaz en tiempo de ejecución. Debido a esta característica, podemos proporcionar fácilmente funciones extendidas para nuestros programas a través del mecanismo SPI.

Antes de hablar sobre el mecanismo de extensión SPI de dubbo, debemos comprender el mecanismo SPI nativo de java para ayudarnos a comprender mejor el SPI de dubbo.

Java Primitive SPI

Primer ejemplo:

1. Defina la interfaz Animal:

public interface Animal {
 void run();
}

2. Escribe 2 clases de implementación, Cat y Dog

public class Cat implements Animal{
 @Override
 public void run() {
      System.out.println("小猫步走起来~");
   }
}
public class Dog implements Animal {
 @Override
 public void run() {
      System.out.println("小狗飞奔~");
   }
}

3. A continuación, cree un archivo en la carpeta META-INF / services, el nombre es el nombre completo de Animal com.sunnick.animal.Animal, y el contenido del archivo es el nombre de clase completamente calificado de la clase de implementación, de la siguiente manera:

com.sunnick.animal.impl.Dog
com.sunnick.animal.impl.Cat

4. Escriba el método para probar

public static void main(String[] s){
   System.out.println("======this is SPI======");
   ServiceLoader<Animal> serviceLoader = ServiceLoader.load(Animal.class);  
       Iterator<Animal> animals = serviceLoader.iterator();  
 while (animals.hasNext()) {  
           animals.next().run();
       }
}

La estructura del directorio es la siguiente:

Los resultados de la prueba son los siguientes:

======this is SPI======
小狗飞奔~
小猫步走起来~

Se puede ver en los resultados de la prueba que nuestras dos clases de implementación se cargaron con éxito y se generó el contenido correspondiente. Pero no mostramos el tipo de Animal especificado en el código. Aquí es donde el mecanismo SPI nativo de Java está funcionando.

El mecanismo de SPI es el siguiente:

SPI es en realidad un mecanismo de carga dinámico implementado por "interfaz + modo de estrategia + archivo de configuración". En el diseño de sistemas, los módulos generalmente se basan en la programación de la interfaz y las clases de implementación especificadas no se muestran directamente. Una vez que se especifica la clase de implementación en el código, no se puede reemplazar con otra implementación sin modificar el código. Para lograr el efecto de la capacidad de conexión dinámica, Java proporciona SPI para realizar el descubrimiento de servicios.

En el ejemplo anterior, la clase de implementación de Animal se carga dinámicamente a través del método ServiceLoader.load (Animal.class). Al rastrear el código fuente del método, se encuentra que el programa leerá el archivo de configuración llamado nombre de clase en el directorio META-INF / services (Por ejemplo, el archivo META-INF / services / com.sunnick.animal.Animal en el ejemplo anterior), como sigue, donde el valor de la constante PREFIX es "META-INF / services /":

try {
    String fullName = PREFIX + service.getName();
 if (loader == null)
 configs = ClassLoader.getSystemResources(fullName);
 else
 configs = loader.getResources(fullName);
} catch (IOException x) {
 fail(service, "Error locating configuration files", x);
}

Luego cargue el objeto de clase a través de la reflexión Class.forName () y cree una instancia de la clase con el método instance (), completando así el descubrimiento del servicio.

String cn = nextName;
nextName = null;
Class<?> c = null;
try {
    c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
 fail(service,
 "Provider " + cn + " not found");
}

Muchos marcos de uso común utilizan mecanismos SPI, como la fachada de registro slf y log4j, el logback y otras implementaciones de registro, las interfaces java, sql.Driver de jdbc y varias implementaciones de conectores de bases de datos.

Uso de SPI de Dubbo

Dubbo no utilizó Java SPI, pero volvió a implementar un mecanismo SPI más potente. La lógica relacionada de Dubbo SPI está encapsulada en la clase ExtensionLoader A través de ExtensionLoader, podemos cargar la clase de implementación especificada. Los archivos de configuración requeridos por Dubbo SPI deben colocarse en la ruta META-INF / dubbo, y el contenido de la configuración es el siguiente:

dog=com.sunnick.animal.impl.Dog
cat=com.sunnick.animal.impl.Cat

A diferencia de la configuración de la clase de implementación de Java SPI, Dubbo SPI se configura a través de pares clave-valor, de modo que la clase de implementación especificada se puede cargar a pedido. Además, al usar Dubbo SPI, debe marcar la anotación @SPI en la interfaz Animal, y las clases de gatos y perros permanecen sin cambios. Demostremos el uso de Dubbo SPI:

@SPI
public interface Animal {
 void run();
}

Método de prueba de escritura:

public void testDubboSPI(){
   System.out.println("======dubbo SPI======");
   ExtensionLoader<Animal> extensionLoader =
         ExtensionLoader.getExtensionLoader(Animal.class);
   Animal cat = extensionLoader.getExtension("cat");
   cat.run();
   Animal dog = extensionLoader.getExtension("dog");
   dog.run();
}

Los resultados de la prueba son los siguientes:

======dubbo SPI======
小猫步走起来~
小狗飞奔~

Análisis de código fuente SPI de dubbo

Dubbo obtiene la instancia a través del método ExtensionLoader. GetExtensionLoader ( Animal. Class ). GetExtension ( "cat" ). En este método, la instancia se obtendrá primero de la lista de caché y, si se pierde, se creará:

public T getExtension(String name) {
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    if ("true".equals(name)) {
        // 获取默认的拓展实现类
        return getDefaultExtension();
    }
    // Holder,顾名思义,用于持有目标对象
    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 中
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

El proceso de creación de una instancia es el siguiente, a saber, el método 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);
        }
        //此处省略一些源码......
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("...");
    }
}

Obtenga todos los archivos de configuración de SPI y analice los pares clave-valor en el archivo de configuración. El código fuente de getExtensionClasses () es el siguiente:

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

La caché también se verifica primero Si la caché falla, el bloqueo se bloquea mediante sincronización. Después de bloquear, revise el caché nuevamente y juzgue que está vacío. En este momento, si las clases siguen siendo nulas, cargue las clases de extensión a través de loadExtensionClasses. La lógica del método loadExtensionClasses se analiza a continuación.

 private Map<String, Class<?>> loadExtensionClasses() {
    // 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的,即示例中的Animal
    SPI defaultAnnotation = (SPI)this.type.getAnnotation(SPI.class);
    if(defaultAnnotation != null) {
        String extensionClasses = defaultAnnotation.value();
        if(extensionClasses != null && (extensionClasses = extensionClasses.trim()).length() > 0) {
	  // 对 SPI 注解内容进行切分
            String[] names = NAME_SEPARATOR.split(extensionClasses);
	  // 检测 SPI 注解内容是否合法,不合法则抛出异常
            if(names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension " + this.type.getName() + ": " + Arrays.toString(names));
            }

            if(names.length == 1) {
                this.cachedDefaultName = names[0];
            }
        }
    }
    HashMap extensionClasses1 = new HashMap();
    // 加载指定文件夹下的配置文件
    this.loadFile(extensionClasses1, "META-INF/dubbo/internal/");
    this.loadFile(extensionClasses1, "META-INF/dubbo/");
    this.loadFile(extensionClasses1, "META-INF/services/");
    return extensionClasses1;
}

Se puede ver que finalmente se llama al método loadFile. Este método consiste en leer el nombre de archivo especificado del directorio especificado, analizar el contenido y poner el par clave-valor en el mapa. El proceso no se repite aquí.

Lo anterior es el proceso de la instancia de carga SPI de dubbo.

Comparación de Dubbo SPI y SPI nativo

La SPI nativa de Java tiene las siguientes desventajas:

  • Es necesario recorrer todas las implementaciones y crear instancias de ellas, es imposible cargar solo una clase de implementación especificada y el mecanismo de carga no es lo suficientemente flexible;
  • Las clases de implementación no se nombran en el archivo de configuración y no se puede hacer referencia a ellas con precisión en el programa;
  • No se usa caché, debe recargarse cada vez que se llama al método de carga

Si desea utilizar Dubbo SPI, la interfaz debe estar marcada con la anotación @SPI. Por el contrario, Dubbo SPI tiene las siguientes mejoras:

  • El archivo de configuración se cambia a la forma de pares clave-valor, que pueden obtener cualquier clase de implementación sin cargar todas las clases de implementación, ahorrando recursos;
  • Aumente la caché para almacenar la instancia, mejore el rendimiento de lectura;

Además, dubbo SPI también proporciona una forma de especificar valores predeterminados (por ejemplo, la clase de implementación predeterminada Animal se puede especificar como Cat por @SPI ("cat")). Al mismo tiempo, dubbo SPI también brinda soporte para funciones avanzadas como IOC y AOP para lograr más tipos de expansión.

para resumir

SPI es un mecanismo de descubrimiento de servicios que brinda la capacidad de descubrir clases de implementación de forma dinámica y encarna la idea de desacoplamiento jerárquico.

En el proceso de diseño de arquitectura y escritura de código, los módulos deben programarse para la interfaz, evitar hacer referencia directa a clases de implementación específicas y lograr efectos conectables.

Dubbo proporciona una versión mejorada del mecanismo SPI. Durante el uso, es necesario poner anotaciones @SPI en la interfaz para que surtan efecto.

Supongo que te gusta

Origin blog.csdn.net/suifeng629/article/details/107140371
Recomendado
Clasificación