前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。
最近,公司需要针对一个使用C#的系统以插件的形式进行二次开发。系统提供了一个类库,我们只需要实现类库中的接口,并实现相应的方法,即可完成一个插件的开发。
然后,我们将实现类打包成dll文件,这个dll文件就像是Java中的jar包一样。我们将这个dll文件上传到指定的文件夹,系统会热更新并加载这个dll文件。之后,我们需要在系统中配置该类的全限定名,这样插件就可以生效了。
这种插件式开发方式非常有趣,我在想Java能否实现这样的动态扩展。经过一番查询和研究,我发现Java确实提供了类似的功能,那就是SPI,本文将对SPI进行一个简单的介绍。
SPI全称Service Provider Interface,是一种服务提供者接口,定义了一组用于实现特定服务的接口或抽象类。它是一种动态替换发现的机制,使得接口和实现可以分离。SPI被专门提供给服务提供者或扩展框架功能的开发者去使用,由调用方提供接口,第三方(提供方)来实现。
SPI(Service Provider Interface) 和 API(Application Programming Interface) 有以下几个区别:
定义方式:API可以是一组函数、类、协议或工具,通常易于理解和使用。API 定义了可供开发者调用的公共方法和功能。而 SPI 是一种机制,用于揭示和加载服务提供者的实现,它通常以接口的形式存在。
使用方式:API 是开发者在应用程序中直接调用的,开发者通过使用 API 提供的方法和功能来实现特定的业务逻辑。而 SPI 是通过类加载器和反射机制动态加载和实例化服务提供者的,开发者无需直接调用 SPI 的机制,而是通过使用 SPI 加载的服务实例来实现特定的功能。
扩展性:API 的设计目的是为了提供一套公共的接口和方法,以便开发者进行二次开发和扩展。而 SPI 的目的是为了实现解耦合和动态加载,允许第三方服务提供者通过 SPI 机制向应用程序中注入其实现,从而实现功能的扩展和替换。
总的来说,API是面向使用者的,它定义了如何使用软件组件;而SPI是面向提供者的,它定义了如何为软件组件提供服务 。
Java SPI是Java 6引入的一种服务发现机制。主要包括以下4个核心概念:
META-INF/services
目录下的配置文件,用于声明服务提供者的实现类。文件名称为服务接口的全限定名,文件内容为服务提供者接口的全限定名。当应用程序需要使用某项服务时,它会在类路径下查找 META-INF/services
目录下的配置文件,文件中列出了对应服务接口的实现类。然后,应用程序可以通过 Java 标准库提供的 ServiceLoader
类动态加载并实例化这些服务提供者,从而使用其功能。这种机制被广泛应用于 Java 中各种框架和组件的扩展开发,例如数据库驱动、日志模块等。
Java SPI的优缺点:
ServiceLoader
类的实例是不安全的。加载不到实现类时抛出并不是真正原因的异常,错误很难定位。Java 提供了许多 SPI,以下是SPI及其提供的服务的一些示例:
java.util.spi.CurrencyNameProvider
:为Currency类提供本地化的货币符号。java.util.spi.LocaleNameProvider
: 为 Locale 类提供本地化名称。java.sql.Driver
:从 4.0 版本开始,JDBC API 支持 SPI 模式。旧版本使用 Class.forName()
方法加载驱动程序。jakarta.persistence.spi.PersistenceProvider
:提供JPA API的实现。java.text.spi.DateFormatProvider
:为指定区域设置的日期和时间格式。java.util.spi.TimeZoneNameProvider
:为 TimeZone 类提供本地化时区名称。javax.sound.sampled.spi
:Java声音SPI接口,开发者可以提供自定义的音频文件读取和写入插件。javax.imageio.spi
:Java图像I/O的SPI接口,开发者可以提供自定义的图像读取和写入插件。下面我们用一个搜索电影的例子来写一个Java SPI的示例。有时候一个人心血来潮想看一些武打电影,我们可以通过UC搜索,UC资源有限,有时就需要使用魔法通过谷歌搜索来找电影,反正不管是黑猫白猫,抓到老鼠就是好猫。所以,可以提供一个搜索电影的服务接口,具体使用哪个搜索就看个人喜好了。
spi-provider
模块,定义一个电影提供者接口MovieProvider
,里面有一个方法searchMovie()
。public interface MovieProvider {
/**
* 搜索电影
* @param movieName 电影名
*/
void searchMovie(String movieName);
}
uc-provider
和 google-provider
,分别引入spi-provider
模块,并在各自模块实现MovieProvider
接口。引入spi-provider
模块:
<dependency>
<groupId>BasicJava</groupId>
<artifactId>spi-provider</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
public class UCProvider implements MovieProvider{
/**
* 搜索电影
*
* @param movieName 电影名
*/
@Override
public void searchMovie(String movieName) {
System.out.println("通过UC搜索:"+movieName);
}
}
public class GoogleProvider implements MovieProvider{
/**
* 搜索电影
*
* @param movieName 电影名
*/
@Override
public void searchMovie(String movieName) {
System.out.println("通过谷歌搜索:"+movieName);
}
}
spi-consumer
模块,引入 uc-provider
和 google-provider
, 并在 META-INF/services
目录下,创建一个以MovieProvider
接口全限定名为命名的文本文件。<dependencies>
<dependency>
<groupId>BasicJava</groupId>
<artifactId>uc-provider</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>BasicJava</groupId>
<artifactId>google-provider</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
UCProvider
和 GoogleProvider
是否生效,然后动态的获取配置文件,话不多说,请看实操。创建 spi-config.properties
配置文件类:
spi-config.properties
配置文件内容如下:
site.sunlong.provider.GoogleProvider=enable
site.sunlong.provider.UCProvider=enable
在 spi-consumer
模块新建一个SpiConsumer用于测试,在该类中每10s读取一次配置文件的内容,判断插件是否生效,最后只调用生效的插件。具体代码如下:
public class SpiConsumer {
private static ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
public static void main(String[] args) {
ServiceLoader<MovieProvider> providers = ServiceLoader.load(MovieProvider.class);
String movieName = "波多";
executor.scheduleAtFixedRate(() -> {
// 读取配置文件
Properties properties = new Properties();
ClassLoader classLoader = SpiConsumer.class.getClassLoader();
try (InputStream input = classLoader.getResourceAsStream("spi-config.properties")) {
if (input == null) {
System.out.println("无法找到配置文件");
return;
}
properties.load(input);
} catch (IOException e) {
e.printStackTrace();
return;
}
for (MovieProvider provider : providers) {
String name = provider.getClass().getName();
String property = properties.getProperty(name);
if ("enable".equals(property)) {
provider.searchMovie(movieName);
}
}
System.out.println(DateTime.now());
System.out.println();
}, 0, 10, TimeUnit.SECONDS); // 初始延迟为0,之后每隔10秒执行一次
}
}
先后将site.sunlong.provider.GoogleProvider
和 site.sunlong.provider.UCProvider
设置为disable,输出结果如下:
通过谷歌搜索:波多
通过UC搜索:波多
2023-11-21T23:08:58.241+08:00
通过谷歌搜索:波多
通过UC搜索:波多
2023-11-21T23:09:08.213+08:00
通过UC搜索:波多
2023-11-21T23:09:18.200+08:00
通过UC搜索:波多
2023-11-21T23:09:28.215+08:00
通过UC搜索:波多
2023-11-21T23:09:38.208+08:00
通过谷歌搜索:波多
2023-11-21T23:09:48.202+08:00
通过谷歌搜索:波多
2023-11-21T23:09:58.210+08:00
打印结果虽然有延迟,但从结果中还是可以看出我们是实现了可插拔插件的功能,只是结果有瑕疵,可以把配置文件放到缓存或者数据库中以便准确的判断插件是否生效。
总的来说,Java SPI 的实现原理是通过类加载器动态加载配置文件,再利用反射机制实例化具体的服务提供者,并将其注入到应用程序中供服务消费者使用。