浅谈Java SPI原理与其在JDBC、Flink中的应用
API:由被调方提供的实现了某个完整功能的接口,主调方直接调用该接口来享用该功能,而无需关注该接口的具体实现。比如使用 JDK 的 InputStream#read 从文件系统中读取数据。
SPI:被调方(框架)提供的功能扩展点接口,主调方可实现这些接口与被调方(框架)进行互动。
MySQL 应用
JDBC 中的 DriverManager 中定义的模板代码:将 Driver 实现类的 Class 的加载工作委托给 SPI。
// From DriverManager.java
private static final String JDBC_DRIVERS_PROPERTY = "jdbc.drivers";
private static Connection getConnection(
// 省略代码 ...
// 通过 SPI 加载 Driver,而无需再通过 Class.forName("com.mysql.jdbc.Driver")
ensureDriversInitialized();
// 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;
// 「驱动实现类」的 Class 被加载后,就会通过「静态代码块」将当前驱动对象注册到 registeredDrivers 中,比如见后文的 MySQL 驱动类 FabricMySQLDriver.java 的实现
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 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());
}
}
// 省略代码...
}
private static void ensureDriversInitialized() {
if (driversInitialized) {
return;
}
synchronized (lockForInitDrivers) {
if (driversInitialized) {
return;
}
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty(JDBC_DRIVERS_PROPERTY);
}
});
} 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()) {
// SPI 的 iterator 是懒加载,全部遍历后才能确保「驱动实现类」的 Class 已经通过 Class.forName 完成加载
driversIterator.next();
}
} catch (Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers != null && !drivers.equals("")) {
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);
}
}
}
driversInitialized = true;
println("JDBC DriverManager initialized");
}
}
SPI 会通过 Class.forName 形式加载 Class 文件
// From ServiceLoader.java : ServiceLoader.LazyClassPathLookupIterator
private Class<?> nextProviderClass() {
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null) {
configs = ClassLoader.getSystemResources(fullName);
} else if (loader == ClassLoaders.platformClassLoader()) {
// The platform classloader doesn't have a class path,
// but the boot loader might.
if (BootLoader.hasClassPath()) {
configs = BootLoader.findResources(fullName);
} else {
configs = Collections.emptyEnumeration();
}
} else {
configs = loader.getResources(fullName);
}
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return null;
}
pending = parse(configs.nextElement());
}
String cn = pending.next();
try {
return Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
return null;
}
}
mysql-connector-java SPI 实现
// From com.mysql.fabric.jdbc.FabricMySQLDriver.java
public class FabricMySQLDriver extends NonRegisteringDriver implements Driver {
// may be extended to support other protocols in the future
public static final String FABRIC_URL_PREFIX = "jdbc:mysql:fabric://";
// 省略代码 ...
// Register ourselves with the DriverManager
static {
try {
DriverManager.registerDriver(new FabricMySQLDriver());
} catch (SQLException ex) {
throw new RuntimeException("Can't register driver", ex);
}
}
// 省略代码 ...
}
深入剖析Spring Boot 的SPI机制
- META-INF/services/以「接口全限定名」命名的文件
- ServiceLoader#load
虽然java提供的SPI机制的思想非常好,但是也存在相应的弊端。
- 接口实现类只能通过程序员手动遍历来确认;
- 服务提供接口必须放到 META-INF/services/ 目录下。
针对java的spi存在的问题,Spring的SPI机制沿用的SPI的思想,但对其进行扩展和优化。Spring采用 spring.factories 方式实现SPI机制,有极强的扩展性。
Java SPI 机制每个服务提供接口都在 META-INF/services/ 目录下分别对应一个配置文件,配置文件中存放当前接口的所有实现类;Spring SPI 机制则在 META-INF/spring.factories 配置文件存放多个接口及对应的实现类:以接口全限定名作为key,实现类作为value来配置,多个实现类用逗号隔开。
SpringBoot 实现「自动装配」的原理就是通过 @EnableAutoConfiguration 执行了 SpringBoot 的 SPI 机制,加载、解析 ClassPath 路径下的所有 spring.factories 文件来实现的。
// From SpringFactoriesLoader.java
/**
* The location to look for factories.
* <p>Can be present in multiple JAR files.
*/
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
// ...
try {
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
// ...
return result;
}
META-INF/spring.factories 文件可以存在多个 JAR 文件中,基于这个「约定」,可以围绕 Spring 生态做许多扩展。
其它
- Java SPI META-INF/services 详解
API (Application Programming Interface)在大多数情况下,都是实现方制定接口协议并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。
SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。
- Java Service Provider Interface:SPI 使用案例
The Service Provider is installed in the form of extensions, a jar file which we place in the application classpath, the Java extension classpath or the user-defined classpath.
- what’s the java classpath
classpaths contain:JAR files, and Paths to the top of package hierarchies.
how to set the classpath:1)通过操作系统环境变量设置;2) 通过 JVM 参数设置:java -cp parameter