一、模式介绍
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
时,我们需要遵循一些最佳实践,同时也需要注意一些潜在的问题,以确保应用程序能够正确地加载和使用实现类。