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方式。
- 定制的接口
/**
* 发送渠道的接口
*/
public interface SendChannel {
/**
* 发送报文
*/
public void sendMessage();
}
- 接口的实现类
实现类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的方式发送请求");
}
}
- 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驱动程序的现有程序将继续工作,不需要修改。
可以总结为两点:
- 以后所有的jdbc驱动都需要包含META-INF/services/java.sql.Driver
- 不需要显示的使用Class.forName()方式了。
那么不在使用Class.forName()方法,那怎样将Driver注册到DriverManager中呢?没错通过java SPI的方式。
4.3 jdbc驱动的源码解读
- Connection connection = DriverManager.getConnection(url, userName, password);
- 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");
}
- 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);
}
}
}
- 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());
}
}
-
此时就获取到连接了
-
总结
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机制有着如下的弊端:
- 只能遍历所有的实现,并全部实例化。
- 配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们。
- 扩展如果依赖其他的扩展,做不到自动注入和装配。
- 扩展很难和其他的框架集成,比如扩展里面依赖了一个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 测试是否使用成功
进行一次远程调用