Java SPI介绍与实践


一、模式介绍

1. 基本信息

当你需要开发一个可插拔的 Java 应用程序时,你会发现一个问题:如何在运行时加载实现了相同接口的多个类,而这些类在编译时是未知的?这就是 Java SPI 的作用。

Java SPI(Service Provider Interface)Java 提供的一种标准机制,用于在运行时自动发现和加载实现某个特定接口的类。它是通过在 JAR 包的 META-INF/services 目录下提供一个特定的文件来实现的,这个文件的名字是特定接口的全限定名,它的内容是实现该接口的类的全限定名。

二、案例演示

1. 工程结构

假设有一个接口 xyz.ibudai.service.HelloService 与其两个接口实现类 StudentHelloServiceTeacherHelloService,三者定义结构如下:

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 实现了多个扩展点,例如 BeanPostProcessorBeanFactoryPostProcessor 等。

在使用 Java SPI 时,我们也需要避免一些常见问题。例如,在实现类中不要引用接口,否则可能会导致循环依赖的问题,同时也需要注意实现类和接口的类加载器问题,确保它们在同一个类加载器中。

3. 总结归纳

首先,在实现类中避免引用接口,以免出现循环依赖的问题。其次,在程序中通过 ServiceLoader.load(Class<T> service) 方法加载实现类。最后,在编写实现类时,确保它们与接口位于同一个类加载器中。

总的来说,Java SPI 是一个非常有用的机制,可以帮助我们实现可插拔的应用程序,并提高应用程序的灵活性和可扩展性。在使用 Java SPI 时,我们需要遵循一些最佳实践,同时也需要注意一些潜在的问题,以确保应用程序能够正确地加载和使用实现类。


文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录