从模版方法模式到 SPI 演变 :好的思想通用而持久

2019-10-28 17:32:53 浏览数 (1)

一般情况下,我们会通过 API 对外提供服务。这里,API 提供服务的接口的逻辑是固定的,换句话说,它具有通用性。但是,但我们遇到具有类似的业务逻辑的场景时,即核心的主干逻辑相同,而细节的实现略有不同,那我们该何去何从?很多时候,我们会选择提供多个 API 接口给不同的业务方使用。事实上,我们可以通过 SPI 扩展点来实现的更加优雅。什么是 SPI?SPI 的英文全称是 Serivce Provider Interface,即服务提供者接口,它是一种动态发现机制,可以在程序执行的过程中去动态的发现某个扩展点的实现类。因此,当 API 被调用时会动态加载并调用 SPI 的特定实现方法。

此时,你是不是联想到了模版方法模式。模板方法模式的核心思想是定义骨架,转移实现,换句话说,它通过定义一个流程的框架,而将一些步骤的具体实现延迟到子类中。事实上,在微服务的落地过程中,这种思想也给我们提供了非常好的理论基础。

现在,我们来看一个案例:电商业务场景中的未发货仅退款。这种情况在电商业务中非常场景,用户下单付款后由于各种原因可能就申请退款了。此时,因为不涉及退货,所以只需要用户申请退款并填写退款原因,然后让卖家审核退款。那么,由于不同平台的退款原因可能不同,我们可以考虑通过 SPI 扩展点来实现。

我们先来看下 JDK 对 SPI 机制的支持。在面向对象编程的设计中,我们会采取面向接口编程的方式。

代码语言:javascript复制
public interface IRefundSeason {
    default List<String> invoke(){
        // 1. 前置业务
        // 。。。
        // 2. 获取退款原因列表
        List<String> refundSeasons = getRefundSeasonList();
        // 3. 后置业务
        // 。。。
        return refundSeasons;
    }

    List<String> getRefundSeasonList();
}

这里,我们在简单地定义两个实现类。

代码语言:javascript复制
public class RefundSeason1 implements IRefundSeason{

    @Override
    public List<String> getRefundSeasonList() {
        return ImmutableList.of("商品降价","商品无货","其他");
    }
}

public class RefundSeason2 implements IRefundSeason{

    @Override
    public List<String> getRefundSeasonList() {
        return ImmutableList.of("不想要了","七天无理由","其他");
    }
}

服务提供者提供接口的一种实现后,我们需要在 META-INF/services 目录中创建一个以接口全限定名的文件 com.lianggzone.design.template_method.example.spi.IRefundSeason

这里,文件地内容为实现类的全限定名。

代码语言:javascript复制
com.lianggzone.design.template_method.example.spi.RefundSeason1

最后,我们通过测试代码来验证下功能。

代码语言:javascript复制
public class RefundSeasonTest {

    public static void main(String[] args) {
        ServiceLoader<IRefundSeason> loader = ServiceLoader.load(IRefundSeason.class);
        loader.forEach(i -> {
            List<String> refundSeasons = i.invoke();
            System.out.println(refundSeasons);
        });
    }
}

至此,我们实现了一个简单地 Java 的 SPI 功能。事实上,Java 中的 SPI 实现非常简单,我们可以阅读 java.util.ServiceLoader 类。

注意的是,ServiceLoader 每次加载都会生成一份实例,且只能遍历获取所有接口实例,非常浪费资源。同时,获取实现类不够灵活,不能根据某个参数获取对应的实现类,且不支持排序,会出现排序不稳定的情况。因此,很多框架为了解决以上的问题,重新实现了一套更强大的 SPI 机制。例如,Dubbo SPI 自定义了一套 SPI 机制,并把所需的配置文件需放置在 META-INF/dubbo 路径下。与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样可以按需加载指定的实现类。Dubbo SPI 源码分析,参见 http://dubbo.apache.org/zh-cn/docs/sourcecodeguide/dubbo-spi.html

0 人点赞