概述
转自:https://www.jianshu.com/p/46aa69643c97
SPI 简介
SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 目前有不少框架用它来做服务的扩展发现, 简单来说,它就是一种动态替换发现的机制, 举个例子来说, 有个接口,想运行时动态的给它添加实现,你只需要添加一个实现,
而后,把新加的实现,描述给JDK知道就行啦(通过改一个文本文件即可)公司内部,目前Dubbo框架就基于SPI机制提供扩展功能。
简单示例
通过一个简单例子来说明SPI是如何使用的。 首先通过一张图来看看,用SPI需要遵循哪些规范,因为spi毕竟是JDK的一种标准。
我们首先需要一个目录,META-INFservices 如下,最终的目录路径就像这样:
└── src
├── com
│?? └── ivanzhang
│?? └── spi
│?? ├── HelloInterface.java
│?? ├── impl
│?? │?? ├── ImageHello.java
│?? │?? └── TextHello.java
│?? └── SPIMain.java
└── META-INF
└── services
└── com.ivanzhang.spi.HelloInterface
文件名字为 接口/抽象类: 全名 文件内容: 接口/抽象类 实现类
就像这样:com.ivanzhang.spi.impl.TextHello``com.ivanzhang.spi.impl.ImageHello
接下来, 我们需要定义接口和实现类:
public interface HelloInterface {
public void sayHello();
}
实现类:
public class TextHello implements HelloInterface {
@Override
public void sayHello() {
System.out.println("Text Hello.");
}
}
public class ImageHello implements HelloInterface {
@Override
public void sayHello() {
System.out.println("Image Hello");
}
}
最后,来看看,如果使用SPI机制,客户端代码:
public class SPIMain {
public static void main(String[] args) {
ServiceLoader<HelloInterface> loaders =
ServiceLoader.load(HelloInterface.class);
for (HelloInterface in : loaders) {
in.sayHello();
}
}
}
最后的输出:Text Hello.Image Hello
Dubbo SPI
Java SPI的使用很简单。也做到了基本的加载扩展点的功能。但Java SPI有以下的不足:
- 需要遍历所有的实现,并实例化,然后我们在循环中才能找到我们需要的实现。
- 配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们。
- 扩展如果依赖其他的扩展,做不到自动注入和装配
- 不提供类似于Spring的AOP功能
- 扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的Java SPI不支持
- 所以Java SPI应付一些简单的场景是可以的,但对于Dubbo,它的功能还是比较弱的。Dubbo对原生SPI机制进行了一些扩展。接下来,我们就更深入地了解下Dubbo的SPI机制。
Dubbo扩展点机制基本概念
在深入学习Dubbo的扩展机制之前,我们先明确Dubbo SPI中的一些基本概念。在接下来的内容中,我们会多次用到这些术语。
-
扩展点(Extension Point)
是一个Java的接口。 -
扩展(Extension)
扩展点的实现类。 -
扩展实例(Extension Instance)
扩展点实现类的实例。 -
扩展自适应实例(Extension Adaptive Instance)
第一次接触这个概念时,可能不太好理解(我第一次也是这样的…)。如果称它为扩展代理类,可能更好理解些。扩展的自适应实例其实就是一个Extension的代理,它实现了扩展点接口。在调用扩展点的接口方法时,会根据实际的参数来决定要使用哪个扩展。比如一个IRepository的扩展点,有一个save方法。有两个实现MysqlRepository和MongoRepository。IRepository的自适应实例在调用接口方法的时候,会根据save方法中的参数,来决定要调用哪个IRepository的实现。如果方法参数中有repository=mysql,那么就调用MysqlRepository的save方法。如果repository=mongo,就调用MongoRepository的save方法。和面向对象的延迟绑定很类似。为什么Dubbo会引入扩展自适应实例的概念呢?- Dubbo中的配置有两种,一种是固定的系统级别的配置,在Dubbo启动之后就不会再改了。还有一种是运行时的配置,可能对于每一次的RPC,这些配置都不同。比如在xml文件中配置了超时时间是10秒钟,这个配置在Dubbo启动之后,就不会改变了。但针对某一次的RPC调用,可以设置它的超时时间是30秒钟,以覆盖系统级别的配置。对于Dubbo而言,每一次的RPC调用的参数都是未知的。只有在运行时,根据这些参数才能做出正确的决定。
- 很多时候,我们的类都是一个单例的,比如Spring的bean,在Spring bean都实例化时,如果它依赖某个扩展点,但是在bean实例化时,是不知道究竟该使用哪个具体的扩展实现的。这时候就需要一个代理模式了,它实现了扩展点接口,方法内部可以根据运行时参数,动态的选择合适的扩展实现。而这个代理就是自适应实例。 自适应扩展实例在Dubbo中的使用非常广泛,Dubbo中,每一个扩展都会有一个自适应类,如果我们没有提供,Dubbo会使用字节码工具为我们自动生成一个。所以我们基本感觉不到自适应类的存在。后面会有例子说明自适应类是怎么工作的。
-
@SPI
@SPI注解作用于扩展点的接口上,表明该接口是一个扩展点。可以被Dubbo的ExtentionLoader加载。如果没有此ExtensionLoader调用会异常。 -
@Adaptive
@Adaptive注解用在扩展接口的方法上。表示该方法是一个自适应方法。Dubbo在为扩展点生成自适应实例时,如果方法有@Adaptive注解,会为该方法生成对应的代码。方法内部会根据方法的参数,来决定使用哪个扩展。 -
ExtentionLoader
类似于Java SPI的ServiceLoader,负责扩展的加载和生命周期维护。 -
扩展别名
和Java SPI不同,Dubbo中的扩展都有一个别名,用于在应用中引用它们。比如
random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance roundrobin=com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance
其中的random,roundrobin就是对应扩展的别名。这样我们在配置文件中使用random或roundrobin就可以了。
- 一些路径
和Java SPI从/META-INF/services目录加载扩展配置类似,Dubbo也会从以下路径去加载扩展配置文件:
- META-INF/dubbo/internal
- META-INF/dubbo
- META-INF/services
Dubbo的LoadBalance扩展点解读
在了解了Dubbo的一些基本概念后,让我们一起来看一个Dubbo中实际的扩展点,对这些概念有一个更直观的认识。
我们选择的是Dubbo中的LoadBalance扩展点。Dubbo中的一个服务,通常有多个Provider,consumer调用服务时,需要在多个Provider中选择一个。这就是一个LoadBalance。我们一起来看看在Dubbo中,LoadBalance是如何成为一个扩展点的。
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
/**
* select one invoker in list.
*
* @param invokers invokers.
* @param url refer url
* @param invocation invocation.
* @return selected invoker.
*/
@Adaptive("loadbalance")
<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}
LoadBalance接口只有一个select方法。select方法从多个invoker中选择其中一个。上面代码中和Dubbo SPI相关的元素有:
-
@SPI(RandomLoadBalance.NAME) @SPI作用于LoadBalance接口,表示接口LoadBalance是一个扩展点。如果没有@SPI注解,试图去加载扩展时,会抛出异常。@SPI注解有一个参数,该参数表示该扩展点的默认实现的别名。如果没有显示的指定扩展,就使用默认实现。RandomLoadBalance.NAME是一个常量,值是"random",是一个随机负载均衡的实现。 random的定义在配置文件META-INF/dubbo/internal/com.alibaba.dubbo.rpc.cluster.LoadBalance中:
random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance
roundrobin=com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance
leastactive=com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance
consistenthash=com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance
可以看到文件中定义了4个LoadBalance的扩展实现。由于负载均衡的实现不是本次的内容,这里就不过多说明。只用知道Dubbo提供了4种负载均衡的实现,我们可以通过xml文件,properties文件,JVM参数显式的指定一个实现。如果没有,默认使用随机。
- @Adaptive(“loadbalance”) @Adaptive注解修饰select方法,表明方法select方法是一个可自适应的方法。Dubbo会自动生成该方法对应的代码。当调用select方法时,会根据具体的方法参数来决定调用哪个扩展实现的select方法。@Adaptive注解的参数loadbalance表示方法参数中的loadbalance的值作为实际要调用的扩展实例。 但奇怪的是,我们发现select的方法中并没有loadbalance参数,那怎么获取loadbalance的值呢?select方法中还有一个URL类型的参数,Dubbo就是从URL中获取loadbalance的值的。这里涉及到Dubbo的URL总线模式,简单说,URL中包含了RPC调用中的所有参数。URL类中有一个Map parameters字段,parameters中就包含了loadbalance。
-
获取LoadBalance扩展
Dubbo中获取LoadBalance的代码如下:LoadBalance lb = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(loadbalanceName);?
使用ExtensionLoader.getExtensionLoader(LoadBalance.class)方法获取一个ExtensionLoader的实例,然后调用getExtension,传入一个扩展的别名来获取对应的扩展实例。
ExtensionLoader中含有一个静态属性:
ConcurrentMap, ExtensionLoader>EXTENSION_LOADERS = new ConcurrentHashMap, ExtensionLoader>();
用于缓存所有的扩展加载实例,这里加载LoadBalance.class,就以LoadBalance.class为key,创建的ExtensionLoader为value存储到上述EXTENSION_LOADERS中。
自定义一个LoadBalance扩展
本节中,我们通过一个简单的例子,来自己实现一个LoadBalance,并把它集成到Dubbo中。我会列出一些关键的步骤和代码。
-
实现LoadBalance接口
首先,编写一个自己实现的LoadBalance,因为是为了演示Dubbo的扩展机制,而不是LoadBalance的实现,所以这里LoadBalance的实现非常简单,选择第一个invoker,并在控制台输出一条日志。package com.dubbo.spi.demo.consumer;
public class DemoLoadBalance implements LoadBalance {
@Override public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException { System.out.println("DemoLoadBalance : Select the first invoker..."); return invokers.get(0); }
}
-
添加扩展配置文件
添加文件:META-INF/dubbo/com.alibaba.dubbo.rpc.cluster.LoadBalance。文件内容如下:demo=com.dubbo.spi.demo.consumer.DemoLoadBalance
-
配置使用自定义LoadBalance
通过上面的两步,已经添加了一个名字为demo的LoadBalance实现,并在配置文件中进行了相应的配置。接下来,需要显式的告诉Dubbo使用demo的负载均衡实现。如果是通过spring的方式使用Dubbo,可以在xml文件中进行设置。
在consumer端的dubbo:reference中配置
<dubbo:reference id="helloService" interface="com.dubbo.spi.demo.api.IHelloService" loadbalance="demo" />
- 启动Dubbo
启动Dubbo,调用一次IHelloService,可以看到控制台会输出一条DemoLoadBalance: Select the first invoker…日志。说明Dubbo的确是使用了我们自定义的LoadBalance。
总结一下,Dubbo SPI有以下的特点:
对Dubbo进行扩展,不需要改动Dubbo的源码
自定义的Dubbo的扩展点实现,是一个普通的Java类,Dubbo没有引入任何Dubbo特有的元素,对代码侵入性几乎为零。
将扩展注册到Dubbo中,只需要在ClassPath中添加配置文件。使用简单。而且不会对现有代码造成影响。符合开闭原则。
Dubbo的扩展机制支持IoC,AOP等高级功能
Dubbo的扩展机制能很好的支持第三方IoC容器,默认支持Spring Bean,可自己扩展来支持其他容器,比如Google的Guice。
切换扩展点的实现,只需要在配置文件中修改具体的实现,不需要改代码。使用方便。
。
最后
以上就是坦率钢铁侠为你收集整理的Java SPI机制简介,及dubbo SPI的全部内容,希望文章能够帮你解决Java SPI机制简介,及dubbo SPI所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复