昨天看Dubbo中关于服务注册和管理部分的时候,接触到了SPI的概念,了解下java的扩展机制。
SPI解决了什么问题
在上次看Dubbo中关于zookeeper注册中心的实现的时候,使用了SPI扩展机制,可以根据配置动态选择实现的子类,从而实现。如果服务的扩展。那么如果不使用这种机制的话要怎么实现呢? 为了避免调用者了解实现细节,不能将bean注入的事情交给调用者去做,所以可行的方式是调用者做配置,服务提供者根据配置对类做注入。 那么在根据配置做注入的时候,一种是if-else,这种硬编码的方式不利于做扩展,后边每次新增一个实现类的话,都需要修改代码;另一种是把每个子类的都事先做实例化,这样根据配置可以获取到对应的bean,但是如果一个子类的实例化开销比较大,这种方式就变得可行度不高了。 所以,SPI做了什么呢?SPI是ServiceProviderInterfaces的简称,也就是服务提供接口,通过SPI服务,在需要扩增基类的接口的时候,只需要在jar包的资源文件下增加一个指定子类的文件即可,对原有的模块没有任何其他影响。 首先看一下简单的demo: 创建一个基类和两个实现类
package basicExercise.spi;
/**
* Created by jinyan on 11/19/17 2:49 PM.
*/
public interface Base {
void print();
}
package basicExercise.spi;
/**
* Created by jinyan on 11/19/17 2:49 PM.
*/
public class BaseImpl1 implements Base {
@Override
public void print() {
System.out.println("base1");
}
}
package basicExercise.spi;
/**
* Created by jinyan on 11/19/17 2:50 PM.
*/
public class BaseImpl2 implements Base {
@Override
public void print() {
System.out.println("base2");
}
}
在资源文件的META-INF/services下创建一个名为basicExercise.spi.Base(基类的FullName),文件内容为
basicExercise.spi.BaseImpl1
也就是其中一个子类的FullName 最后创建一个测试类:
public static void main(String[] args) {
ServiceLoader<Base> sl = ServiceLoader.load(Base.class);
Iterator<Base> s = sl.iterator();
if (s.hasNext()) {
Base ss = s.next();
ss.print();
}
}
运行main方法,控制台打印base1
,说明print执行的是BaseImpl1中的方法。试想如果需要增加一个BaseImpl3的print方法,那么只需要修改资源文件中的basicExercise.spi.Base中文件内容即可,系统中无需做代码修改,这种扩展方式在工程项目中是非常借鉴的。在Dubbo中,提供了多种注册中心,如果未来增加了一个注册中心,Dubbo代码中只需要引入新的jar包即可,Dubbo的调用中如果需要使用未来的这个注册中心,也只需要以原有的方式配置register即可,这就体现了高扩展性。
jdk 实现原理
以上对SPI的应用使用的是jdk提供的工具类ServiceLoader,这个类有三个约定好的规则:
- 资源文件要放置在“META-INF/services/”文件夹下
- 资源文件的命名需要按照基类的class全称命名,如示例中的“basicExercise.spi.Base”
- 资源文件中添加的内容为加载的子类的class全名,且不可换行 在调用Service.load(Class class)方法主要做了两件事情:
- 初始化ServiceLoader
- 对遍历器做初始化,在s.hasNext遍历时,加载资源文件:
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
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;
}
其中ClassLoader.getSystemResources(fullName)
会根据拼接好的资源文件名查找资源文件,并通过nextName = pending.next();
获取到资源文件中设定的子类的名称
接下来,示例代码中的next()
方法获取到子类的实例
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
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 {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
S p = service.cast(c.newInstance())
把原有的基类的实例映射成为子类的实例,最终返回的Base ss = s.next()
ss对象就成为了设定好的子类的实例。
看到一些博客中提到JDK和Dubbo扩展机制的对比:
Dubbo改进了JDK标准的SPI的以下问题:
* JDK标准的SPI会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
* 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK标准的ScriptEngine,通过getName();获取脚本类型的名称,但如果RubyScriptEngine因为所依赖的jruby.jar不存在,导致RubyScriptEngine类加载失败,这个失败原因被吃掉了,和ruby对应不起来,当用户执行ruby脚本时,会报不支持ruby,而不是真正失败的原因。
* 增加了对扩展点IoC和AOP的支持,一个扩展点可以直接setter注入其它扩展点。
但是我看源码中只是实例化了配置文件中配置的子类,并没有实例化所有的子类,不太确定这里是否存在谬误。
Dubbo对SPI的扩展
Dubbo中对jdk的扩展机制做了进一步的扩展,整体的实现位于com.alibaba.dubbo.common.extension
包下
做加载工作的类为ExtensionLoader
要使用Dubbo做分布式调用,需要在配置文件配置Provider和Consumer的相关信息,以下仅以Provider注册中心的配置和解析为例,梳理下Dubbo对SPI的实现
<dubbo:application name="demo-provider"/>
<dubbo:registry address="multicast://224.5.6.7:1234"/>
<dubbo:protocol name="dubbo" port="20880"/>
<dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService"/>
<bean id="demoService" class="com.alibaba.dubbo.demo.provider.DemoServiceImpl"/>
在引用了Dubbo服务的项目启动时,类DubboNamespaceHandler
对xml中的配置信息做加载和解析,将各个配置解析到对应的类实体中。
主要看下对dubbo:protocol
元素的解析
else if ("protocol".equals(property)
&& ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(value)
&& (!parserContext.getRegistry().containsBeanDefinition(value)
|| !ProtocolConfig.class.getName().equals(parserContext.getRegistry().getBeanDefinition(value).getBeanClassName()))) {
if ("dubbo:provider".equals(element.getTagName())) {
logger.warn("Recommended replace <dubbo:provider protocol=\"" + value + "\" ... /> to <dubbo:protocol name=\"" + value + "\" ... />");
}
// 兼容旧版本配置
ProtocolConfig protocol = new ProtocolConfig();
protocol.setName(value);
reference = protocol;
}
其中的ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(value)
就是Dubbo中的SPI机制
ExtensionLoader.getExtensionLoader(Protocol.class)
以Protocol.class
初始化一个ExtensionLoader,并以Protocol.class
为KEY,这个ExtensionLoader为VALUE存入ConcurrentMap<Class<?>, ExtensionLoader<?»中,并将这个ExtensionLoader对象返回。ExtensionLoader.hasExtension(value)
做的动作就多了。
public boolean hasExtension(String name)——>
private Class<?> getExtensionClass(String name)——>
private Map<String, Class<?>> getExtensionClasses()——>
private Map<String, Class<?>> loadExtensionClasses()——>
private void loadFile(Map<String, Class<?>> extensionClasses, String dir)
loadFile负责从指定的资源文件中,解析资源文件的配置,加载初始化扩展的子类。
如对zookeeper=com.alibaba.dubbo.registry.zookeeper.ZookeeperRegistryFactory
的配置,在返回的extensionClasses中,key为zookeeper,对应的value为ZookeeperRegistryFactory.class。如此完成注册中心扩展模块的初始化和加载。
再来详细看下最关键的两个接口实现
// 此方法已经getExtensionClasses方法同步过。
private Map<String, Class<?>> loadExtensionClasses() {
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;
}
对标注了@SPI注解的类,且SPI的value不为空的,会将value指定的值做默认值。所以在RegistryFactory的定义中可以看到,这里设定了默认的注册中心
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);
}
if (urls != null) {
while (urls.hasMoreElements()) {
java.net.URL url = urls.nextElement();
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), "utf-8"));
try {
String line = null;
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) {
Class<?> clazz = Class.forName(line, true, classLoader);
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error when load extension class(interface: " +
type + ", class line: " + clazz.getName() + "), class "
+ clazz.getName() + "is not subtype of interface.");
}
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) {
clazz.getConstructor();
if (name == null || name.length() == 0) {
name = findAnnotationName(clazz);
if (name == null || name.length() == 0) {
if (clazz.getSimpleName().length() > type.getSimpleName().length()
&& clazz.getSimpleName().endsWith(type.getSimpleName())) {
name = clazz.getSimpleName().substring(0, clazz.getSimpleName().length() - type.getSimpleName().length()).toLowerCase();
} else {
throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + url);
}
}
}
String[] names = NAME_SEPARATOR.split(name);
if (names != null && names.length > 0) {
Activate activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
cachedActivates.put(names[0], activate);
}
for (String n : names) {
if (!cachedNames.containsKey(clazz)) {
cachedNames.put(clazz, n);
}
Class<?> c = extensionClasses.get(n);
if (c == null) {
extensionClasses.put(n, clazz);
} else if (c != clazz) {
throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
}
}
}
}
}
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + type + ", class line: " + line + ") in " + url + ", cause: " + t.getMessage(), t);
exceptions.put(line, e);
}
}
} // end of while read lines
} finally {
reader.close();
}
} catch (Throwable t) {
logger.error("Exception when load extension class(interface: " +
type + ", class file: " + url + ") in " + url, t);
}
} // end of while urls
}
} catch (Throwable t) {
logger.error("Exception when load extension class(interface: " +
type + ", description file: " + fileName + ").", t);
}
}
判断类实现上是否标注Adaptive注解,如果标注了注解,将这个类作为适配类暂存,否则通过字节码生成适配类。生成适配类的过程通过public T getAdaptiveExtension()
实现。
类ExtensionFactory也标注了SPI注解,是一个扩展点,它的扩展类包括了以下三个:
其中的AdaptiveExtensionFactory
就标注了Adaptive注解。
对没有标注了Adaptive注解的实现类,对其做实例化的时候,需要对它生成一个新的字节码,并使用这个新生成的字节码做实例化。在生成字节码的时候,做的工作是:对于标注了Adaptive注解的方法,会寻找对应的扩展类并作为参数传入set方法。
在代码中有一个代码实现方式很特别,每次获取一个变量的时候,首先判断是否为空,如果为空,则做相关的初始化动作。这种实现方式的好处是节省了内存,避免存储过多信息;另一个是懒加载,避免了一开始做太多初始化工作。另外看代码的时候会觉得很乱,如loadFile方法在多处被调用,看起来很乱,但在运行的时候,这种处理方式可以保证初始化工作只做一次。可能是实现流程太复杂了,已经理不清楚调用顺序了,所以索性采用了这种保险的方式吧,这种处理还是可以借鉴下的。 JDK的这种扩展方式很巧妙,通过把配置信息放入到配置文件中,避免了代码的硬编码,动态实现加载对原有代码没有侵入。以后设计系统可以多多采用这种扩展方式。