什么是SPI
SPI全称Service Provider Interface,是Java提供的一种接口扩展机制。通过该机制可以将接口的定义与接口的实现分离,实现代码解耦。
使用方式
SPI的使用方法很简单,只需要按如下步骤即可:
-
定义接口
package cn.bdqfork.spi; /** * @author bdq * @since 2020/3/2 */ public interface UserService { void sayHello(); }
-
实现接口
package cn.bdqfork.spi; /** * @author bdq * @since 2020/3/2 */ public class UserServiceImpl implements UserService { @Override public void sayHello() { System.out.println("hello"); } }
-
编写扩展文件
在META-INF/services目录下创建一个以接口全类名命名的文件,即com.test.service.UserService文件。
- src -main -resources - META-INF - services - cn.bdqfork.spi.UserService
文件的内容为实现类的全类名。
cn.bdqfork.spi.UserServiceImpl
-
使用ServiceLoader加载服务
package cn.bdqfork.spi; import java.util.ServiceLoader; /** * @author bdq * @since 2020/3/2 */ public class Main { public static void main(String[] args) { ServiceLoader<UserService> userServices = ServiceLoader.load(UserService.class); for (UserService userService : userServices) { userService.sayHello(); } } }
SPI的缺点
虽然SPI机制可以很方便的将接口与其实现分离,但是却有两个缺点:
- 在ServiceLoader加载的时候,会一次性将所有的Service都加载到JVM中,包括并不会用到的一些扩展。
- 多线程并发访问的时候会有线程问题。
SPI的实现原理
SPI机制的原理实际上不是很难,整个加载扩展服务的过程如下:
- 扫描META-INF/services目录下的文件。
- 读取文件内容,加载服务实现的Class。
- 实例化服务实现。
- 返回服务实例。
实现一个简单SPI
了解了SPI的实现原理之后,便可以很简单的实现一个SPI,下面笔者介绍一下笔者实现的一个SPI,参考了Dubbo的SPI实现,但整体原理还是差不多的。
首先定义一个ExtensionLoader类以及相关属性:
/**
* SPI扩展
*
* @author bdq
* @since 2019-08-20
*/
public class ExtensionLoader<T> {
/**
* 扫描路径
*/
private static final String PREFIX = "META-INF/extensions/";
/**
* 缓存
*/
private static final Map<String, ExtensionLoader<?>> CACHES = new ConcurrentHashMap<>();
/**
* 扩展Class名称缓存
*/
private final Map<Class<T>, String> classNames = new ConcurrentHashMap<>();
/**
* 扩展Class缓存
*/
private final Map<String, Class<T>> extensionClasses = new ConcurrentHashMap<>();
/**
* 扩展实例缓存
*/
private volatile Map<String, T> cacheExtensions;
/**
* 默认扩展名
*/
private String defaultName;
/**
* 扩展服务类型
*/
private Class<T> type;
private ExtensionLoader(Class<T> type) {
this.type = type;
}
// ......
}
然后实现加载扩展的方法,主要实现了扩展实现类的加载,具体代码如下:
private void loadExtensionClasses() {
if (classNames.size() > 0) {
return;
}
try {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// 加载扩展文件
Enumeration<URL> urlEnumeration = classLoader.getResources(PREFIX + type.getName());
while (urlEnumeration.hasMoreElements()) {
URL url = urlEnumeration.nextElement();
if (url.getPath().isEmpty()) {
throw new IllegalArgumentException("Extension path " + PREFIX + type.getName() + " don't exsist !");
}
// 读取文件内容
if (url.getProtocol().equals("file") || url.getProtocol().equals("jar")) {
URLConnection urlConnection = url.openConnection();
Reader reader = new InputStreamReader(urlConnection.getInputStream());
BufferedReader bufferedReader = new BufferedReader(reader);
// 逐行读取
String line;
while ((line = bufferedReader.readLine()) != null) {
if (line.equals("")) {
continue;
}
// 过滤注释
if (line.contains("#")) {
line = line.substring(0, line.indexOf("#"));
}
// 解析key=value
String[] values = line.split("=");
String name = values[0].trim();
String impl = values[1].trim();
if (extensionClasses.containsKey(name)) {
throw new IllegalStateException("Duplicate extension named " + name);
}
// 加载Class
@SuppressWarnings("unchecked")
Class<T> clazz = (Class<T>) classLoader.loadClass(impl);
// 缓存Class
classNames.putIfAbsent(clazz, name);
extensionClasses.putIfAbsent(name, clazz);
}
}
}
} catch (Exception e) {
throw new IllegalArgumentException("Fail to get extension class from " + PREFIX + type.getName() + "!", e);
}
}
实例化所有的扩展,并缓存到Map中。
/**
* 获取所有扩展
*
* @return Map<String, T>
*/
public Map<String, T> getExtensions() {
if (cacheExtensions == null) {
cacheExtensions = new ConcurrentHashMap<>();
loadExtensionClasses();
for (Map.Entry<String, Class<T>> entry : extensionClasses.entrySet()) {
Class<T> clazz = entry.getValue();
T instance;
try {
instance = clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
cacheExtensions.putIfAbsent(entry.getKey(), instance);
}
}
return Collections.unmodifiableMap(cacheExtensions);
}
提供按名获取扩展和获取默认扩展的方法。
/**
* 根据extensionName获取扩展实例
*
* @param extensionName 扩展名称
* @return T
*/
public T getExtension(String extensionName) {
T extension = getExtensions().get(extensionName);
if (extension != null) {
return extension;
}
throw new IllegalStateException("No extension named " + extensionName + " for class " + type.getName() + "!");
}
/**
* 根据extensionName获取扩展实例
*
* @return T
*/
public T getDefaultExtension() {
T extension = getExtensions().get(defaultName);
if (extension != null) {
return extension;
}
throw new IllegalStateException("No default extension named " + defaultName + " for class " + type.getName() + "!");
}
最后提供一个工厂方法,创建ExtensionLoader实例。
/**
* 获取扩展接口对应的ExtensionLoader
*
* @param clazz 扩展接口
* @param <T> Class类型
* @return ExtensionLoader<T>
*/
@SuppressWarnings("unchecked")
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> clazz) {
String className = clazz.getName();
if (!clazz.isInterface()) {
throw new IllegalArgumentException("Fail to create ExtensionLoader for class " + className
+ ", class is not Interface !");
}
SPI spi = clazz.getAnnotation(SPI.class);
if (spi == null) {
throw new IllegalArgumentException("Fail to create ExtensionLoader for class " + className
+ ", class is not annotated by @SPI !");
}
ExtensionLoader<T> extensionLoader = (ExtensionLoader<T>) CACHES.get(className);
if (extensionLoader == null) {
CACHES.putIfAbsent(className, new ExtensionLoader<>(clazz));
extensionLoader = (ExtensionLoader<T>) CACHES.get(className);
extensionLoader.defaultName = spi.value();
}
return extensionLoader;
}
SPI注解的定义如下:
/**
* 注解在扩展类上,表示可以扩展
*
* @author bdq
* @since 2019/9/21
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SPI {
/**
* 默认扩展名
*/
String value() default "";
}
以上是一个简单SPI的实现代码,使用方法也很简单,跟JDK的使用方法差不多,区别是需要使用@SPI注解在服务接口上表明扩展需求,且配置文件存储在META-INF/extensions文件夹下,具体使用方法如下。
-
首先定义接口。
package com.github.bdqfork.core.extension; /** * @author bdq * @since 2020/2/22 */ @SPI public interface IExtensionTest { }
-
实现接口
package com.github.bdqfork.core.extension; /** * @author bdq * @since 2020/2/22 */ public class ExtensionTestImpl1 implements IExtensionTest { } package com.github.bdqfork.core.extension; /** * @author bdq * @since 2020/2/22 */ public class ExtensionTestImpl2 implements IExtensionTest { }
-
编写配置文件
在META-INF/services目录下创建一个以接口全类名命名的文件,即com.github.bdqfork.core.extension.ExtensionLoaderTest文件。
- src -main -resources - META-INF - extensions - com.github.bdqfork.core.extension.IExtensionTest
文件内容如下:
imp1=com.github.bdqfork.core.extension.ExtensionTestImpl1 imp2=com.github.bdqfork.core.extension.ExtensionTestImpl2
这里为每一个扩展实例提供了名称。
-
通过ExtensionLoader获取扩展实例
package com.github.bdqfork.core.extension; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class ExtensionLoaderTest { @Test public void getExtension() { ExtensionLoader<IExtensionTest> extensionLoader = ExtensionLoader.getExtensionLoader(IExtensionTest.class); IExtensionTest iExtensionTest = extensionLoader.getExtension("imp1"); assert iExtensionTest != null; } }
以上就是一个简单SPI的实现,在笔者的ioc容器festival和rpc框架hamal中均使用到了SPI,提供扩展服务,感兴趣的同学欢迎查看笔者的项目。