Dubbo进阶(五):Dubbo扩展点加载机制(上)

SPI 全称为 Service Provider Interface,是一种服务发现机制。当程序运行调用接口时,会根据配置文件或默认规则信息加载对应的实现类。所以在程序中并没有直接指定使用接口的哪个实现,而是在外部进行装配。

Dubbo的扩展点加载机制类似于JavaSPI加载机制,但是JavaSPI加载机制在查找具体某个实现的时候,只能通过遍历进行查找并会实例化所有实现类,因此对于实际不需要加载的扩展实现也会实例化,造成一定的内存浪费。Dubbo SPI加载机制可通过扩展点名称进行查找,避免实例化所有实现;同时,增加了对扩展点IoC和AOP的支持,一个扩展点实现可以注入其他扩展点实现并进行Wrapper包装。

在了解Dubbo SPI机制之前我们先了解一下JavaSPI机制。

Java SPI

以MySQL数据库驱动为例,来看一下Java SPI是如何实现的。

你想一下首先市面上的数据库五花八门,不同的数据库底层协议的大不相同,所以首先需要定制一个接口,来约束一下这些数据库,使得 Java 语言的使用者在调用数据库的时候可以方便、统一的面向接口编程。

这个时候Java SPI就派上用场了,大家都约定好将实现类的配置写在一个地方,然后到时候都去哪个地方查一下就知道了。约定在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名。

首先,MySQL软件驱动包在META-INF/services目录下创建了java.sql.Driver文件
在这里插入图片描述

java.sql.Driver文件如下,内容为具体实现类的全路径名

在这里插入图片描述
DriverManager类在被虚拟机加载时会运行如下代码:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

在这里插入图片描述

其中loadInitialDrivers()就是DriverManager使用SPI机制加载mysql声明的驱动,loadInitialDrivers()核心代码如下:

    private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

参照MySQL数据库驱动总结一下Java SPI具体的实现步骤:

  • 定义一个接口及对应的方法
  • 实现这个接口
  • META-INF/services下创建一个以接口全路径命名的文件,如:java.sql.Driver
  • 文件内容为具体实现类的全路径名,如果有多个,就用分行符号分隔
  • 在代码中通过ServiceLoader来加载具体类的实现类

但是Java SPI也是有一些缺陷的

  • JDK自带的SPI会一次性实例化所有扩展点实现,如果扩展点不使用,那么会浪费资源
  • 在扩展点加载失败的情况下,JDK扩展点加载机制无法提供扩展点加载失败的真正原因
  • JDK自带SPI机制不支持IOC和AOP的功能
Dubbo SPI

相比Java SPIDubbo SPI做了一些改进和优化:

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

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配置文件路径META-INF/services(兼容Java SPI)、META-INF/dubbo/(用户扩展)、META-INF/dubbo/internal (Dubbo内部使用)
SPI配置文件名称全路径类名
文件内容格式key=value,多个用换行符分隔
Dubbo SPI 示例,来自官网

Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下。

  • 定义一个Robot接口
@SPI
public interface Robot {
    void sayHello();
}
  • 接下来定义两个实现类,分别为 OptimusPrime 和 Bumblebee。
public class OptimusPrime implements Robot {
    
    @Override
    public void sayHello() {
        System.out.println("Hello, I am Optimus Prime.");
    }
}

public class Bumblebee implements Robot {

    @Override
    public void sayHello() {
        System.out.println("Hello, I am Bumblebee.");
    }
}
  • 接下来 META-INF/dubbo 文件夹下创建一个文件,名称为 Robot 的全限定名 org.apache.spi.Robot。文件内容为实现类的全限定的类名,如下:
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
  • 与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外,在测试 Dubbo SPI 时,需要在 Robot 接口上标注 @SPI 注解。
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();
    }
}

测试结果如下:
在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值