本片文章是针对dubbo SPI机制深入分析的平滑过渡的作用。当然咱们主要是学习优秀的思想,SPI就是一种解耦非常优秀的思想,我们可以思考在我们项目开发中是否可以使用、是否可以帮助我们解决某些问题、或者能够更加提升项目的框架等
一、SPI是什么
SPI(service provider interface)是java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
如果用上面这句话来描述SPI那么是一点卵用没有,下面用生动的例子来阐述。其实SPI跟我们的策略设计模式比较相似,如果对策略设计模式不太了解的,可以先花点时间去学习一下。
实例:假如,我们在京东上购买商品需要付款,假如我们可以选择的支付的模块有支付宝、微信、银行卡。如果我们使用策略设计模式的话,简单的代码如下。
/*** * 抽象支付 */
public interface Pay {
void pay();
}
/** * @Auther: * @Date: 2020/5/2 14:28 * @Description: 支付宝付款 */
public class AliPay implements Pay {
@Override
public void pay() {
System.out.println("使用支付宝pay....");
}
}
/** * @Auther: * @Date: 2020/5/2 14:28 * @Description: 微信pay */
public class WechatPay implements Pay {
@Override
public void pay() {
System.out.println("使用微信支付....");
}
}
/** * @Auther: * @Date: 2020/5/2 14:29 * @Description:银行卡pay */
public class BankCardPay implements Pay {
@Override
public void pay() {
System.out.println("使用银行卡支付....");
}
}
你可以根据你的需求创建出相应的Pay的实现类,然后调用pay(),例如我简写一下
/**
* @Auther:
* @Date: 2020/5/2 14:36
* @Description:
*/
public class Context {
private final Pay pay;
public Context(Pay pay) {
this.pay = pay;
}
public void invokeStrategy(){
pay.pay();
}
}
/** * @Auther: * @Date: 2020/5/2 14:35 * @Description: */
public class PayTest {
public static void main(String[] args) {
//ali
Context aliContext = new Context(new AliPay());
aliContext.invokeStrategy();
//wechat
Context wechatContext = new Context(new WechatPay());
wechatContext.invokeStrategy();
}
}
从上面的代码中我们其实可以看到还是需要我们显示的创建出相应的支付模块。
二、SPI如何使用
那么现在有这样的场景:当我的项目里面有什么支付模块我就使用什么样的支付模块,比如说有支付宝支付模块就选择支付宝、有微信支付模块我就选择微信支付、同时有多个的时候,我默认选择第一个,此时我们就可以使用SPI,先看下如何使用。
1、创建META-INF/services文件夹,然后创建一个以Pay接口全限定名为名字的文件
2、在文件中编写想要实现哪个Pay的实现类(AliPay,WechatPay,BankCardPay),注意也要是全限定名
假如是是支付宝支付的模块,上面文件的内容:
com.taolong.dubbo.spi.strategy.AliPay
3、获取Pay并调用
获取并调用的逻辑,我就修改下上面的策略模式中的Context的invokerStrategy方法,这里假设默认使用第一个
public void invokeStrategy(){
ServiceLoader<Pay> payServiceLoader = ServiceLoader.load(Pay.class);
Iterator<Pay> iterator = payServiceLoader.iterator();
if (iterator.hasNext()){
iterator.next().pay();
}
}
main方法调用
public static void main(String[] args) {
// //ali
// Context aliContext = new Context(new AliPay());
// aliContext.invokeStrategy();
// //wechat
// Context wechatContext = new Context(new WechatPay());
// wechatContext.invokeStrategy();
Context context = new Context();
context.invokeStrategy();
}
上面就是使用的SPI机制,让其选择一个Pay的实现类,这样子就比较灵活了,比如正常的团队工作情况是下面这样子。
A团队:负责支付宝支付支付模块的开发
B团队:负责微信支付模块的开发
C团队:负责银行卡支付模块的开发
此时A团队的支付模块里面只需要新建一个“com.taolong.dubbo.spi.strategy.Pay”的文件,文件的内容对应Alipay(他们自己命名),然后编写自己的支付逻辑即可
B团队也只需要新建“com.taolong.dubbo.spi.strategy.Pay”文件的内容比如是WechatPay(他们自己命名),然后编写自己的逻辑即可。
相当于Pay定义了一个规范,不管是微信、还是支付宝支付只要符合这个规范就行。在使用的使用,我们只需要加入相应的依赖(比如支付宝模块的pom依赖,微信模块的pom依赖),项目就能自动发现具体的实现类,然后调用相应的模块的支付方法。
三、SPI的优秀实现案例
如果对我上面的描述不太理解的话,我们来看一个真实的使用上述SPI的例子—数据库驱动(Driver)
我们知道,当我们的项目里面使用引用了mysql的驱动pom依赖时,我们的项目里面会自动选择使用mysql的驱动,我们甚至不需要手动去加载。我们来看看它的具体实现。
1、首先看一下java.sql.Driver的类
这里面也是相当于定义了一个规范
2、其次看mysql驱动包的META-INF/services文件夹下面有没有指定的文件
很熟悉,命名就是java.sql.Driver
3、打开文件查看一下文件内容
这里面就能看到我们的mysql的驱动了,到这里基本上就确认这也是使用SPI实现的,顺便说一下,现在为什么我们不需要使用Class.forName()去加载驱动了,这是因为DriverManager使用SPI的机制已经帮我们加载好了,我们来看看DriverManager的类
(1)静态代码块
/** * Load the initial JDBC drivers by checking the System property * jdbc.properties and then use the {@code ServiceLoader} mechanism */
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
oadInitialDrivers()
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);
}
}
}
很明显就能看到它的调用方法跟我们上面将的例子是一样的,唯一不同的是它会加载所有的驱动。
不仅仅是数据库驱动使用了SPI,还有slf4j、spring等等
上面的java的SPI的源码本文限于篇幅,就不讲解了,感兴趣的可以自行阅读,不过,我们也能够猜测出它的主要的逻辑,下面用一副简单的图来描述一下
不管是文件名还是文件内容都是全限定名,所以通过反射很容易创建相应的类