当Java SPI遇上Groovy

本文描述一种Java SPI机制与Groovy相结合的方式,实现中借鉴了Dubbo SPI的思想,旨在提供一种更加动态灵活的集成方式。抛砖引玉。

从Java SPI说起

SPI,想必大家对此耳熟能详,全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器(java.util.ServiceLoader)读取配置文件,加载实现类。这样可以在运行时动态地为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。

SPI机制的约定:

  1. 在META-INF/services/目录中创建以接口全限定名命名的文件该文件内容为Api具体实现类的全限定名

  2. 使用ServiceLoader类动态加载META-INF中的实现类

  3. 如SPI的实现类为Jar则需要放在主程序classPath中

  4. API具体实现类必须有一个不带参数的构造方法

以JDBC MySQL connector为例,在mysql-connector-java.jar中, META-INF/services/java.sql.Driver文件中写明了

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

java.sql.DriverManager类初始化时会调用loadInitialDrivers 方法,该方法通过ServiceLoader#load遍历所有的Driver,从而加载到MySQL driver的实现类。

 

Dubbo SPI

Dubbo 并未使用原生的Java SPI,而是重新实现了一套功能更强的 SPI 机制。

Dubbo框架本身提供了很多的扩展点加载。例如协议、负载均衡、序列化、线程池等等,在具体使用时,只需要使用对应的参数配置即可引入实现。

例如,对于发布服务时的引用协议,protocol的值可以是dubbo、hessian、http、thrift等等,也可以是用户定义并实现的某一种协议。

<dubbo:provider protocol="xxx1" />

那么在具体的实现中,所有的服务协议实现自Protocol接口,不同的名称的协议通过ExtensionLoader加载,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类,而Dubbo SPI 所需的配置文件则放置在 META-INF/dubbo 路径下,对于com.alibaba.dubbo.rpc.Protocol,其配置内容如下

mock=com.alibaba.dubbo.rpc.support.MockProtocol
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
...

在文件中,key为不同协议的名称,value为对应的实现。

如果是用户自己的实现的Protocol,则需要将相应实现打成jar在classpath下,并且jar文件的 META-INF/dubbo目录下包含对应的com.alibaba.dubbo.rpc.Protocol接口文件和配置内容。

总结一下,Java SPI 和 Dubbo SPI都首先需要定义一个接口,然后通过一个Loader去加载指定目录下该接口文件名所定义的实现类。

 

我的SGI

现如今,SpringBoot大行其道,我们的应用war/jar包往往都是Fat Jar, 那么就意味着如果想要使用SPI,那么实现类的Jar则需要提前打到Fat Jar中,这样classLoader才能加载到Jar中的类,那么就需要预先引入依赖进行控制,而令这个SPI机制变得不那么的可插拔了。

退一步讲,即使是不使用SpringBoot,那么一个新的实现类引入,必须要放到classpath下的指定目录,然后免不了重启这个应用去加载。

那么能不能更加地动态灵活呢?

由此引出我的项目,基于Groovy实现的SPI,spi-groovy-integration简称SGI(https://github.com/timestatic/spi-groovy-integration),通过Groovy的动态灵活的特性,可以随时随地加载实现类,从文件、数据库.(关于Groovy的简介和Java项目的集成可参考上一篇文章Groovy简介与使用)。

下面举个例子:

首先使用 @SGI注解,定义一个接口,(旅行可以有多种方式)

@SGI
public interface TravelStrategy {
    String travel(String destination);
}

实现接口,并使用 @Impl注解,在注解中指定名称(可以骑自行车去旅行)

@GImpl(value = "bike")
public class BikeTravelStrategy implements TravelStrategy {
    @Override
    public String travel(String destination) {
        return "go to " + destination +  " by bike ~";
    }
}

使用 ExtensionLoader ,查找对应的SGI接口实现类并加载,然后根据名称调用相应方法(骑自行车去hangzhou)

ExtensionLoader<TravelStrategy> loader = ExtensionLoader.getExtensionLoader(TravelStrategy.class);
for (Class<?> clazz : PackageScanner.findGImplClass(TravelStrategy.class)) {
    loader.addExtension(clazz);
}
loader.getExtension("bike").travel("hangzhou")

也可以从外部文件中加载实现类, 例如将如下内容保存为NuoyafangzhuoTravelStrategy.groovy

package com.github.sgi.example.service

@GImpl(value = "nuoyafangzhou")
class NuoYaFangZhouTrave implements TravelStrategy {

    @Override
    String travel(String destination) {
        return "go to " + destination +  " by nuo ya fang zhou ~ .";
    }
}

在应用中读取该文件, 并加载(这样就可以及时应对发大水的情况了)

String path = "/root/file/NuoyafangzhuoTravelStrategy.groovy";
loader.addExtension(FileScanner.readFile(path));
loader.getExtension("nuoyafangzhou").travel("hangzhou")

当然, 也可以开启一个定时任务, 定时读取最新的文件内容重新加载

也可以将实现类注册为spring bean

@GImpl(isSpringBean = true, value = "flight")
// ....

// register
ExtensionSpringLoader.getExtensionSpringLoader(TravelStrategy.class).addExtensionSpringContext(script)
    
// get bean 
beanFactory.getBean("flight")

应用场景

结合业务系统的参数, 不同的参数可以调用不同的实现类, 基于Groovy动态的特性, 实现类本身也可以随时地变化,让系统更灵活,快速响应业务变化。

在某些环境下,比如需要对接甲方的业务系统,那么在我们自己的环境中是无法搭建相同的环境,同时对接文档可能也不够完善,难以测试,更是无法debug,全靠打日志推敲以解决问题。因此在对接的过程中免不了要反复的去传文件重启调试,当然也可以通过热部署或者像是Arthas的redfine重新加载class(用过之后依然感觉还是不够方便)。那么在使用了这种SPI机制之后,可以让代码的修改和类加载更加快速有效。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值