文章目录
1. JAVA的SPI机制
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来框架扩展和组件替换。在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。但如果需要替换另一种实现,就需要修改代码,生成新的jar包,违反了可拔插的原则。为了实现在模块装配的时候能不在程序里指明,这就需要一种服务发现机制。
Java SPI就是提供这样的一个服务发现机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外
,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
1.1 SPI的原理:
在jdk6里面引进的一个新的特性ServiceLoader
,从官方的文档来说,它主要是用来装载一系列的service provider
。而且ServiceLoader可以通过service provider的配置文件来装载指定的service provider。
当服务的提供者,提供了服务接口的一种实现之后,我们只需要在jar包的META-INF/services/
目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/
里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
如上图,JDBC可以适配多种数据源,其中Oracle就使用SPI机制,在META-INF/services/目录下创建了一个java.sql.Driver接口,在接口文件内部是对接口的具体的实现类
1.2 JAVA SPI的使用场景
数据库JDBC驱动加载
JDBC加载不同类型数据库的驱动日志门面接口实现类加载
SLF4J加载不同提供商的日志实现类Spring
- ServletContainerInitializer的实现
- 自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)
- 自动装配AutoConfiguration等等
Dubbo
Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口
其中以JDBC驱动加载为例:
- 首先在Java中定义了接口
java.sql.Driver
,并没有具体的实现,具体的实现都是由不同厂商提供。 - 在MySQL的jar包mysql-connector-java-6.0.6.jar中,可以找到
META-INF/services
目录,该目录下会有一个名字为java.sql.Driver
的文件,文件内容是com.mysql.cj.jdbc.Driver
,这里面的内容就是针对Java中定义的接口的实现。 - 同样在PostgreSQL的jar包PostgreSQL-42.0.0.jar中,也可以找到同样的配置文件,文件内容是
org.postgresql.Driver
,这是PostgreSQL对Java的java.sql.Driver的实现。
1.3 JAVA SPI代码示例
步骤1:
定义一组接口 (假设是org.foo.demo.IShout),并写出接口的一个或多个实现,(假设是org.foo.demo.animal.Dog、org.foo.demo.animal.Cat)。
public interface IShout {
void shout();
}
public class Cat implements IShout {
@Override
public void shout() {
System.out.println("miao miao");
}
}
public class Dog implements IShout {
@Override
public void shout() {
System.out.println("wang wang");
}
}
步骤2:
在 src/main/resources/
下建立 /META-INF/services
目录, 新增一个以接口命名的文件 (org.foo.demo.IShout文件),内容是要应用的实现类(这里是org.foo.demo.animal.Dog和org.foo.demo.animal.Cat,每行一个类)。
文件位置:
- src
-main
-resources
- META-INF
- services
- org.foo.demo.IShout
文件内容:
org.foo.demo.animal.Dog
org.foo.demo.animal.Cat
步骤3:
使用 ServiceLoader
来加载配置文件中指定的实现。
public class SPIMain {
public static void main(String[] args) {
ServiceLoader<IShout> shouts = ServiceLoader.load(IShout.class);
for (IShout s : shouts) {
s.shout();
}
}
}
代码输出
:
wang wang
miao miao
1.3 JAVA SPI 源码分析
java的spi机制是使用 ServiceLoader
来加载配置文件中指定的实现的,那么来看一下 ServiceLoader
的具体实现:
首先,ServiceLoader
实现了Iterable
接口,所以它有迭代器的属性。在iterator()
方法中主要都是调用的lookupIterator
的相应hasNext和next
方法,lookupIterator
是懒加载迭代器。
// ServiceLoader实现了Iterable接口,可以遍历所有的服务实现者
public final class ServiceLoader<S> implements Iterable<S>
{
// 查找配置文件的目录
private static final String PREFIX = "META-INF/services/";
// 表示要被加载的服务的类或接口
private final Class<S> service;
// 这个ClassLoader用来定位,加载,实例化服务提供者
private final ClassLoader loader;
// 访问控制上下文
private final AccessControlContext acc;
// 缓存已经被实例化的服务提供者,按照实例化的顺序存储
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒加载迭代器
private LazyIterator lookupIterator;
}
iterator()
方法
懒加载迭代器lookupIterator
中的hasNext()
方法中,静态变量PREFIX
就是”META-INF/services/”
目录,这也就是为什么需要在classpath下的META-INF/services/
目录里创建一个以服务接口命名的文件。
最后,通过反射方法Class.forName()
加载类对象,并用newInstance
方法将类实例化,并把实例化后的类缓存到providers
对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象。
1.3 JAVA SPI 的不足
- 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
- 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
- 多个并发多线程使用 ServiceLoader 类的实例是不安全的。
2. Dubbo的SPI机制
Dubbo并没有使用JAVA的SPI机制,而是自定义了一套SPI方案,那么Dubbo为什么不使用JAVA的SPI机制呢?结合JAVA SPI的不足,我们不难看出,JAVA SPI无法按需加载实现类,而是使用Iterator
将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。
Duboo支持多种协议的功能就是使用Dubbo SPI来实现的,导入了不同协议的jar包,在每个jar包下的META-INF/services/
目录中是这些协议的公共接口org.apache.dubbo.rpc.Protocol
,公共接口内部是不同协议具体的实现类的全类名。这个全类名和JAVA SPI的全类名有一些区别:
- JAVA SPI的全类名:
org.apache.dubbo.rpc.protocol.http.HttpProtocol
- Dubbo SPI的全类名:
http=org.apache.dubbo.rpc.protocol.http.HttpProtocol
可以看到Dubbo SPI在全类名前面加上了协议标识http,这就可以通过配置文件中的配置的协议来找到对应的协议实现类,不会加载全部的协议实现类,实现了按需加载!如果以后想要增加新的协议,同样只需要像已有协议那样,在类路径下的META-INF/services/
目录中制定新协议的具体实现类的全类名即可!
2.1 通过demo理解Dubbo SPI
Dubbo 对配置文件目录的约定,不同于 Java SPI ,Dubbo 分为了三类目录。
META-INF/services/
:该目录下的 SPI 配置文件是为了用来兼容 Java SPI 。META-INF/dubbo/
:该目录存放用户自定义的 SPI 配置文件。META-INF/dubbo/internal/
:该目录存放 Dubbo 内部使用的 SPI 配置文件。
接下来以一个示例来了解Dubbo SPI,首先定义接口car
,再写两个实现类RedCar
、BlackCar
// car接口
public interface Car {
String getCarName(URL url);
}
// redCar实现car接口
public class RedCar implements Car {
@Override
public String getCarName(URL url) {
return "red";
}
}
// blackCar实现car接口
public class BlackCar implements Car {
@Override
public String getCarName(URL url) {
return "black";
}
}
然后利用Dubbo SPI机制写配置文件,在META-INF/dubbo/
目录下新建文件com.demo.Car
文件,在文件内写入:
red=com.demo.RedCar
black=com.demo.BlackCar
通过Dubbo SPI 按需加载redCar
的实例
ExtensionLoader<Car> carExtensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
Car car = carExtensionLoader.getExtension("red");
System.out.println(car);
打印结果如下:
Dubbo SPI 除了可以按需加载实现类之外,增加了 IOC 和 AOP
的特性
- IOC:对应injectExtension方法 查找 set 方法,根据参数找到依赖对象则注入。
- AOP:对应WrapperClass,包装类是因为一个扩展接口可能有多个扩展实现类,而这些扩展实现类会有一个相同的或者公共的逻辑,如果每个实现类都写一遍代码就重复了,并且比较不好维护。因此就搞了个包装类,Dubbo 里帮你自动包装,
只需要某个扩展类的构造函数只有一个参数,并且是扩展接口类型,就会被判定为包装类
,然后记录到配置文件
,用来包装别的实现类。
以AOP为例,WrapperClass包装类代码如下:
public class CarWrapper implements Car {
private Car car;
//构造函数只有一个参数,并且是扩展接口car类型
public CarWrapper(Car car) {
this.car = car;
}
//包装类的具体实现
@Override
public String getCarName() {
System.out.println("wrapper...");
return null;
}
}
记录到配置文件
red=com.demo.RedCar
black=com.demo.BlackCar
com.demo.CarWrapper # 新增的包装类CarWrapper
还是通过Dubbo SPI 按需加载redCar
的实例,不过本次加入了包装类CarWrapper
,打印结果如下:可以看到redCar实例变成了CarWrapper包装类实例
,我们就可以在CarWrapper包装类中做代码扩展
3. Dubbo的SPI源码解析
从上文可以看到Dubbo的核心都在下面这几行代码中,可以看到大致流程就是先通过接口类找到一个 ExtensionLoader
,然后再通过 ExtensionLoader.getExtension(name)
得到指定名字的实现类实例
ExtensionLoader<Protocol> extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol protocol = extensionLoader.getExtension("http");
System.out.println(protocol);
3.1 获取接口对应的 ExtensionLoader
先看一下 ExtensionLoader.getExtensionLoader(Protocol.class)
获取 ExtensionLoader
的代码:
ExtensionLoader
中只是做了一些判断然后从缓存里面找是否已经存在这个接口类型的 ExtensionLoader
,如果没有就新建一个塞入缓存。最后返回接口类对应的 ExtensionLoader
。其中缓存是一个ConcurrentHashMap结构:
3.2 根据入参的http获取对应的http协议实例
再来看一下 getExtension()
方法:这个方法就是从类对应的 ExtensionLoader 中通过名字http
找到实例化完的http协议实现类。
可以看到Dubbo在获取对应的接口实例的时候,使用懒汉式考虑了线程安全问题,采用了一个Holder
作为锁,这个Holder
实际上是根据传入的name:http
创建的一个对象,那么在配置多协议的时候,将会有多个Holder
生成,比如:Dubbo
协议的Holder
、Http
协议的Holder
等等!
看到这里,你或许会有疑问,为什么不使用一个不变的对象作为锁呢?而要使用一个会根据不同协议不断变化的锁。Dubbo这样做的目的在于:
- 当同时有多个线程生成不同的协议实例时,比如两个线程分别去生成Http、dubbo协议实例。这个holder锁在多线程中根据不同的协议会被生成多个Hoder对象,
synchronized (holder)
中的holder
就有多个,这个锁等于没加,不会造成阻塞,多线程加快了多协议实例的生成! - 当同时有多个线程生成同一个协议实例时,比如多个线程同时生成Http协议。那么这时候肯定是只允许一个Http协议实例生成的,此时代码中的
synchronized (holder)
中的holder != null
,只允许一个线程进入代码生成,其他会被阻塞,保证同一个协议只有一个实例!
那么Dubbo是如何通过上述代码:instance = createExtension(name)
创建的http实例呢?
createExtension(name)
代码如下:
其中getExtensionClasses()
代码中会通过类加载器找到并遍历Dubbo支持的几种SPI配置文件中,找到所有的配置内容,如果有Wrapper
包装类会把它们放入缓存中,后续会进行AOP
操作,最后返回http实例instance
4. Dubbo的IOC依赖注入
上文我们已经知道Dubbo依赖注入的方法是injectExtension(instance)
这段代码,那么Dubbo是如何进行依赖注入的呢?先看下面这个例子
car接口和实现:
@SPI
public interface Car {
//@Adaptive注解声明了哪个方法要使用URL告知Dubbo
@Adaptive
String getCarName(URL url);
}
// redCar实现car接口
public class RedCar implements Car {
@Override
public String getCarName(URL url) {
return "red";
}
}
// blackCar实现car接口
public class BlackCar implements Car {
@Override
public String getCarName(URL url) {
return "black";
}
}
Person 接口和实现:
@SPI
public interface Person {
Car getCar();
}
//BlackPerson 实现Person接口
public class BlackPerson implements Person {
//Car需要被赋值
private Car car;
public void setCar(Car car) {
this.car = car;
}
@Override
public Car getCar() {
return car;
}
}
调用Person的getCar方法
ExtensionLoader<Person> extensionLoader = ExtensionLoader.getExtensionLoader(Person.class);
Person person = extensionLoader.getExtension("black");
//调用
System.out.println(person.getCar().getCarName(url));
打印结果会报错,因为Dubbo在为BlackPerson
类中的Car
属性进行赋值时,赋值的是一个Adaptive
代理对象,然后在使用代理对象调用getCarName
时,Dubbo不知道调用哪个Car的实现类中的方法,需要人为的告诉它,由于我们没有告诉Dubbo,所以会报错!
Dubbo规定在调用之前必须使用一个URL来告知调用哪个实现的方法!这样才不会报错!
//告知Dubbo调用blackCar的getCarName方法
URL url = new URL("x", "localhost", 8080);
url = url.addParameter("car", "black");
//调用
System.out.println(person.getCar().getCarName(url));
你可能会疑惑,为什么加个URL指定就可以了?带着这个问题,我们先看一下Dubbo是怎么依赖注入属性的,再看一下怎么就通过URL的配置,告诉Dubbo调用哪个实例的方法的!
依赖注入源码:
下面进入injectExtension(instance)
依注入代码中看一下具体实现!
由于AdaptiveExtensionFactory类
带有@Adaptive
注解且是ExtensionFactory = objectFactory
的实现,所以objectFactory.getExtension()
这个方法调用的其实是AdaptiveExtensionFactory.getExtension()
,这个方法内部在注入对象属性时,会先尝试去Spring容器中获取Car实现,如果没有,再由Dubbo创建Car
的Adaptive代理对象
。
从源码中截取到Adaptive代理对象内容如下:
import org.apache.dubbo.common.extension.ExtensionLoader;
//代理对象Car$Adaptive
public class Car$Adaptive implements com.tuling.Car {
//方法代理
public java.lang.String getCarName(org.apache.dubbo.common.URL arg0) {
//如果没有url参数,会抛异常
if (arg0 == null) throw new IllegalArgumentException("url == null");
//把参数赋值给url
org.apache.dubbo.common.URL url = arg0;
//根据配置的URL,获取car的实例为:black
String extName = url.getParameter("car");
if(extName == null) throw new IllegalStateException("Failed to get extension (com.tuling.Car) name from url (" + url.toString() + ") use keys([car])");
//通过name获取扩展点实例
com.tuling.Car extension = (com.tuling.Car)ExtensionLoader.getExtensionLoader(com.tuling.Car.class).getExtension(extName);
//调用black中的方法
return extension.getCarName(arg0);
}
}
可以看到:Dubbo在代理对象中强制要求: 必须有URL配置,并能通过key获取对应接口的扩展点实例,这样Dubbo才能知道调用哪个实例的getCarName
方法!
注意:
- 通过Dubbo生成代理类要调用某个方法时,方法上必须指定
@Adaptive
注解 - 通过Dubbo生成代理类要调用某个方法时,方法入参必须有
URL
,如:getCarName(URL url)
,或者是一个类,类中有setUrl方法。 - 如果不满足上述条件,Dubbo代理类调用方法时会报错!
5. Dubbo的自适应扩展点@Adaptive
@Adaptive可以为接口生成对应的扩展点实例,有两种方式:
- 在方法上加@Adaptive注解,代理对象是通过Dubbo内部生成代理类,然后生成代理对象的
- 在类上加@Adaptive注解,在Dubbo中还设计另外一种机制来生成自适应扩展点,这种机制就是可以通过@Adaptive注解来指定某个类为某个接口的代理类,如果指定了,Dubbo在生成自适应扩展点对象时实际上生成的就是@Adaptive注解所注解的类的实例对象。
6. Dubbo的AOP
dubbo中也实现了一套非常简单的AOP,就是利用Wrapper
,如果一个接口的扩展点中包含了多个Wrapper类,那么在实例化完某个扩展点后,就会利用这些Wrapper
类对这个实例进行包裹.
比如:现在有一个DubboProtocol
的实例,同时对于Protocol
这个接口还有很多的Wrapper
,比如ProtocolFilterWrapper
、ProtocolListenerWrapper
,那么,当对DubboProtocol
的实例完成了IOC之后,就会先调用new ProtocolFilterWrapper(DubboProtocol
生成一个新的Protocol
的实例,再对此实例进行IOC,完了之后,会再调用new ProtocolListenerWrapper(ProtocolFilterWrapper实例)
生成一个新的Protocol的实例,然后进行IOC,从而完成DubboProtocol
实例的AOP。