【java】从java SPI机制到dubbo SPI机制(源码浅析)
(仔细读了源码画了流程图,对dubbo的SPI机制总算有了了解,期待以后继续的收获,痛并快乐着)
java的SPI机制到底是什么?
简单实现一个java-SPI示例,其有什么不足?
dubbo源码如何实现改进SPI,对SPI进行AOP、IOC、自适应、自激活?
(dubbo为2.7.2版本)
目录
一、什么是SPI?
SPI 即 Service Provider Interface(服务提供者接口),是一种将服务接口与服务实现分离以达到解耦、大大提升了程序可扩展性的机制。可以通过服务提供者接口引入服务提供的实现类,实现可插拔。
有人可能会问:SPI和我们熟悉的API有何不同?
如图,api 指 服务提供者提供具体方法,服务使用者通过api调用这个具体方法。spi 指 服务提供者提供接口,服务开发者通过接口约束和规范开发服务,而服务使用者就可以通过接口去调用任意服务开发者就该接口开发的服务。举个例子:java提供了数据库连接接口,mysql实现该接口,提供连接mysql数据库的方法,Oracle也实现了该接口,提供了连接Oracle数据库的方法,服务使用者就可以通过调用java的数据库连接接口选择连接某一数据库。
二、简单的SPI示例实现
依旧不太清楚的可以实现以下代码去理解SPI
1.服务提供者提供的接口
package com.funyoo.javaSpi;
/**
* 连接提供者接口
*/
public interface ConnectionDeveloper {
/**
* 连接数据库方法
* @param ip
* @param user
* @param password
* @return
*/
public String connection(String ip, String user, String password);
}
打成jar包
2.服务开发者使用该接口开发服务
MysqlConnection类实现ConnectionDevelop接口,即服务开发者根据接口规定开发服务。
package com.funyoo.javaSpi;
public class MysqlConnection implements ConnectionDeveloper {
@Override
public String connection(String ip, String user, String password) {
System.out.println("用户:" + user +" 连接mysql数据库[" + ip + "]" + " 密码为" + password);
return "连接mysql数据库成功";
}
}
META-INF/services文件夹底下存放实现某接口的服务类
打成jar包,为服务开发者根据该接口开发的服务。
3.服务调用者通过接口调用服务
package com.funyoo.javaSpiTest;
import com.funyoo.javaSpi.ConnectionDeveloper;
import java.util.ServiceLoader;
public class javaSpiTest {
// 加载对应接口的服务,即实现类
public ServiceLoader<ConnectionDeveloper> serviceLoader = ServiceLoader.load(ConnectionDeveloper.class);
private ConnectionDeveloper getDeveloper() {
ConnectionDeveloper dev = null;
for (ConnectionDeveloper developer : serviceLoader) {
System.out.println("加载到 " + developer.getClass());
dev = developer;
}
if (dev == null) {
System.out.println("SPI失败");
}
return dev;
}
public static void main(String[] args) {
javaSpiTest test = new javaSpiTest();
ConnectionDeveloper developer = test.getDeveloper();
developer.connection("funyoo", "192.168.1.1", "123456");
}
}
4.结果
三、Java SPI有什么不足?
通过上面可以发现,SPI需要把所有的实现都实例化了,即便我们不需要,也都给实例化了。
dubbo文档的说法就是
JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
如果扩展点加载失败,连扩展点的名称都拿不到了。
比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,
但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,
这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,
会报不支持 ruby,而不是真正失败的原因。
四、dubbo SPI所做的优化
dubbo增加了对拓展点AOP、IOC的支持
1.AOP (拓展点自动包装)
dubbo把带有@SPI注解的接口称为一个拓展点,而其实现类就是该拓展点的拓展 ,大家都知道dubbo是支持多协议的,以Protocol接口为例,他就是一个拓展点。规定他的拓展必须实现以下方法。
@SPI("dubbo")
public interface Protocol {
// 默认接口
int getDefaultPort();
// 暴露服务
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
// 引用远程服务
@Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
// 销毁
void destroy();
}
dubbo实现Protocol接口的Wrapper类有两个,ProtocolFilterWrapper和ProtocolListenerWrapper,以ProtocolListenerWrapper为例,即,协议侦听包装者。
public class ProtocolListenerWrapper implements Protocol {
private final Protocol protocol;
public ProtocolListenerWrapper(Protocol protocol) {
if (protocol == null) {
throw new IllegalArgumentException("protocol == null");
}
this.protocol = protocol;
}
@Override
public int getDefaultPort() {
return protocol.getDefaultPort();
}
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
if (REGISTRY_PROTOCOL.equals(invoker.getUrl().getProtocol())) {
return protocol.export(invoker);
}
return new ListenerExporterWrapper<T>(protocol.export(invoker),
Collections.unmodifiableList(ExtensionLoader.getExtensionLoader(ExporterListener.class)
.getActivateExtension(invoker.getUrl(), EXPORTER_LISTENER_KEY)));
}
@Override
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
if (REGISTRY_PROTOCOL.equals(url.getProtocol())) {
return protocol.refer(type, url);
}
return new ListenerInvokerWrapper<T>(protocol.refer(type, url),
Collections.unmodifiableList(
ExtensionLoader.getExtensionLoader(InvokerListener.class)
.getActivateExtension(url, INVOKER_LISTENER_KEY)));
}
@Override
public void destroy() {
protocol.destroy();
}
}
从代码可以看出,Wrapper类并不是拓展点的真正实现,Wrapper类只不过起包装作用,其拥有真正的拓展,所以说,当扩展加载器 ExtensionLoader (作用同ServiceLoader)加载拓展时实际上返回的是Wrapper的实例。
当打开源码中另一个Wrapper类,会明显发现,这个Wrapper类实现接口方法的逻辑和另一个的完全不同。所以,Wrapper类的作用显而易见,通过它可以将所有拓展的公共逻辑放到自己这里处理,而每新加一个Wrapper就会新加逻辑,Wrapper在真正拓展执行方法的时候对其能做更多的操作,就如同是真正拓展的代理。这个就类似AOP。装饰者模式。
上面提到了扩展加载器 ExtensionLoader
2.IOC (拓展点自动装配)
dubbo将SPI的相关逻辑都封装在这个拓展加载器里面,通过ExtensionLoader我们可以加载指定的实现类,一个扩展接口就对应一个ExtensionLoader对象。
获取扩展加载器 ExtensionLoader流程
/**
* 获取拓展加载器
* @param type 接口类型
* @param <T>
* @return 拓展加载器
*/
@SuppressWarnings("unchecked")
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Extension type == null");
}
// 拓展点必须是一个接口
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
}
// 拓展点必须有@SPI注解
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type (" + type +
") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
}
// 从拓展点加载器缓存(ConcurrentHashMap)中获得拓展点加载器
// EXTENSION_LOADERS是static ConcurrentMap<Class<?>, ExtensionLoader<?>>类型的ConcurrentHashMap;
// 由此看出一个拓展点(即接口)对应一个加载器
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
// 若得不到则new拓展点加载器并放置进缓存中
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
如代码及我给出的注释所说,获取拓展点加载器必须确定拓展点类型为接口且带有@SPI注解,满足则会先在缓存中找,找不到才会采用构造方法创建。
看他创建拓展点加载器的构造方法
private ExtensionLoader(Class<?> type) {
this.type = type;
objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}
除了进行type设置就是生成了这个叫objectFactory(private final ExtensionFactory objectFactory)的东东。因此,所有拓展点都有这个实例。
构造拓展点加载器的时候,获得ExtensionFactory.class接口的拓展点加载器,结合上面获取加载器的方法,因此在前方先判断了type类型,避免陷入循环中。objectFactory由getAdaptiveExtension()获得,获取自适应拓展。
何为自适应拓展?可以理解为依赖注入IOC,当本拓展加载器实例中需要其他拓展加载器,这时通过反射调用setter方法进行依赖注入。下面会详细说说。
获取拓展点的自适应实例流程
getAdaptiveExtension()
/**
* 获取拓展点自适应实例流程
* @return 拓展点自适应类实例
*/
@SuppressWarnings("unchecked")
public T getAdaptiveExtension() {
// 从缓存中获取
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
if (createAdaptiveInstanceError == null) {
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
// 创建拓展点自适应实例
instance = createAdaptiveExtension();
// 设置缓存中
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
}
}
}
} else {
throw new IllegalStateException("Failed to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
}
}
return (T) instance;
}
获取拓展点的自适应实例流程---创建拓展点自适应实例
/**
* 创建拓展点的自适应实现实例
* @return 拓展点的自适应实现实例
*/
@SuppressWarnings("unchecked")
private T createAdaptiveExtension() {
try {
// 返回依赖注入后的拓展点实例
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
} catch (Exception e) {
throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
}
}
injectExtension(T)方法为对参数(拓展点实例)依赖注入。
依赖注入:injectExtension(T)
/**
* 依赖注入拓展点实例
* @param instance 拓展点实例
* @return 拓展点实例(依赖注入后)
*/
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
// 遍历拓展点实例的方法,通过setter方法注入依赖的拓展点
for (Method method : instance.getClass().getMethods()) {
if (isSetter(method)) {
/**
* Check {@link DisableInject} to see if we need auto injection for this property
*/
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
Class<?> pt = method.getParameterTypes()[0];
if (ReflectUtils.isPrimitives(pt)) {
continue;
}
try {
// 获取setter参数名
String property = getSetterProperty(method);
// 从实例列表中找到对应参数类型和参数名的实例
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
// 反射调用setter注入
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("Failed to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
很明显,拓展点实例是通过其下不带@DisableInject注解的setter进行反射注入,实现IOC
3.拓展点自适应
那如何得到参数(自适应拓展点实例)呢?----getAdaptiveExtensionClass().newInstance()
/**
* 获取拓展点自适应实现类类型
* @return
*/
private Class<?> getAdaptiveExtensionClass() {
// 从资源文件中加载拓展点的所有拓展类
getExtensionClasses();
// 获取缓存自适应类型
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
// 若资源文件里没有,则用字节码编译自适应拓展类
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
/**
* 获取拓展点类型表
* @return
*/
private Map<String, Class<?>> getExtensionClasses() {
// 从缓存获取
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// 载入拓展点所有实现类并缓存
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
下面就是去加载配置资源加载拓展点的所有实现类 loadExtensionClasses(),流程图很清楚,建议对照源码
什么是拓展点自适应?
有时候我们不希望在Dubbo启动阶段就加载所有的拓展类,而是希望在用到某个拓展类时才加载,这就需要借助于自适应拓展机制。
当拓展方法被调用的时候,才根据URL进行加载。Dubbo 会为拓展接口生成具有代理功能的代码。然后通过 javassist 或 jdk 编译这段代码,得到 Class 类。最后再通过反射创建代理类,在代理类中,就可以通过URL对象的参数来确定到底调用哪个实现类。
关于@Adaptive注解:
注解在拓展类上:这个类就是缺省的代理
注解在拓展点(接口)方法上:dubbo 动态的生成一个这个扩展点的适配扩展类(生成代码 ,动态编译实例化 Class ),名称为 扩展点 Interface 的简单类名 + $Adaptive ,例如 : Protocol$Adpative 。这么做的目的是为了在运行时去适配不同的扩展实例 , 在运行时通过传入的 URL 类型的参数或者内部含有获取 URL 方法的参数 ,从 URL 中获取到要使用的扩展类的名称 ,再去根据名称加载对应的扩展实例 ,用这个扩展实例对象调用相同的方法 。
4.拓展点自激活
在dubbo中,某些组件可以同时有多个实现同时加载时,就可以通过@Activate注解自动激活,常见的自动激活扩展,如过滤器Filter,有顺序要求,提供了三个排序属性,before、after和order。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Activate {
// 组过滤
String[] group() default {};
String[] value() default {};
// 排序
@Deprecated
String[] before() default {};
@Deprecated
String[] after() default {};
int order() default 0;
}
由上面的流程图可以了解到:在第一次获取拓展加载器的就是就会读取配置,加载相应拓展点的所有拓展,经过一系列判断,将其放入对应的缓存里。
最后还会缓存激活实现类(当然其内部也同样进行一系列判断,图中并没画出来)。
从拓展加载器中获取激活拓展类
public List<T> getActivateExtension(URL url, String[] values, String group) {
List<T> exts = new ArrayList<>();
List<String> names = values == null ? new ArrayList<>(0) : Arrays.asList(values);
if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
// 读取到拓展点的所有实现类
getExtensionClasses();
// 遍历自激活类缓存
for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
String name = entry.getKey();
Object activate = entry.getValue();
String[] activateGroup, activateValue;
if (activate instanceof Activate) {
activateGroup = ((Activate) activate).group();
activateValue = ((Activate) activate).value();
} else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
} else {
continue;
}
// 如果匹配到组和该类注解中组相同
if (isMatchGroup(group, activateGroup)) {
// 获取该拓展经一系列判断后加入列表
T ext = getExtension(name);
if (!names.contains(name)
&& !names.contains(REMOVE_VALUE_PREFIX + name)
&& isActive(activateValue, url)) {
exts.add(ext);
}
}
}
// 排序list
exts.sort(ActivateComparator.COMPARATOR);
}
List<T> usrs = new ArrayList<>();
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
if (!name.startsWith(REMOVE_VALUE_PREFIX)
&& !names.contains(REMOVE_VALUE_PREFIX + name)) {
if (DEFAULT_KEY.equals(name)) {
if (!usrs.isEmpty()) {
exts.addAll(0, usrs);
usrs.clear();
}
} else {
T ext = getExtension(name);
usrs.add(ext);
}
}
}
if (!usrs.isEmpty()) {
exts.addAll(usrs);
}
return exts;
}
这个方法就是在之前加载配置时缓存的cachedActivates中过滤查询符合条件的自动激活实例,并根据@Activate注解中配置的排序规则排序。就是一个过滤器,对我不需要的实例以排除。
五、总结
1.duboo对java的SPI进行了改进,可以在需要时加载,可以进行代理,可以依赖注入
2.对dubbo代码进行拓展,不需改动源码,对拓展点拓展的管理,只需要对配置文件进行管理,包括修改,增加等。
3.对于拓展加载器ExtensionLoader来说,一个拓展点只有一个加载器,一个拓展点也只有一个自适应拓展实例,其起代理(适配)作用。
4.自动激活扩展实现可以有多个,正因为有多个,自动激动扩展实现可能有顺序性,也可以分组,比如过滤器啊,***链都可以通过拓展相应拓展点,打注解,修改配置文件完成。
(仔细读了源码画了流程图,对dubbo的SPI机制总算有了了解,也有了不少收获,期待以后继续的收获,痛并快乐着)