0、背景

总所周知,SPI在很多地方都有着很好的实践,比如JDBC驱动的加载、dubbo等,SpringBoot项目的autoConfiguration也是类似的原理。这里就感觉用来做模块解耦也不错。比如我们做应用管理的一个服务,它具有应用安装、升级、扩容等功能,经常在安装、升级、扩容流程中应用要求做一些定制化的东西,比如安装数据库,需要初始化database;安装kafka,需要初始化topic;对kafka的消费者进行扩容,需要同步扩topic的分区等等。而对于做应用管理的服务来说,它不应该区别化的对待这些应用,否则随着时间的发展,这个服务就会变得越来越复杂,难以与上层应用解耦。

这时候我们可以使用SPI的机制,为每一个支持扩展的流程定义一个扩展接口,交给上层应用去实现,使用SPI来加载这些实现类,从而实现让应用在安装、升级、扩容流程中加入自己个性化的功能。
对于应用管理服务和应用自身都有好处:

  • 于应用管理服务而言,只需要维护扩展接口,无需理解应用的逻辑,无需维护应用的个性化定制功能
  • 于上层应用而言,无需理解应用管理服务的逻辑,需要扩展就自己去实现接口,自己维护定制化的流程代码,当不需要的定制时候直接删除实现即可。

1、先玩一玩SPI吧

1.1 定义一个接口

新建一个module,名为service-spi,新增如下的接口:

package com.example.service.spi;

public interface MyServiceSpi {
    int order();
    void service();
}

这个接口中有两个方法,service是让应用自己去实现的扩展,order用来控制多个实现类的顺序,万一多个应用之间有顺序要求呢,可用这个order来控制。

1.2 定义第一个实现类

新建一个module名为service-impl-one,pom.xml中引用service-spi,新增实现类:

package com.example.service.imp.one;

import com.example.service.spi.MyServiceSpi;

public class ServiceImpleOne implements MyServiceSpi {

    public int order() {
        return 0;
    }

    public void service() {
        System.out.println("my service one");
    }
}

在resources中增加文件夹META-INF/services,新建一个名为com.example.service.spi.MyServiceSpi的文件,与接口的全路径一致
文件内容为com.example.service.imp.one.ServiceImpleOne

1.3 定义第二个实现类

新建一个module名为service-impl-two,pom.xml中引用service-spi,新增实现类:

package com.example.service.imp.two;

import com.example.service.spi.MyServiceSpi;

public class ServiceImplTwo implements MyServiceSpi {
    public int order() {
        return 1;
    }

    public void service() {
        System.out.println("my service two");
    }
}

在resources中增加文件夹META-INF/services,新建一个名为com.example.service.spi.MyServiceSpi的文件,与接口的全路径一致
文件内容为com.example.service.imp.two.ServiceImplTwo

1.4 调用MyServiceSpi

调用的module引用

        <dependency>
            <groupId>org.example</groupId>
            <artifactId>service-impl-one</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>service-imp-two</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

通过ServiceLoader获取所有实现类并排序后依次调用

        ServiceLoader<MyServiceSpi> loader = ServiceLoader.load(MyServiceSpi.class);
        Iterator<MyServiceSpi> iterator = loader.iterator();
        List<MyServiceSpi> myServiceSpiList = new ArrayList<>();
        while(iterator.hasNext()){
            myServiceSpiList.add(iterator.next());
        }
        // 根据order进行排序,order值小的先执行
        Collections.sort(myServiceSpiList, new Comparator<MyServiceSpi>() {
            @Override
            public int compare(MyServiceSpi o1, MyServiceSpi o2) {
                return o1.order()- o2.order();
            }
        });
        // 串行调用所有实现类
        for (MyServiceSpi serviceSpi:myServiceSpiList) {
            serviceSpi.service();
        }

运行后结果
my service one
my service two

2、思考

  • 调用扩展接口地方是不需要关心接口到底有没有实现类,有多少个实现类,这达到我们想要的模块之间解耦。
  • 但是,发现没有,提供接口的jar要和实现的jar跑在同一个jvm里面,这就意味着要么提前把扩展jar包全部打到我们的应用管理服务中,要么支持在单独安装或者升级时动态加载扩展jar,然后reload重新加载实现类。
Logo

Kafka开源项目指南提供详尽教程,助开发者掌握其架构、配置和使用,实现高效数据流管理和实时处理。它高性能、可扩展,适合日志收集和实时数据处理,通过持久化保障数据安全,是企业大数据生态系统的核心。

更多推荐