Java SPI + Dubbo SPI

1. 什么是SPI

SPI全称Service Provider Interface,服务提供接口,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。

2. API和SPI

API (Application Programming Interface)叫应用程序接口,是有实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。

SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。

换句话说,API就是实现方已经弄好了,调用方直接按照实现方的规则进行使用就行。而SPI则是有调用方制定规范,实现提供实现,然后又调用发来选择想要的实现。

API 
调用方:我要实现一个功能
实现方:实现好了,只能这么使用,爱用不用

SPI
调用方:我要实现一个功能,并且必须按照这个规范来实现
实现方1:实现了
实现方2: 实现了
调用方:我看看我到底需要那个。

3. SPI的简单实现

说明:定制一个发送渠道的接口,有两种实现,一种是通过esb方式,一种是通过mq方式。

  1. 定制的接口
/**
 * 发送渠道的接口
 */
public interface SendChannel {

    /**
     * 发送报文
     */
    public void sendMessage();
}
  1. 接口的实现类
    实现类1
public class EsbSendChannel implements SendChannel{
    public void sendMessage() {
        System.out.println("通过esb方法发送请求");
    }
}

实现类2

public class MqSendChannel implements SendChannel{
    public void sendMessage() {
        System.out.println("通过mq的方式发送请求");
    }
}
  1. META-INF/services/配置

创建META-INF/services/路径,然后建立一个接口的全名称文件,com.hww.javaspi.SendChannel
然后把两个接口的实现类全名称放入文件中。
在这里插入图片描述4. 使用测试
通过一个java工具类ServiceLoader,load方法,获取解析META-INF/services/路径下的文件,将文件中配置的实现类加载并创建实例,让后进行遍历,调用sengMessage方法。

public class MySpi {
    public static void main(String[] args){
        ServiceLoader<SendChannel> loadSendChannels = ServiceLoader.load(SendChannel.class);
        for (SendChannel loadSendChannel : loadSendChannels) {
            loadSendChannel.sendMessage();
        }
    }
}

4. 为啥要使用SPI

根据上述实现了简单的SPI,那么问题来了,为啥要使用SPI,既然都已经有实现类了,为啥就不能通过直接调用实现类的方法来调用方法呢,费这劲干啥。

面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。于是就有了SPI这种服务发现机制。

这就涉及到SPI的使用场景了,数据库驱动的场景就比较典型了。我们都知道现在有多种数据库吧,最熟悉就是mqsql和oracle数据库了,而且oracle 和mysql数据库的驱动是不一样的。都需要对接JDK。驱动不可能让数据库厂商各自去实现吧,大家都不同还怎么对接JDK。所以JDK就制定了一个规范,这个规范就是java.sql.Driver接口。所以各种数据库如果想对接JDK,那么就必须实现java.sql.Driver接口。
各种数据库都有自己的驱动包。比如mysql的mysql-connector-java-8.0.16.jar 比如oracle的 odbc6.jar等。

4.1 之前获取数据库连接的方式

String sql = "select * from dual";
String userName = "username";
String password = "password";
String url = "mysql.jdbc//localhost:3306/tzwTest";
//加载mysql的Driver,执行Driver的静态块,将Driver进行注册到DriverManager中。DriverManager.registerDriver(new Driver());
Class.forName("com.mysql.cj.jdbc.Driver");
//从DriverManage获取数据库连接。
Connection connection = DriverManager.getConnection(url, userName, password);
PreparedStatement preparedStatement = connection.prepareStatement(sql);
.....

4.2 现在获取

String sql = "select * from dual";
String userName = "username";
String password = "password";
String url = "mysql.jdbc//localhost:3306/tzwTest";
//从DriverManage获取数据库连接,根据url的前缀获取想要的Driver驱动,并注册到DriverManager
Connection connection = DriverManager.getConnection(url, userName, password);
PreparedStatement preparedStatement = connection.prepareStatement(sql);
......

总结:
发现此时不在需要写Class.forName(…)方法就可以直接获取到连接。
java.sql.DriverManager类上有一段注释:

The DriverManager methods getConnection and getDrivers have been enhanced to support the Java Standard Edition Service Provider mechanism. JDBC 4.0 Drivers must include the file META-INF/services/java.sql.Driver. This file contains the name of the JDBC drivers implementation of java.sql.Driver. For example, to load the my.sql.Driver class, the META-INF/services/java.sql.Driver file would contain the entry:my.sql.Driver
Applications no longer need to explicitly load JDBC drivers using Class.forName(). Existing programs which currently load JDBC drivers using Class.forName() will continue to work without modification.
翻译为:
驱动管理器方法getConnection和getDrivers得到了增强,以支持Java标准版服务提供者机制。JDBC 4.0驱动程序必须包含文件META-INF/services/java.sql.Driver。该文件包含java.sql.Driver的JDBC驱动程序实现的名称。例如,要加载my.sql.Driver类,使用META-INF/services/java.sql。驱动文件将包含如下条目:my.sql.Driver
应用程序不再需要使用Class.forName()显式加载JDBC驱动程序。目前使用Class.forName()加载JDBC驱动程序的现有程序将继续工作,不需要修改。

可以总结为两点:

  1. 以后所有的jdbc驱动都需要包含META-INF/services/java.sql.Driver
  2. 不需要显示的使用Class.forName()方式了。
    那么不在使用Class.forName()方法,那怎样将Driver注册到DriverManager中呢?没错通过java SPI的方式。

