一、模式介绍
1. 基本信息
当你需要开发一个可插拔的 Java 应用程序时,你会发现一个问题:如何在运行时加载实现了相同接口的多个类,而这些类在编译时是未知的?这就是 Java SPI 的作用。
Java SPI(Service Provider Interface) 是 Java 提供的一种标准机制,用于在运行时自动发现和加载实现某个特定接口的类。它是通过在 JAR 包的 META-INF/services 目录下提供一个特定的文件来实现的,这个文件的名字是特定接口的全限定名,它的内容是实现该接口的类的全限定名。
二、案例演示
1. 工程结构
假设有一个接口 xyz.ibudai.service.HelloService 与其两个接口实现类 StudentHelloService 和 TeacherHelloService,三者定义结构如下:
package xyz.ibudai.service;
public interface HelloService {
String getName();
void sayHello();
}
class StudentHelloService implements HelloService {
@Override
public String getName() {
return "Student";
}
@Override
public void sayHello() {
System.out.println("Student say hello.");
}
}
class TeacherHelloService implements HelloService {
@Override
public String getName() {
return "Teacher";
}
@Override
public void sayHello() {
System.out.println("Teacher say hello.");
}
}
2. 文件配置
接下来在工程的 resources 目录下创建 META-INF/services 目录,并在该目录下创建 xyz.ibudai.service.HelloService 文件,注意这个文件名必须和接口类的完整限定名一致,并在文件内定义需要加载的实现类完整限定名。
如上述创建了两个接口实现类,若需要加载两个类则其配置内容如下:
xyz.ibudai.service.Impl.StudentHelloService
xyz.ibudai.service.Impl.TeacherHelloService
3. 加载实现
在完成第二步中的配置之后,当程序运行时 Java SPI 就会自动加载 META-INF/services 目录配置文件中定义的实现类,并将它们实例化,当需要适配多个工程时即可增改配置文件从而实现动态的注册加载。
而在代码实现中则无需像传统一样通过手动 new 的方式创建对象,使用 ServiceLoader 即可获取已经自动实例化完成的对象,即 META-INF/services 配置文件中的类对象。
需要注意通过 load() 获取的实例集合并无法直接区分,因此在定义接口时通常会添加标识,如上述的中的 getName() 方法即用于标识作用。
public void demo() {
Map<String, HelloService> serviceMap = new HashMap<>();
ServiceLoader<HelloService> loaders = ServiceLoader.load(HelloService.class);
for (HelloService service : loaders) {
// 根据标识存入
serviceMap.put(service.getName(), service);
}
// 获取 StudentHello 实例对象
HelloService student = serviceMap.get("Student");
student.sayHello();
// 获取 TeacherHello 实例对象
HelloService teacher = serviceMap.get("Teacher");
teacher.sayHello();
}
三、功能特性
1. 优缺点
它可以帮助我们实现可插拔的应用程序,如果需要扩展应用程序,只需编写一个新的实现类,并将其打包为 JAR 文件即可,而不需要改变应用程序的源代码。
其次,它提供了一种标准化的机制,让开发者可以更加方便地编写和使用扩展。Java SPI 已经被广泛使用在许多框架和库中,例如 Spring、Hibernate 等。
但是由于 Java SPI 是通过类加载器来加载实现类的,因此存在类加载器隔离的问题。如果实现类和接口位于不同的类加载器中,就无法通过 Java SPI 加载实现类。
其次,Java SPI 可能会导致依赖冲突的问题。由于 Java SPI 是基于接口来进行扩展的,因此可能存在多个实现类都实现了同一个接口,这可能会导致依赖冲突的问题。最后,使用 Java SPI 需要遵循一些最佳实践,例如不要在 SPI 实现类中引用 SPI 接口,否则可能会导致循环依赖的问题。
2. 应用场景
在 Spring 框架中,通过 Java SPI 实现了多个扩展点,例如 BeanPostProcessor、BeanFactoryPostProcessor 等。
在使用 Java SPI 时,我们也需要避免一些常见问题。例如,在实现类中不要引用接口,否则可能会导致循环依赖的问题,同时也需要注意实现类和接口的类加载器问题,确保它们在同一个类加载器中。
3. 总结归纳
首先,在实现类中避免引用接口,以免出现循环依赖的问题。其次,在程序中通过 ServiceLoader.load(Class<T> service) 方法加载实现类。最后,在编写实现类时,确保它们与接口位于同一个类加载器中。
总的来说,Java SPI 是一个非常有用的机制,可以帮助我们实现可插拔的应用程序,并提高应用程序的灵活性和可扩展性。在使用 Java SPI 时,我们需要遵循一些最佳实践,同时也需要注意一些潜在的问题,以确保应用程序能够正确地加载和使用实现类。