[ Java ] 到底什么是 SPI ?
Posted 削尖的螺丝刀
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[ Java ] 到底什么是 SPI ?相关的知识,希望对你有一定的参考价值。
昨天在和我的小伙伴探讨 Druid 源码的时候,他提出了一个问题,问题是这样描述的:DruidDataSource#getConnection 中的 init 执行 DruidDriver.getInstance 的时候是如何把其他驱动注册的,也就是下面这里:
public void init() throws SQLException
if (inited)
return;
// bug fixed for dead lock, for issue #2980
DruidDriver.getInstance();
到底是为什么呢? 那么我们就以这个问题作为钥匙,开启我们对SPI学习的大门。 先跟进去看一下 ( 确实如果不是有人提醒我根本没在意,这里面有几个关键的静态代码块 )
public static DruidDriver getInstance()
return instance;
DruidDriver的静态代码块:
static
AccessController.doPrivileged(new PrivilegedAction<Object>()
@Override
public Object run()
registerDriver(instance);
return null;
);
public static boolean registerDriver(Driver driver)
try
DriverManager.registerDriver(driver);
try
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
ObjectName objectName = new ObjectName(MBEAN_NAME);
if (!mbeanServer.isRegistered(objectName))
mbeanServer.registerMBean(instance, objectName);
catch (Throwable ex)
if (LOG == null)
LOG = LogFactory.getLog(DruidDriver.class);
LOG.warn("register druid-driver mbean error", ex);
return true;
catch (Exception e)
if (LOG == null)
LOG = LogFactory.getLog(DruidDriver.class);
LOG.error("registerDriver error", e);
return false;
再跟进看一下 DriverManager.registerDriver(driver) 的内部也有个静态代码块:
/**
* 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");
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;
);
可以看到关键方法:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
我们都知道静态代码块是执行在最前面,而这里对其他驱动的加载正是在静态代码块里实现的,可是问题是为什么其他的驱动也会被加载呢?
这 一切都要从Java类的加载机制 以及 这个问题的最关键原理 —— SPI 说起,而它通过驱动加载的方式,也算是打破类在加载过程中双亲委派机制 的一个关键手段 !
( 如果你要问为什么要打破? 原因就在于驱动属于Java核心类库,而核心类库的 Class文件 只能由启动类加载器加载,如果对加载过程和双亲委派原理掌握的不是很透彻的同学,可以通过这篇文章了解一下 : [ Java ] 一文说透所谓的双亲委派 )
那么 SPI 到底是个什么东西呢 ? 又为什么要有它 ?
说到底,最关键的就为了两个字 —— 解耦 !
如果你一定让我多说两个字,那就是为了方便在不改动原始代码的基础上 —— 扩展 !
我们仔细想想,如果你是一个厂家,你生产了一个 jar 包给别人用,但是里面有些方法你觉得可以让别人自定义,但是又不想修改 jar 包怎么办? 这就是 SPI 的关键作用。他可以不通过修改原厂jar包实现客户自己定义关键方法。
再来解释下 SPI , 它的全称是: Service Provider Interface , 是一种面向接口变成的思想规范,它是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。
- API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。
- SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。
有了上面的理论基础,我们这里写一个 Demo 来自己实现一套 SPI 看看:
1.首先我们建立一个普通Maven项目,模拟自己是一个SDK的提供者,我们再里面提供一个可以自定义的SPI接口
2.然后我们建立一个该接口的实现类
3.接下来我们建立一个测试工具,这个工具就模拟了SDK要提供服务的整理流程,中间夹杂了自定义方法:
4.指定 SPI 加载类
敲黑板,关键点来了: SPI 的规范就是 ,要在资源包下 建立一个 META-INF.services 的文件夹,然后再这个文件夹里建立一个文件,千万注意的是,文件名必须是你要提供的服务类的全限定类名:那么我这里就是 spi.SPIService ,然后再这个文件里,写入你对这个服务实现类的全限定类名。
5.那么我们最终建立的内容结构就是这个样子了:
6.我们给这个项目 打成一个 jar 包,然后再另外创建一个maven项目来模拟我们的真实项目,然后引入刚才的jar包。
7.接下来我们建立一个SPIUser类,来加载并使用这个jar包提供的服务。
8.执行结果,可以看到我们jar包提供的服务生效了,且执行了默认的方法。
9.这个时候我们再运用 SPI 的机制,自定义一套方法,逻辑和上面一样,只不过是在当前项目中完成,相当于”重写“ 的概念:
10.同样,定义SPI路径
11.再次 执行刚才的方法,可以看到结果变了:
上面完整描述了 SPI 的实现过程 , 这会我们再来看看 Druid 源码中的内容,同样是不是也看到了 SPI 的身影 :
至此,我们对 SPI 机制有了一个整体的了解,而他的好处,我刚才也说了,关键就在解耦,概括起来如下:
- 不需要改动源码就可以实现扩展,解耦。
- 实现扩展对原来的代码几乎没有侵入性。
- 只需要添加配置就可以实现扩展,符合开闭原则。
以上是关于[ Java ] 到底什么是 SPI ?的主要内容,如果未能解决你的问题,请参考以下文章