SPI全称为Service Provider Interface,是一种服务发现机制。SPI的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以运行时,动态为接口替换实现类。正因此特性,我们可以容易的通过SPI机制为我们的程序拓展功能。SPI机制在第三方框架中也有所应有,比如Dubbo就是通过SPI机制加载所有的组件。不过,Dubbo并未使用JAVA原生的SPI机制,而是对其进行了增强,使其能更好的满足需求。
SPI示例
前面简单介绍了SPI机制原理,本节通过一个示例演示JAVA SPI的使用方法
`
public interface Strategy {
public void run();
}
` 接下来定义俩个实现类
`
public class People implements Strategy {
@Override
public void run() {
System.out.println("people run");
}
}
public class Animal implements Strategy {
@Override
public void run() {
System.out.println("animal run");
}
}
`
接下来 META-INF/services 文件夹下创建一个文件,名称为 Robot 的全限定名 org.apache.spi.Robot。文件内容为实现类的全限定的类名,如下
com.example.demo.alitest.People
com.example.demo.alitest.Animal
`
@Test
public void testRun(){
ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Strategy.class);
System.out.println("Java SPI");
serviceLoader.forEach(Strategy::run);
}
`
Dubbo的SPI实现
Dubbo SPI 除了支持按需加载实现类,还增加了IOC和AOP等特性,这些特性将会在接下来的源码进行分析。Dubbo所需的配置文件需放置在META-INF/dubbo路径下,配置内容如下
people = com.example.demo.alitest.People
animal = com.example.demo.alitest.Animal
Dubbo的SPI除了支持按需加载加载接口实现类,还增加了IOC和AOP特性
DubboSPI的源码分析
dubbo首先通过ExtensionLoader的getExtensionLoader方法获取一个ExtensionLoader实例,然后在通过ExtensionLoader#getExtension方法获取拓展类。
`
public T getExtension(String name) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
}
if ("true".equals(name)) {
return getDefaultExtension();
}
//持有目标对象的实例
final Holder<Object> holder = getOrCreateHolder(name);
Object instance = holder.get();
//双重检查
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
`
`
private T createExtension(String name) {
// 从配置文件中加载所有的拓展类,可得到“配置项名称”到“配置类”的映射关系表
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
// 通过反射创建实例
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// 向实例中注入依赖
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
// 循环创建 Wrapper 实例
for (Class<?> wrapperClass : wrapperClasses) {
// 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。
// 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量
instance = injectExtension(
(T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("...");
}
} `
以上实现步骤
①、通过getExtensionClasses获取所有的类的Clazz对象
②、通过反射创建拓展对象
③、向拓展对象注入依赖
④、将拓展对象包裹在相应的 Wrapper 对象中
获取所有拓展类
我们在通过名称获取拓展类之前,首先需要根据配置文件解析出拓展项名称到拓展类的映射关系表(Map<名称, 拓展类>),之后再根据拓展项名称从映射关系表中取出相应的拓展类即可。相关过程的代码分析如下:
`
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;
}
`
这里先去缓存中去获取,然后在进行非空判断,为空的情况下,通过synchronized的关键字来进行锁住,在次去缓存中获取,依旧为null,则通过loadExtensionClasses加载拓展类。下面对loadExtensionClasses方法进行分析。
`
private Map<String, Class<?>> loadExtensionClasses() {
cacheDefaultExtensionName();
Map<String, Class<?>> extensionClasses = new HashMap<>();
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
return extensionClasses;
}
` loadExtensionClasses总共做了俩件事情,①、对SPI注解进行加载。②、调用loadDirectory方法加载指定文件夹配置文件,接下来我们来看看loadDirectory方法做了哪些事情。
`
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {
String fileName = dir + type;
try {
Enumeration<java.net.URL> urls;
ClassLoader classLoader = findClassLoader();
if (classLoader != null) {
urls = classLoader.getResources(fileName);
} else {
urls = ClassLoader.getSystemResources(fileName);
}
if (urls != null) {
while (urls.hasMoreElements()) {
java.net.URL resourceURL = urls.nextElement();
loadResource(extensionClasses, classLoader, resourceURL);
}
}
} catch (Throwable t) {
logger.error("Exception occurred when loading extension class (interface: " +
type + ", description file: " + fileName + ").", t);
}
}
`
loadDirectory 方法通过classloader获取所有资源链接,然后通过loadResource方法加载资源。
`
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
try {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
final int ci = line.indexOf('#');
if (ci >= 0) {
line = line.substring(0, ci);
}
line = line.trim();
if (line.length() > 0) {
try {
String name = null;
int i = line.indexOf('=');
if (i > 0) {
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
}
} 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("Exception occurred when loading extension class (interface: " +
type + ", class file: " + resourceURL + ") in " + resourceURL, t);
}
}
loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于主要用于操作缓存,该方法的逻辑如下:
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
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);
} else if (isWrapperClass(clazz)) {
cacheWrapperClass(clazz);
} else {
clazz.getConstructor();
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);
}
}
}
}
`
loadClass底层是采用反射的原理来进行加载类。
本篇文章是参考dubbo源码分析文档。
dubbo的源码文档地址:dubbo.apache.org/zh-cn/docs/…