SPI概述
SPI全称Service Provider Interface(服务提供者接口),是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件
SPI使用场景
-
SPI在Java核心库中被广泛使用,如JDBC、JNDI、JCE体系、Java文件系统、JAXP等。SPI允许第三方为这些接口提供实现
-
spring-boot-starter,spring中各种功能的集成,只需要引入boot-starter包,即可引入复杂的插件。同样是利用了SPI机制
-
插件开发,比如idea插件可以在resources/META-INF/plugin.xml中自定义扩展;Eclipse插件遍历plugins文件夹中的目录,扫描每个插件的清单文件MANIFEST.MF等都是基于SPI思想
-
Dubbo中通过SPI加载扩展
......
Java SPI
假设我们有一个加密的接口,需要动态的选择加密算法,那么只需要声明一个加密接口,并提供各自算法的实现,具体的实现并不在程序中确定,而是由程序之外的配置确定
Java SPI 示例
1、定义接口:
public interface EncrypitionService{
void encrypt();
}
2、定义RSA加密的实现:
@Service
public class RsaEncryptServiceImpl implements EncrypitionService {
@Override
public void encrypt() {
LogTools.info("RSA encrypt!");
}
}
3、定义MD5加密的实现:
@Service
public class Md5EncryptServiceImpl implements EncrypitionService {
@Override
public void encrypt() {
LogTools.info("MD5 encrypt!");
}
}
4、新建src\main\resources\META-INF\services目录,在目录下新建以EncrypitionService全限定名命名的文件com.xxx.service.EncrypitionService,内容为EncrypitionService接口的实现类:
com.xxx.service.impl.Md5EncryptServiceImpl
com.xxx.service.impl.RsaEncryptServiceImpl
5、加密算法调用:
public class EncryptMain {
public static void main(String[] args) {
ServiceLoader<EncrypitionService> encrypts = ServiceLoader.load(EncrypitionService.class);
for (EncrypitionService service:encrypts){
service.encrypt();
}
}
}
6、执行EncryptMain
在EncryptMain中,通过构建ServiceLoader实例,遍历ServiceLoader实例来达到了执行META-INF\目录下配置的加密实现的调用。EncrypitionService的具体实现可以由文件com.xxx.service.EncrypitionService中配置的实现类来决定
源码分析
下面分析ServiceLoader的核心代码,ServiceLoader实现了Iterable接口,在程序中遍历返回的ServiceLoader实例时,会使用创建的迭代器,ServiceLoader的实际加载过程就交给了LazyIterator来做。
服务提供者懒加载的迭代器
private class LazyIteratorimplements Iterator<S>{
Class<S> service;
ClassLoader loader;
// 配置文件资源的URL
Enumeration<URL> configs = null;
// 需要加载的实现类的全限定名的集合
Iterator<String> pending = null;
// 下一个需要加载的实现类的全限定名
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//资源名称为META-INF/services + '类的全限定类名'
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 从ClassPath加载资源
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 从资源中解析出需要加载的所有实现类的全限定名
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
// 获取下一个需要加载的实现类的全限定名
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 反射构造Class<S>实例
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
// 判断实现类必须是当前加载的类或者接口的派生类,否则抛出异常终止
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//通过Class#newInstance()进行实例化,并且强制转化为对应的类型的实例
S p = service.cast(c.newInstance());
//添加缓存,Key为实现类的全限定名,Value为实现类的实例
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
ServiceLoader的原理其实不难理解,根据给定的参数(接口)定位到该接口与实现类的映射配置文件的路径,然后读取该配置文件,获取到该接口的子类限定名,再通过反射去构造实例。
Dubbo SPI使用
Dubbo并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。Dubbo通过SPI加载了所有的组件
Dubbo SPI示例
1、定义接口:Dubbo SPI 通过键值对的方式进行配置,可以按需加载指定的实现类,需要在接口上标注 @SPI 注解
@SPI
public interface EncrypitionService{
void encrypt();
}
2、 在META-INF/dubbo下新增com.alibaba.dubbo.rpc.service.EncrypitionService文件
RsaEncryptServiceImpl= com.xxx.service.impl.RsaEncryptServiceImpl
5、通过ExtensionLoader获取RsaEncryptServiceImpl实现
public class main {
public static void main(String[] args) {
EncrypitionService service= ExtensionLoader.getExtensionLoader(EncrypitionService.class).getExtension("RsaEncryptServiceImpl");
service.encrypt();
}
}
原码解析
从上面的例子分析ExtensionLoader的工作过程,ExtensionLoader的入口分为getExtension、getActivateExtension、getAdaptiveExtension,下面对getExtension的实现原理做分析
@SuppressWarnings("unchecked")
public T getExtension(String name) {
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Extension name == null");
if ("true".equals(name)) {
// 自适应扩展类
return getDefaultExtension();
}
// 检查缓存中是否存在所需数据 没有则创建并缓存
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(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;
}
开始创建扩展类对象
- 通过 getExtensionClasses 获取所有的拓展类
- 通过反射创建拓展对象
- 向拓展对象中注入依赖
- 将拓展对象包裹在相应的 Wrapper 对象中
private T createExtension(String name) {
// 尝试从缓存获取Class,没有则调用创建
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, (T) clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// IOC实现 注入依赖
injectExtension(instance);
// 从配置文件中获取的wrapper对象
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && wrapperClasses.size() > 0) {
for (Class<?> wrapperClass : wrapperClasses) {
// AOP实现
// 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。
// 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
type + ") could not be instantiated: " + t.getMessage(), t);
}
}
Dubbo IOC:通过反射调用setter方法设置依赖(下例注释来自于dubbo官网)
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
// 遍历目标类的所有方法
for (Method method : instance.getClass().getMethods()) {
// 检测方法是否以 set 开头,且方法仅有一个参数,且方法访问级别为 public
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
// 获取 setter 方法参数类型
Class<?> pt = method.getParameterTypes()[0];
try {
// 获取属性名,比如 setName 方法对应属性名 name
String property = method.getName().length() > 3 ?
method.getName().substring(3, 4).toLowerCase() +
method.getName().substring(4) : "";
// 从 ObjectFactory 中获取依赖对象
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
// 通过反射调用 setter 方法设置依赖
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("fail to inject via method...");
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
加载扩展类配置信息
private Map<String, Class<?>> getExtensionClasses() {
// 加载扩展类Class
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
实例化扩展类
private Map<String, Class<?>> loadExtensionClasses() {
// 检查是否有SPI注解,如果有则获取value,并作为默认实现名,放入缓存
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
String value = defaultAnnotation.value();
if (value != null && (value = value.trim()).length() > 0) {
String[] names = NAME_SEPARATOR.split(value);
if (names.length > 1) {
throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
+ ": " + Arrays.toString(names));
}
if (names.length == 1) cachedDefaultName = names[0];
}
}
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
// 到指定目录查找并解析配置文件
loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
loadFile(extensionClasses, DUBBO_DIRECTORY);
loadFile(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}
查找扩展类实现,注入扩展类实例
private void loadFile(Map<String, Class<?>> extensionClasses, String dir) {
String fileName = dir + type.getName();
try {
Enumeration<java.net.URL> urls;
ClassLoader classLoader = findClassLoader();
// 获取配置文件
if (classLoader != null) {
urls = classLoader.getResources(fileName);
} else {
urls = ClassLoader.getSystemResources(fileName);
}
// 遍历urls,解析字符串,得到扩展实现类,并缓存
if (urls != null) {
while (urls.hasMoreElements()) {
......
// 自适应
if (clazz.isAnnotationPresent(Adaptive.class)) {
if (cachedAdaptiveClass == null) {
cachedAdaptiveClass = clazz;
} else if (!cachedAdaptiveClass.equals(clazz)) {
throw new IllegalStateException("More than 1 adaptive class found: "
+ cachedAdaptiveClass.getClass().getName()
+ ", " + clazz.getClass().getName());
}
} else {
try {
clazz.getConstructor(type);
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);
} catch (NoSuchMethodException e)
......
} // end of while urls
}
} catch (Throwable t) {
logger.error("Exception when load extension class(interface: " +
type + ", description file: " + fileName + ").", t);
}
}
可以看到getExtension根据SPI配置加载了所有扩展类并缓存起来,并只初始化了传入名称的对应的扩展类实例,并且实现了对扩展类属性进行依赖注入的功能即IOC,同时也提供了利用装饰者模式实现的AOP功能,本文因为篇幅有限对Dubbo中IOC、AOP的实现暂不做过多扩展,后续再更新
总结
SPI的思想在各大框架中经常会被使用,它通过服务发现的机制把装配的控制权移交到程序之外,实现了程序扩展可插拔。对比Java SPI和Dubbo SPI,Java SPI内部的实现使用迭代器,在使用时需要遍历服务提供者使用if判断才能获取指定提供者,而Dubbo SPI可直接获取指定名称的扩展,也可通过注解声明默认扩展;Java SPI不支持依赖注入,如果依赖了其他扩展,做不到自动注入,而Duubo SPI则增加了对IOC、AOP的支持。