SPI 全称为 (Service Provider Interface),即服务提供商接口。常出现的地方有日志门面接口实现类加载、Spring SPI、JDK SPI、dubbo SPI、hadoop、solr cloud、elasticjob等,这里主要介绍下JDK SPI和dubbo SPI
java.util.ServiceLoader:
假设我们有一个服务类型是com.example.CodecSet,它有两个abstract方法:
public abstract Encoder getEncoder(String encodingName);public abstract Decoder getDecoder(String encodingName);
其中有一种提供者的编解码实现为com.example.impl.StandardCodecs,它实现了com.example.CodecSet的方法,那么可以在工程的 META-INF/services/com.example.CodecSet目录中添加一行com.example.impl.StandardCodecs # Standard codecs
然后在项目中就可以使用ServiceLoader.load(CodecSet.class)来获取和使用StandardCodecs了。
java.util.ServiceLoader#load(java.lang.Class<S>, java.lang.ClassLoader): /** * Creates a new service loader for the given service type and class * loader. * * @param <S> the class of the service type * * @param service * The interface or abstract class representing the service * * @param loader * The class loader to be used to load provider-configuration files * and provider classes, or <tt>null</tt> if the system class * loader (or, failing that, the bootstrap class loader) is to be * used * * @return A new service loader */ public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) { return new ServiceLoader<>(service, loader); }
可以看到,这里可以传入自定义的类加载器,可以从其他目录读取。
实际加载过程在java.util.ServiceLoader#iterator方法中:
public Iterator<S> iterator() { return new Iterator<S>() { Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator(); public boolean hasNext() { if (knownProviders.hasNext()) return true; return lookupIterator.hasNext(); } public S next() { if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next(); } public void remove() { throw new UnsupportedOperationException(); } }; }
这里有两点需要注意:1.查找接口时只能通过迭代的方式,查找时实际上是使用的lookupIterator的迭带方法来获取; 2.providers在多线程并发操作时会有线程安全问题
2.并发操作时的线程不安全性,如providers的使用上面。
dubbo中使用了大量的SPI,如rpc协议(Protocol),过滤器(Filter),序列化(Serialization),底层传输方式(Transporter),代理工厂(ProxyFactory),(注册中心)Registry,(负载均衡)LoadBalance,(容器)Container等多个地方,具体如下图:
com.alibaba.dubbo.common.extension.ExtensionLoader是spi拓展加载器。在ExtensionLoader的源码注释上说明了其在jdk spi上做的几点改进:
private T injectExtension(T instance) { try { if (objectFactory != null) { for (Method method : instance.getClass().getMethods()) { if (method.getName().startsWith("set") && method.getParameterTypes().length == 1 && Modifier.isPublic(method.getModifiers())) { Class<?> pt = method.getParameterTypes()[0]; try { //针对set方法大小写区分规范的属性进行注入 String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : ""; Object object = objectFactory.getExtension(pt, property); if (object != null) { method.invoke(instance, object); } } catch (Exception e) { logger.error("fail to inject via method " + method.getName() + " of interface " + type.getName() + ": " + e.getMessage(), e); } } } } } catch (Exception e) { logger.error(e.getMessage(), e); } return instance; }
这里注入的主要是@Adaptive和Wrapper实现。关于Adaptive和Wrapper后面会有专门的文章介绍。
示例可以看看ServiceConfig中的使用方法:
关于Adaptive extension的部分后面会专门分析,这里咱们关注一下com.alibaba.dubbo.common.extension.ExtensionLoader#getAdaptiveExtension方法:
private final Holder<Object> cachedAdaptiveInstance = new Holder<Object>(); 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("fail to create adaptive instance: " + t.toString(), t); } } } } else { throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError); } } return (T) instance; } public class Holder<T> { private volatile T value; public void set(T value) { this.value = value; } public T get() { return value; } }
这里是用一个Holder对象来缓存adaptive实例,使用double check的机制进行延迟加载,一方面防止重排序,另一方面降低同步的开销,Holder中的value为volatile是为了保证多线程条件下的线程可见性,这样double-check才能正常工作(http://gee.cs.oswego.edu/dl/cpj/updates.html)。相同的用法在 ExtensionLoader中还有很多,有兴趣的可以自己去翻一翻。
这里介绍了jdk spi和dubbo spi的相应机制,通过spi机制极大地提高了接口设计的灵活性,这也是dubbo能够达到高拓展性的一个基础。