4.3 jdbc驱动的源码解读

  1. Connection connection = DriverManager.getConnection(url, userName, password);
  2. DriverManager中有静态的static块先执行。
    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     * 通过检查System属性加载初始的JDBC驱动程序jdbc属性,使用{@code ServiceLoader}机制
     */
    static {
    	//初始化方法
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
  1. loadInitialDrivers
private static void loadInitialDrivers() {
        String drivers;
        try {
        	//AccessController.doPrivileged是一个native方法,参数是一个接口PrivilegedAction,这里是一个匿名内部类的写法,
        	//可以理解为AccessController直接调用 PrivilegedAction实现类的run方法。
            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()
		//翻译:
		//如果驱动被打包为Service Provider,加载它。
		//通过类加载器获取所有驱动程序 
		//公开为java.sql.Driver.class服务。
		// ServiceLoader.load()替换sun.misc.Providers()
		
		//可以理解为AccessController直接调用 PrivilegedAction实现类的run方法。run方法中通过ServiceLoader.load去加载驱动。
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
				//serviceLoader.load()方法其实就是创建出一个LazyIterator类
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                //loadedDrivers.iterator()返回一个Iterator的实现。而实现中调用的方法都是LazyIterator的方法
                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{
                	//调用Iterator类中的方法,在Iterator类中的方法在调用LazyIterator中的方法
                	//在lazyItertor方法中扫描jar,找到所有java.sql.Driver的实现类,将这些类通过Class.forName(mysql.jdbc...)
                	//进行加载,加载的过成中将Driver注册至DriverManager中。DriverManager.registerDriver(new Driver)...
                    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);
            }
        }
    }
  1. DriverManager.getConnection(url,username,password)
 //  Worker method called by the public getConnection() methods.
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        /*
         * When callerCl is null, we should check the application's
         * (which is invoking this class indirectly)
         * classloader, so that the JDBC driver class outside rt.jar
         * can be loaded from here.
         */
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;
		//遍历所有注册的Driver驱动。
        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    //在获取connection连接是,判断这个Driver驱动的前缀是否和url中的前缀一致.
                    //如果一致进行获取连接,不一致则返回空。
                    //mysql驱动的前缀为 jdbc:mysql:
                    //oracle驱动的前缀为 jdbc:oracle:
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }

            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }

        }
  1. 此时就获取到连接了

  2. 总结
    a. 此时就体现了SPI的思想,不需要在代码里硬编译了class.forName()。查找所有的MATA-INF/services/java.sql.Driver。进行class.froName()注册,注册到DriverManager中,在根据前缀的方式判断使用那个Drvier。
    b.ServiceLoader是SPI的是一种实现,SPI,全称Service Provider Interface,用于一些服务提供给第三方实现或者扩展,可以增强框架的扩展或者替换一些组件。 通过jar包来实现扩展功能的热插拔。例如原来使用的mysql驱动,如果要换成oracle驱动,那么是么也不需要修改,只需要增加oracle驱动jar包,顺便修改一下用户名密码url就可以了。
    c. 同时也体现了Java SPI的缺点,就是加载的时候需要加载所有的MATA-INF/services/java.sql.Driver以及文件中配置的所有实现类。不能够制定加载哪些,不加载哪些。

5. Dubbo 的SPI

dubbo作为一个高度可扩展的rpc框架,也依赖于java的spi,并且dubbo对java原生的spi机制作出了一定的扩展,使得其功能更加强大。

5.1 Java SPI的弊端

从上面的java spi的原理中可以了解到,java的spi机制有着如下的弊端:

  1. 只能遍历所有的实现,并全部实例化。
  2. 配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们。
  3. 扩展如果依赖其他的扩展,做不到自动注入和装配。
  4. 扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的Java SPI不支持。

5.2 Dubbo SPI的介绍

有几个概念:
扩展点:就是需要扩展的接口。
扩展:就是接口的实现类
扩展自适应实例:其实就是一个Extension的代理,它实现了扩展点接口。在调用扩展点的接口方法时,会根据实际的参数来决定要使用哪个扩展。dubbo会根据接口中的参数,自动地决定选择哪个实现。
@SPI:该注解作用于扩展点的接口上,表明该接口是一个扩展点
@Adaptive:@Adaptive注解用在扩展接口的方法上。表示该方法是一个自适应方法。Dubbo在为扩展点生成自适应实例时,如果方法有@Adaptive注解,会为该方法生成对应的代码。

5.3 Dubbo SPI的使用

需求:比如要扩展dubbo的负载均衡,首先LoadBalance本身就是一个@SPI标注的接口,本身就是一个扩展点。

5.3.1 要扩展的接口(扩展点)
@SPI("random")
public interface LoadBalance {
    @Adaptive({"loadbalance"})
    <T> Invoker<T> select(List<Invoker<T>> var1, URL var2, Invocation var3) throws RpcException;
}
5.3.2 接口实现类(扩展)

自定义一个实现类:

public class MyBalance implements LoadBalance {
    @Override
    public <T> Invoker<T> select(List<Invoker<T>> list, URL url, Invocation invocation) throws RpcException {
        System.out.println("这是自定义负载均衡loadbalance,返回第一个地址");
        Invoker<T> tInvoker = list.get(0);
        return tInvoker;
    }
}
5.3.3 配置

和java spi一样,需要在META-INF下配置相关的文件,并在文件中指定实现类。
在这里插入图片描述

5.3.4 使用

在xml中配置我们自定义的loadBalance

  <!-- 生成远程服务代理,可以和本地bean一样使用demoService -->
    <dubbo:reference id="demoService" interface="com.hww.api.IDemoService" loadbalance="myLoadBalance">
        <dubbo:method name="sayHello"/>
        <dubbo:method name="sayHi"/>
    </dubbo:reference>
5.3.5 测试是否使用成功

进行一次远程调用

5.4 Dubbo SPI原理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值