文章目录
SPI
SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 目前有不少框架用它
来做服务的扩展发现,简单来说,它就是一种动态替换发现的机制。使用SPI机制的优势是实现解耦,
使得第三方服务模块的装配控制逻辑与调用者的业务代码分离
Java SPI
Java中如果想要使用SPI功能,先提供标准服务接口,然后再提供相关接口实现和调用者。这样就可以通
过SPI机制中约定好的信息进行查询相应的接口实现。
Java SPI遵循如下约定
- 当服务提供者提供了接口的一种具体实现后,在META-INF/services目录下创建一个以“接口全
限定名”为命名的文件,内容为实现类的全限定名; - 接口实现类所在的jar包放在主程序的classpath中;
- 主程序通过java.util.ServiceLoader动态装载实现模块,它通过扫描META-INF/services目录下
的配置文件找到实现类的全限定名,把类加载到JVM; - SPI的实现类必须携带一个无参构造方法;
Java SPI 示例:
public interface IHelloService {
String sayHello();
}
public class HumanHelloServiceImpl implements IHelloService {
@Override
public String sayHello() {
return "你好";
}
}
public class CatHelloServiceImpl implements IHelloService {
@Override
public String sayHello() {
return "Mio Mio";
}
}
import com.james.demo.IHelloService;
import java.util.ServiceLoader;
public class HelloServiceTest {
public static void main(String[] args) {
ServiceLoader<IHelloService> iHelloServices = ServiceLoader.load(IHelloService.class);
for (IHelloService iHelloService :iHelloServices) {
System.out.println(iHelloService.getClass().getName() + ":"+ iHelloService.sayHello());
}
}
}
Dubbo SPI
dubbo中大量的使用了SPI来作为扩展点,通过实现同一接口的前提下,可以进行定制自己的实现类。
比如比较常见的协议,负载均衡,都可以通过SPI的方式进行定制化,自己扩展。Dubbo中已经存在的
所有已经实现好的扩展点。
Dubbo中的SPI
翻看Dubbo的jar包,其大量的SPI在META-INF 下的internal目录下
比如
org.apache.dubbo.cache.CacheFactory
threadlocal=org.apache.dubbo.cache.support.threadlocal.ThreadLocalCacheFactory
lru=org.apache.dubbo.cache.support.lru.LruCacheFactory
jcache=org.apache.dubbo.cache.support.jcache.JCacheFactory
expiring=org.apache.dubbo.cache.support.expiring.ExpiringCacheFactory
org.apache.dubbo.common.serialize.Serialization
fastjson=org.apache.dubbo.common.serialize.fastjson.FastJsonSerialization
fst=org.apache.dubbo.common.serialize.fst.FstSerialization
hessian2=org.apache.dubbo.common.serialize.hessian2.Hessian2Serialization
native-hessian=org.apache.dubbo.serialize.hessian.Hessian2Serialization
java=org.apache.dubbo.common.serialize.java.JavaSerialization
compactedjava=org.apache.dubbo.common.serialize.java.CompactedJavaSerialization
nativejava=org.apache.dubbo.common.serialize.nativejava.NativeJavaSerialization
kryo=org.apache.dubbo.common.serialize.kryo.KryoSerialization
kryo2=org.apache.dubbo.common.serialize.kryo.optimized.KryoSerialization2
avro=org.apache.dubbo.common.serialize.avro.AvroSerialization
protostuff=org.apache.dubbo.common.serialize.protostuff.ProtostuffSerialization
gson=org.apache.dubbo.common.serialize.gson.GsonSerialization
protobuf-json=org.apache.dubbo.common.serialize.protobuf.support.GenericProtobufJsonSerialization
protobuf=org.apache.dubbo.common.serialize.protobuf.support.GenericProtobufSerialization
org.apache.dubbo.registry.client.ServiceDiscovery
file=org.apache.dubbo.registry.client.FileSystemServiceDiscovery
zookeeper=org.apache.dubbo.registry.zookeeper.ZookeeperServiceDiscovery
consul=org.apache.dubbo.registry.consul.ConsulServiceDiscovery
etcd3=org.apache.dubbo.registry.etcd.EtcdServiceDiscovery
eureka=org.apache.dubbo.registry.eureka.EurekaServiceDiscovery
nacos=org.apache.dubbo.registry.nacos.NacosServiceDiscovery
org.apache.dubbo.rpc.cluster.LoadBalance
random=org.apache.dubbo.rpc.cluster.loadbalance.RandomLoadBalance
roundrobin=org.apache.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance
leastactive=org.apache.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance
consistenthash=org.apache.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance
shortestresponse=org.apache.dubbo.rpc.cluster.loadbalance.ShortestResponseLoadBalance
Dubbo SPI遵循如下约定
- 定义的接口,必须加上org.apache.dubbo.common.extension.SPI注解,当服务提供者提供了接口的一种具体实现后,在META-INF/duboo目录下创建一个以“接口全限定名”为命名的文件,内容为标签=实现类的全限定名;
- 接口实现类所在的jar包放在主程序的classpath中;
- 主程序通过org.apache.dubbo.common.extension.ExtensionLoader 获取扩展加载器,它通过扫描META-INF/duboo目录下的配置文件找到实现类的全限定名,遍历所有的支持的扩展点(extensionLoader.getSupportedExtensions()),把类加载到JVM;
- 通过扩展点获取具体的实现
Dubbo SPI示例:
接口
import org.apache.dubbo.common.extension.SPI;
@SPI
public interface IHelloService {
String sayHello();
}
实现类
public class CatHelloServiceImpl implements IHelloService {
@Override
public String sayHello() {
return "Mio Mio";
}
}
public class DogHelloServiceImpl implements IHelloService {
@Override
public String sayHello() {
return "Wang Wang";
}
}
public class HumanHelloServiceImpl implements IHelloService {
@Override
public String sayHello() {
return "你好";
}
}
扩展点 META-INF/dubbo
com.james.demo.IHelloService
cat=com.james.demo.impl.CatHelloServiceImpl
dog=com.james.demo.impl.DogHelloServiceImpl
human=com.james.demo.impl.HumanHelloServiceImpl
测试
import org.apache.dubbo.common.extension.ExtensionLoader;
import java.util.Set;
public class DubboSpiTest {
public static void main(String[] args) {
/**
* 获取扩展加载器
*/
ExtensionLoader<IHelloService> extensionLoader = ExtensionLoader.getExtensionLoader(IHelloService.class);
/**
* 遍历所有的支持的扩展点 META-INF.dubbo
*/
Set<String> loadedExtensions = extensionLoader.getSupportedExtensions();
for (String extension : loadedExtensions) {
System.out.println(extension + ":" + extensionLoader.getExtension(extension).sayHello());
}
/**
* cat:Mio Mio
* dog:Wang Wang
* human:你好
*/
}
}
Dubbo中SPI的动态扩展(Adaptive)
Dubbo中的Adaptive功能,主要解决的问题是如何动态的选择具体的扩展点。通过
getAdaptiveExtension 统一对指定接口对应的所有扩展点进行封装,通过URL的方式对扩展点来进行
动态选择。 (dubbo中所有的注册信息都是通过URL的形式进行处理的。)这里同样采用相同的方式进行
实现。
org.apache.dubbo.common.extension.Adaptive
org.apache.dubbo.common.URL
接口:
api中的 HelloService 扩展如下方法, 与原先类似,在sayHello中增加 Adaptive 注解,并且在参数中
提供URL参数.注意这里的URL参数的类为 org.apache.dubbo.common.URL
其中@SPI(value = “human”)可以指定一个字符串参数,用于指明该SPI的默认实现。
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;
@SPI(value = "human")
public interface IHelloService {
String sayHello();
@Adaptive
String sayHello(URL url);
}
实现类
与上面Service实现类代码相似,只需增加URL形参即可
如:
public class HumanHelloServiceImpl implements HelloService {
@Override
public String sayHello() {
return "你好";
}
@Override
public String sayHello(URL url) {
return "你好";
}
}
扩展点
测试
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class DubboSpiAdaptiveTest {
public static void main(String[] args) {
/**
* 获取HelloService扩展加载器
* 通过 getAdaptiveExtension 来提供一个统一的类来对所有的扩展点提供支持(底层对所有的扩展
* 点进行封装)。
*/
HelloService helloServiceExtensionLoader = ExtensionLoader
.getExtensionLoader(HelloService.class)
.getAdaptiveExtension();
/**
* 因为在这里只是临时测试,所以为了保证URL规范,前面的信息均为测试值即可,关键的点在于
* hello.service 参数,这个参数的值指定的就是具体的实现方式。关于为什么叫
* hello.service 是因为这个接口的名称,其中后面的大写部分被dubbo自动转码为 . 分割。
*
*
* 调用时通过参数中增加 URL 对象来实现动态的扩展点使用
*/
System.out.println(helloServiceExtensionLoader.sayHello(URL.valueOf("test://localhost/hello?hello.service=dog")));
System.out.println(helloServiceExtensionLoader.sayHello(URL.valueOf("test://localhost/hello?hello.service=cat")));
System.out.println(helloServiceExtensionLoader.sayHello(URL.valueOf("test://localhost/hello?hello.service=human")));
/**
* 如果URL没有提供该参数,则该方法会使用默认在 SPI 注解中声明的实现。
* 这里是使用默认扩展点, 即在接口@SPI中配置的默认值@SPI(value = "human")
*/
System.out.println(helloServiceExtensionLoader.sayHello(URL.valueOf("test://localhost/hello")));
}
}
上面测试的注意点:
- 在这里只是临时测试,所以为了保证URL规范,前面的信息均为测试值即可,关键的点在于
hello.service 参数,这个参数的值指定的就是具体的实现方式。为什么是hello.service? 是因为这个接口的名称,其中后面的大写部分被dubbo自动转码为 . 分割。 - 通过 getAdaptiveExtension 来提供一个统一的类来对所有的扩展点提供支持(底层对所有的扩展
点进行封装)。 - 调用时通过参数中增加 URL 对象来实现动态的扩展点使用。
- 如果URL没有提供该参数,则该方法会使用默认在 SPI 注解中声明的实现
Dubbo为什么不用Java的SPI
1. JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加 载,会很浪费资源
2. 如果有扩展点加载失败,则所有扩展点无法使用
3. 提供了对扩展点包装的功能(Adaptive),并且还支持通过set的方式对其他的扩展点进行注入