SPI解释的实际用法(例如JDBC)?
real usage of SPI explanation(JDBC for example)?
我想了解 Service provider interface(SPI)
我写了一个使用这种技术的例子:
Project structure:
主要:
ReportRenderer reportRenderer = ReportRenderer.getInstance();
System.out.println(reportRenderer.getClass());
ReportRenderer:
public class ReportRenderer {
public static ReportRenderer getInstance() {
final Iterator<ReportRenderer> providers = ServiceLoader.load(ReportRenderer.class).iterator();
if (providers.hasNext()) {
return providers.next();
}
return new ReportRenderer();
}
FileReportRenderer:
public class FileReportRenderer extends ReportRenderer {...
META-INF/services/my.spi.renderer.ReportRenderer
的内容:
my.spi.renderer.FileReportRenderer
我已经创建了 jar 并启动了应用程序:
D:\work\SPI_test\build\libs>java -jar SPI_test.jar
class my.spi.renderer.FileReportRenderer
这很清楚。但我不明白如何在我的实际应用程序中使用这个技巧。我可以获得哪些福利?
Wiki 说 JDBC
使用了这种技术。
我发现
mysql-connector-java-5.1.16-bin.jar里面有相关的东西:
mysql-连接器-java-5.1.16-bin.jar\META-INF\services\java.sql.Driver
它包含:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
请说明connectir内部是如何使用SPI的,为什么?这是实现 JDBC 的唯一方法吗?
服务定义允许动态发现接口的实现(或class)。例如,在 JDBC 中,java.sql.DriverManager
使用服务机制来查找和加载 java.sql.Driver
.
的实现
具体来说(来自 Java 9 的代码):
/**
* 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;
}
});
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);
}
}
}
您的代码不需要知道将使用哪个 driver(因此,不需要 Class.forName(<nameOfDriverClass>)
),它只需要在运行时出现。因此无需显式加载或配置要使用的 driver class。您真正需要的唯一东西是 JDBC url(可能通过配置或其他方式提供),并且 DriverManager
将尝试所有已加载的 driver 直到建立连接通过 driver 之一。这意味着如果您有不同的 driver 支持相同的 URL,那么您可以交换 driver。或者如果你想使用不同的数据库,你可以将它的 driver 放在 class 路径上并配置正确的 URL (我忽略了 SQL 方言的潜在问题).
所以要回答 follow-up 问题:
clarify how does connectir uses SPI inside and why
Connector/J 本身不使用服务加载器机制,它是 Java 本身用来加载 driver 的。 Connector/J 实现了这一点,因为自 JDBC 4.
以来,JDBC 规范要求这样做
JDBC 有一个警告:driver 需要在初始(系统)class 路径上才能自动加载。位于其他 class 路径(例如 Web 应用程序中的 drivers)的驱动程序可能仍需要显式加载。
如果你在 Java 内部搜索,你会发现 ServiceLoader
被广泛使用,一些例子:
java.awt.Toolkit
使用它来加载 javax.accessibility.AccessibilityProvider
实现,这允许 Java 加载 platform-specific 对辅助功能的支持
java.net.URL
使用它来加载 java.net.spi.URLStreamHandlerProvider
实现,它允许您添加对不同网络协议的支持(例如 http、ftp 等)
java.nio.charset.Charset
使用它来加载 java.nio.charset.spi.CharsetProvider
,这允许您向 Java 添加额外的字符集
- 等等,等等
工具和库可以使用服务加载器机制来支持plugins/extensions。在您自己的示例中,另一个图书馆,甚至可能是您图书馆的用户,可以实现他们自己的 ReportRenderer
,并让您的 tool/library.
自动发现和使用它
我想了解 Service provider interface(SPI)
我写了一个使用这种技术的例子:
Project structure:
主要:
ReportRenderer reportRenderer = ReportRenderer.getInstance();
System.out.println(reportRenderer.getClass());
ReportRenderer:
public class ReportRenderer {
public static ReportRenderer getInstance() {
final Iterator<ReportRenderer> providers = ServiceLoader.load(ReportRenderer.class).iterator();
if (providers.hasNext()) {
return providers.next();
}
return new ReportRenderer();
}
FileReportRenderer:
public class FileReportRenderer extends ReportRenderer {...
META-INF/services/my.spi.renderer.ReportRenderer
的内容:
my.spi.renderer.FileReportRenderer
我已经创建了 jar 并启动了应用程序:
D:\work\SPI_test\build\libs>java -jar SPI_test.jar
class my.spi.renderer.FileReportRenderer
这很清楚。但我不明白如何在我的实际应用程序中使用这个技巧。我可以获得哪些福利?
Wiki 说 JDBC
使用了这种技术。
我发现
mysql-connector-java-5.1.16-bin.jar里面有相关的东西:
mysql-连接器-java-5.1.16-bin.jar\META-INF\services\java.sql.Driver
它包含:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
请说明connectir内部是如何使用SPI的,为什么?这是实现 JDBC 的唯一方法吗?
服务定义允许动态发现接口的实现(或class)。例如,在 JDBC 中,java.sql.DriverManager
使用服务机制来查找和加载 java.sql.Driver
.
具体来说(来自 Java 9 的代码):
/**
* 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;
}
});
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);
}
}
}
您的代码不需要知道将使用哪个 driver(因此,不需要 Class.forName(<nameOfDriverClass>)
),它只需要在运行时出现。因此无需显式加载或配置要使用的 driver class。您真正需要的唯一东西是 JDBC url(可能通过配置或其他方式提供),并且 DriverManager
将尝试所有已加载的 driver 直到建立连接通过 driver 之一。这意味着如果您有不同的 driver 支持相同的 URL,那么您可以交换 driver。或者如果你想使用不同的数据库,你可以将它的 driver 放在 class 路径上并配置正确的 URL (我忽略了 SQL 方言的潜在问题).
所以要回答 follow-up 问题:
clarify how does connectir uses SPI inside and why
Connector/J 本身不使用服务加载器机制,它是 Java 本身用来加载 driver 的。 Connector/J 实现了这一点,因为自 JDBC 4.
以来,JDBC 规范要求这样做JDBC 有一个警告:driver 需要在初始(系统)class 路径上才能自动加载。位于其他 class 路径(例如 Web 应用程序中的 drivers)的驱动程序可能仍需要显式加载。
如果你在 Java 内部搜索,你会发现 ServiceLoader
被广泛使用,一些例子:
java.awt.Toolkit
使用它来加载javax.accessibility.AccessibilityProvider
实现,这允许 Java 加载 platform-specific 对辅助功能的支持java.net.URL
使用它来加载java.net.spi.URLStreamHandlerProvider
实现,它允许您添加对不同网络协议的支持(例如 http、ftp 等)java.nio.charset.Charset
使用它来加载java.nio.charset.spi.CharsetProvider
,这允许您向 Java 添加额外的字符集
- 等等,等等
工具和库可以使用服务加载器机制来支持plugins/extensions。在您自己的示例中,另一个图书馆,甚至可能是您图书馆的用户,可以实现他们自己的 ReportRenderer
,并让您的 tool/library.