SPI机制简介
spi是一种服务发现机制,其目的是,在调用一个spi时,动态的选择不同的实现方式。这和api有很大区别,api是定死的,调用哪个api,其实现类的实现方式是一样的。而spi可以动态选择不同实现类作为实现。
其本质,是将接口实现类的全限定名配置在文件中,并由服务加载器读取,加载实现类,这样,就可以实现在运行时,动态为接口替换实现类。
常见的SPI机制
Java SPI、Spring SPI、Dubbo SPI
Java SPI
spi机制的配置文件保存在resources/META-INF.services下
文件名是interface的全限定名
例如:com.youlong.spi.ITestService
文件内容是接口实现类的全限定名列表
例如:
com.youlong.spi.impl.TestServiceImpl1 com.youlong.spi.impl.TestServiceImpl2
调用时使用ServiceLoader进行加载,假设该接口中定义doTest方法
例如:
java
ServiceLoader<ITestService> serviceLoader = ServiceLoader.load(ITestService.class);
serviceLoader.forEach(ITestService::doTest);
Java SPI并不能按需加载,容易造成资源浪费,并且只能通过iterator获取,不能通过某个参数来获取对应实现类。
Spring SPI
spi机制的配置文件保存在spring.factories文件,相比于java spi,spring的spi只要一个文件即可,所有spi实现对应关系都保存在一个文件,而java spi一个接口对应一个文件。
spring spi配置文件中,以kv形式保存
例如:
com.youlong.spi.ITestService =
com.youlong.spi.impl.TestServiceImpl1,
com.youlong.spi.impl.TestServiceImpl2
调用时使用SpringFactoriesLoader.loadFactories来加载实现类列表
例如:
java
List<ITestService> testServices = SpringFactoriesLoader.loadFactories(ITestService.class, Thread.currentThread().getContextClassLoader())
testServices.forEach(ITestService::doTest);
spring spi和java spi类似,还是无法获取固定的某个实现类,只能按顺序获取所有实现。
Dubbo SPI
Dubbo SPI所需配置文件放在META-INF/dubbo,命名按照接口的全限定名来命名
与Spring SPI和Java SPI配置不同,Dubbo SPI通过键值对的方式进行配置,K是实现方式命名,V是实现类全限定名
例子:
文件名:com.youlong.spi.ITestService
内容:
ini
testServiceImpl1 = com.youlong.spi.impl.TestServiceImpl1
testServiceImpl2 = com.youlong.spi.impl.TestServiceImpl2
com.youlong.spi.impl.TestServiceWrapper 注意:这个没有name的类,是AOP,dubbo的切面类
Dubbo SPI相关逻辑被封装在ExtensionLoader类中,通过此类可以按需加载不同实现类。另外,在要实现SPI机制的接口上,要加上@SPI注解,表示这个接口需要被扩展
获取所需实现类实例举例:
假设我们需要TestServiceImpl1实现类
java
ExtensionLoader<ITestService> loader = ExtensionLoader.getExtensionLoader(ITestService);
ITestService testServiceImpl1 = loader.getExtension("testServiceImpl1");
testServiceImpl1.doTest();
Dubbo SPI除了支持按需加载接口实现类,还增加了IOC和AOP特性。
下面,我们来深入探究以下Dubbo SPI的实现原理
Dubbo SPI实现原理
上面提到了Dubbo SPI使用ExtensionLoader来获取扩展点加载器
下面让我们看一下源码中getExtensionLoader是如何实现的
java
@Override
@SuppressWarnings("unchecked")
public <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
checkDestroyed();
// 为空则抛异常
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() + "!");
}
// 从本地缓存去获取对应的扩展点加载器
ExtensionLoader<T> loader = (ExtensionLoader<T>) extensionLoadersMap.get(type);
ExtensionScope scope = extensionScopeMap.get(type);
if (scope == null) {
SPI annotation = type.getAnnotation(SPI.class);
scope = annotation.scope();
extensionScopeMap.put(type, scope);
}
if (loader == null && scope == ExtensionScope.SELF) {
// create an instance in self scope
loader = createExtensionLoader0(type);
}
// 2. find in parent
if (loader == null) {
if (this.parent != null) {
loader = this.parent.getExtensionLoader(type);
}
}
// 3. create it
if (loader == null) {
loader = createExtensionLoader(type);
}
return loader;
}
这里特殊说明一下,扩展点加载器的本地缓存是ConsurrentMap类型的,也就是说它是线程安全的(注意:JavaSPI机制并不是线程安全的)
java
private final ConcurrentMap<Class<?>, ExtensionLoader<?>> extensionLoadersMap = new ConcurrentHashMap<>(64);
继续说getExtensionLoader方法,会从本地缓存获取ExtensionLoader,若获取不到,则回去new一个ExtensionLoader并放入到本地缓存,最后将其返回
下面继续看下getExtension()方法
java
public T getExtension(String name, boolean wrap) {
checkDestroyed();
// 名称为空则报错
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
}
// 名称为true,则加载默认扩展点,默认扩展点下面会讲
if ("true".equals(name)) {
return getDefaultExtension();
}
String cacheKey = name;
if (!wrap) {
cacheKey += "_origin";
}
// 这里用对象持有帮助对象Holder去辅助实现双检索单例
final Holder<Object> holder = getOrCreateHolder(cacheKey);
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
// 创建扩展点实例
instance = createExtension(name, wrap);
holder.set(instance);
}
}
}
return (T) instance;
}
默认扩展点:就是在接口的@SPI注解上定义的扩展点
例如:@SPI("testServiceImpl1")
默认扩展点就是testServiceImpl1 上述过程是去从Holder中获取一个扩展点实例(Holder也是一个ConcurrentHashMap,key是实例name),获取不到,则双检索单例去创建,并set给Holder。
下面看一下createExtension()是如何创建的(可以猜一下哎,肯定用了反射)
java
private T createExtension(String name, boolean wrap) {
// 先得到一个类
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null || unacceptableExceptions.contains(name)) {
throw findException(name);
}
try {
T instance = (T) extensionInstances.get(clazz);
if (instance == null) {
// 根据实现类得到一个实例
extensionInstances.putIfAbsent(clazz, createExtensionInstance(clazz));
instance = (T) extensionInstances.get(clazz);
// 在初始化前的处理
instance = postProcessBeforeInitialization(instance, name);
// 依赖注入IOC
injectExtension(instance);
// 后置处理
instance = postProcessAfterInitialization(instance, name);
}
if (wrap) {
List<Class<?>> wrapperClassesList = new ArrayList<>();
if (cachedWrapperClasses != null) {
wrapperClassesList.addAll(cachedWrapperClasses);
wrapperClassesList.sort(WrapperComparator.COMPARATOR);
Collections.reverse(wrapperClassesList);
}
// aop
if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
for (Class<?> wrapperClass : wrapperClassesList) {
Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
boolean match = (wrapper == null) ||
((ArrayUtils.isEmpty(wrapper.matches()) || ArrayUtils.contains(wrapper.matches(), name)) &&
!ArrayUtils.contains(wrapper.mismatches(), name));
if (match) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
instance = postProcessAfterInitialization(instance, name);
}
}
}
}
// 检查instance是否实现了Lifecycle接口,若实现了,则调用initialize方法
initExtension(instance);
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
type + ") couldn't be instantiated: " + t.getMessage(), t);
}
}
这个方法里,会先获得实现类,然后根据实现类去创建实例
从配置文件获取实现类Map
我们首先来看下,是如何根据名字找到对应的实现类的 getExtensionClasses()
java
private Map<String, Class<?>> getExtensionClasses() {
// 获取所有的class
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
try {
// 加载扩展类
classes = loadExtensionClasses();
} catch (InterruptedException e) {
logger.error(COMMON_ERROR_LOAD_EXTENSION, "", "", "Exception occurred when loading extension class (interface: " + type + ")", e);
throw new IllegalStateException("Exception occurred when loading extension class (interface: " + type + ")", e);
}
cachedClasses.set(classes);
}
}
}
return classes;
}
这个方法会获得一个包含了所有name的对应的Class
这里,提前猜测一下,首先会获取文件中所有实现类的URL资源,然后根据类全限定名加载这些实现类,从而获得所有实现类的Map<name,Class>。
接下来,看下加载扩展类
java
/**
* synchronized in getExtensionClasses
*/
@SuppressWarnings("deprecation")
private Map<String, Class<?>> loadExtensionClasses() throws InterruptedException {
checkDestroyed();
// 默认扩展器缓存(SPI注解里的name)
cacheDefaultExtensionName();
Map<String, Class<?>> extensionClasses = new HashMap<>();
// 加载不同目录下的extensionClass
for (LoadingStrategy strategy : strategies) {
loadDirectory(extensionClasses, strategy, type.getName());
// compatible with old ExtensionFactory
if (this.type == ExtensionInjector.class) {
loadDirectory(extensionClasses, strategy, ExtensionFactory.class.getName());
}
}
return extensionClasses;
}
此方法首先会对default扩展器进行处理,然后加载不同目录下的扩展器实现类。strategies是不同目录的列表,是通过loadLoadingStrategies方法加载获得的。代码如下
java
private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();
private static LoadingStrategy[] loadLoadingStrategies() {
return stream(load(LoadingStrategy.class).spliterator(), false)
.sorted()
.toArray(LoadingStrategy[]::new);
}
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
看代码能知道,这下面用到Java SPI机制了,这里直接放配置文件看下
文件名:org.apache.dubbo.common.extension.LoadingStrategy
dart
org.apache.dubbo.common.extension.DubboInternalLoadingStrategy
org.apache.dubbo.common.extension.DubboLoadingStrategy
org.apache.dubbo.common.extension.ServicesLoadingStrategy
随便找一个实现类看下
java
public class DubboLoadingStrategy implements LoadingStrategy {
@Override
public String directory() {
return "META-INF/dubbo/";
}
其实就是一个文件目录path
我们回去继续看下loadDirectory方法
java
private void loadDirectory(Map<String, Class<?>> extensionClasses, LoadingStrategy strategy, String type) throws InterruptedException {
loadDirectoryInternal(extensionClasses, strategy, type);
try {
// 这里是dubbo中自己定义的SPI接口用到了这里,我们自己定义的不需要管
String oldType = type.replace("org.apache", "com.alibaba");
if (oldType.equals(type)) {
return;
}
//if class not found,skip try to load resources
ClassUtils.forName(oldType);
loadDirectoryInternal(extensionClasses, strategy, oldType);
} catch (ClassNotFoundException classNotFoundException) {
}
}
上述方法去执行加载扩展实现类,后续是为了兼容老版本(先用此版本找一遍,再替换type前缀,去找一遍),替换的type名称,此处为dubbo内部的SPI,我们自己定义的,不用管。
loadDirectoryInternal方法中会根据文件path来利用ClassLoader获取对应URL loadDirectoryInternal方法关键代码如下
java
Set<ClassLoader> classLoaders = scopeModel.getClassLoaders();
if (CollectionUtils.isEmpty(classLoaders)) {
Enumeration<java.net.URL> resources = ClassLoader.getSystemResources(fileName);
if (resources != null) {
while (resources.hasMoreElements()) {
loadResource(extensionClasses, null, resources.nextElement(), loadingStrategy.overridden(),
loadingStrategy.includedPackages(),
loadingStrategy.excludedPackages(),
loadingStrategy.onlyExtensionClassLoaderPackages());
}
}
} else {
classLoadersToLoad.addAll(classLoaders);
}
在ClassLoader通过fileName获取URL列表resources后,执行loadResource方法,如下
java
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
java.net.URL resourceURL, boolean overridden, String[] includedPackages, String[] excludedPackages, String[] onlyExtensionClassLoaderPackages) {
try {
List<String> newContentList = getResourceContent(resourceURL);
String clazz;
for (String line : newContentList) {
try {
String name = null;
int i = line.indexOf('=');
if (i > 0) {
name = line.substring(0, i).trim();
clazz = line.substring(i + 1).trim();
} else {
clazz = line;
}
if (StringUtils.isNotEmpty(clazz) && !isExcluded(clazz, excludedPackages) && isIncluded(clazz, includedPackages)
&& !isExcludedByClassLoader(clazz, classLoader, onlyExtensionClassLoaderPackages)) {
// 将反射获得的Class加载进去
loadClass(extensionClasses, resourceURL, Class.forName(clazz, true, classLoader), name, overridden);
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type +
", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
exceptions.put(line, e);
}
}
} catch (Throwable t) {
logger.error(COMMON_ERROR_LOAD_EXTENSION, "", "", "Exception occurred when loading extension class (interface: " +
type + ", class file: " + resourceURL + ") in " + resourceURL, t);
}
}
这个方法,就是将文件中的内容封装成一个newContentList,之后分割,获取类的全限定名clazz,之后,执行loadClass方法,其中,会传入class.forName获取到的Class。
loadClass代码如下
java
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
boolean overridden) {
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error occurred when loading extension class (interface: " +
type + ", class line: " + clazz.getName() + "), class "
+ clazz.getName() + " is not subtype of interface.");
}
if (clazz.isAnnotationPresent(Adaptive.class)) {
cacheAdaptiveClass(clazz, overridden);
// 判断是不是包装类,aop就是依据此模式实现的,判断条件是:若有构造方法,参数是这个接口的type,则是包装类。可以到这个方法源码中具体看一下。
} else if (isWrapperClass(clazz)) {
cacheWrapperClass(clazz);
} else {
if (StringUtils.isEmpty(name)) {
name = findAnnotationName(clazz);
if (name.length() == 0) {
throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
}
}
String[] names = NAME_SEPARATOR.split(name);
if (ArrayUtils.isNotEmpty(names)) {
cacheActivateClass(clazz, names[0]);
for (String n : names) {
cacheName(clazz, n);
saveInExtensionClass(extensionClasses, clazz, n, overridden);
}
}
}
}
这个方法会对clazz进行一系列校验,其中,isWrapperClass用来判断是否是包装类,
若为包装类,则放到 Set<Class<?>> cachedWrapperClasses本地缓存中,这也是Dubbo中AOP的实现方式。目的是给我们这个接口的所有实现类增加一个切面。
到最后其实就是填充了extensionClasses
根据配置文件进行类加载过程已经结束,下面该进行实例化。
实现类的实例化
看下createExtensionInstance下的instantiate方法
java
public <T> T instantiate(Class<T> type) throws ReflectiveOperationException {
// should not use default constructor directly, maybe also has another constructor matched scope model arguments
// 1. try to get default constructor
Constructor<T> defaultConstructor = null;
try {
defaultConstructor = type.getConstructor();
} catch (NoSuchMethodException e) {
// ignore no default constructor
}
// 2. use matched constructor if found
List<Constructor<?>> matchedConstructors = new ArrayList<>();
Constructor<?>[] declaredConstructors = type.getConstructors();
for (Constructor<?> constructor : declaredConstructors) {
if (isMatched(constructor)) {
matchedConstructors.add(constructor);
}
}
// remove default constructor from matchedConstructors
if (defaultConstructor != null) {
matchedConstructors.remove(defaultConstructor);
}
// match order:
// 1. the only matched constructor with parameters
// 2. default constructor if absent
Constructor<?> targetConstructor;
if (matchedConstructors.size() > 1) {
throw new IllegalArgumentException("Expect only one but found " +
matchedConstructors.size() + " matched constructors for type: " + type.getName() +
", matched constructors: " + matchedConstructors);
} else if (matchedConstructors.size() == 1) {
targetConstructor = matchedConstructors.get(0);
} else if (defaultConstructor != null) {
targetConstructor = defaultConstructor;
} else {
throw new IllegalArgumentException("None matched constructor was found for type: " + type.getName());
}
// create instance with arguments
Class<?>[] parameterTypes = targetConstructor.getParameterTypes();
Object[] args = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
args[i] = getArgumentValueForType(parameterTypes[i]);
}
return (T) targetConstructor.newInstance(args);
}
这里会有一个获取targetConstructor的过程,需要实现类中有无参构造方法,否则会报错。最后通过反射创建一个实例。
回到前面,创建完实例之后,有一个前置处理器(创建完实例之后,初始化之前),之后会进行依赖注入,然后进行AOP切面处理,再然后进行后置处理器(和spring bean的生命周期很像)
AOP切面的实现
我们来看下AOP切面处理的实现,createExtension方法中有下面一行代码
java
// aop,这里得到的instance是Wrapper类的实例
instance = injectExtension(
(T) wrapperClass.getConstructor(type).newInstance(instance));
此代码表示,再进行依赖注入时,传入的是一个实现该接口的Wrapper类的实例(若配置了Wrapper类,则会走到这一步,若没有配置,则不会去执行这一步)
依赖注入的实现
下面看一下依赖注入的过程,Dubbo的依赖注入方式是setter方式
java
private T injectExtension(T instance) {
if (injector == null) {
return instance;
}
try {
for (Method method : instance.getClass().getMethods()) {
// 不是set方法,则跳过
if (!isSetter(method)) {
continue;
}
/**
* Check {@link DisableInject} to see if we need auto-injection for this property
*/
// 若标记了不可注入注解,则跳过
if (method.isAnnotationPresent(DisableInject.class)) {
continue;
}
// When spiXXX implements ScopeModelAware, ExtensionAccessorAware,
// the setXXX of ScopeModelAware and ExtensionAccessorAware does not need to be injected
// 属于ScopeModelAware和ExtensionAccessorAware的set方法,则跳过
if (method.getDeclaringClass() == ScopeModelAware.class) {
continue;
}
if (instance instanceof ScopeModelAware || instance instanceof ExtensionAccessorAware) {
if (ignoredInjectMethodsDesc.contains(ReflectUtils.getDesc(method))) {
continue;
}
}
Class<?> pt = method.getParameterTypes()[0];
// 若set方法的参数是基本类型,则跳过
if (ReflectUtils.isPrimitives(pt)) {
continue;
}
try {
String property = getSetterProperty(method);
Object object = injector.getInstance(pt, property);
if (object != null) {
// 把所需的对象object注入到目标实例instance
method.invoke(instance, object);
}
} catch (Exception e) {
}
}
} catch (Exception e) {
logger.error(COMMON_ERROR_LOAD_EXTENSION, "", "", e.getMessage(), e);
}
return instance;
}
这个方法完成了向instance中依赖注入的工作,具体实现方式就是调用set方法,往instance里set一个依赖实例
在createExtension()方法的最后,回去检验此instance是否实现了Lifecycle接口,若是,则执行初始化方法。
至此,Dubbo SPI底层的实现原理解析就结束了。
总结
Dubbo SPI机制中,配置文件中会保存实现类和Wrapper类相关信息,要按需获取某一个扩展点时,首先,通过接口的type去获取ExtensionLoader,获取到之后,再根据name获取Extension扩展点实例,其底层会去读取配置文件,获取对应的实现类的全限定类名和Wrapper类的全限定类名,再根据反射获取对应的Class和WrapperClass(aop切面类。以拿到name和Class的Map。之后,会通过反射,将Class进行实例化,得到一个个的空对象实例。 再依靠setter方法进行依赖注入,依赖注入之后,会检查是否有Wrapper包装类,若有,则创建包装类的实例,并将instance当做参数传入到包装类中,再对此包装类实例进行依赖注入,完成这个工作,在调用扩展点的方法时,就会自动去调用包装类的方法,实现aop切面的功能。在完成aop处理之后,我们就可以得到一个完整的接口实现类对象(外层可能是个包装类)。