目录
一、Java的SPI
二、Dubbo的SPI
dubbo的IOC
dubbo的AOP
动态编译
SPI 全称为 Service Provider Interface,是一种服务发现机制。本质是由将接口的实现类的全限定名配置在文件当中,由服务加载,这样可以在运行时,动态的为接口加载实现类。
举个例子,我们在用JDBC连接数据库时,创建连接就能直接获取到Mysql或者Oracle,Java时如何调用到相对应的驱动呢?
我们将连接数据库看作一个扩展点,其他数据都是该点的实现,当我们需要连接相对应的的数据库时,Spi会帮助我们发现有哪些实现并加载。
一、Java的SPI
按照惯例,先写一个例子,先定义一个接口EatFood,其次创建它的两个实现类,KFC和McDonald,然后META-INF/services下创建一个EatFood的全限定名的文件,里面存放着两个实现类的全限定名,然后我们可以在执行一下代码
public interface EatFood {
void eat();
}
public class KFC implements EatFood{
@Override
public void eat() {
System.out.println("吃肯德基");
}
}
public class McDonald implements EatFood{
@Override
public void eat() {
System.out.println("吃麦当劳");
}
}
com.yxl.KFC
com.yxl.McDonald
测试代码
public class JavaSpiTest {
@Test
public void test(){
ServiceLoader<EatFood> eatFoodServiceLoader = ServiceLoader.load(EatFood.class);
Iterator<EatFood> iterator = eatFoodServiceLoader.iterator();
while(iterator.hasNext()){
EatFood eatFood = iterator.next();
eatFood.eat();
}
}
}
很简单的一段代码,这时我们来看一下源码的流程
点进 ServiceLoader.load(EatFood.class)中的load,可以到是为传进去的这个拓展点创建了一个服务加载器
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
然后我们点进eatFoodServiceLoader.iterator()中的iterator,然后点进knownProviders.hasNext()
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
可以看到这样一段代码,然后点进hasNextService()
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);
}
}
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;
}
上图的方法就是判断是否有相对应的服务,可以看到中间的代码
String fullName = PREFIX + service.getName();
前者PREFIX就是就是我们刚刚创建的目录路径
service.getName()就是获取我们传进来的EatFood的全限定名
下面这段代码则是会解析该文件pending = parse(service, configs.nextElement());
然后我们再回到刚刚的迭代器,可以看到如果返回了True,继续获取对应的服务
然后在这边可以看到通过反射创建该服务的实例
这就是java的Spi的流程,下面我们讲一讲Dubbo的Spi
二、Dubbo的SPI
上面我们讲了Java的SPI,机智的小伙伴们肯定已经发现了缺点,就是没有办法选取特定的实例,只能通过迭代的方式进行操作。Dubbo中并没有使用Java的SPI,而是重新编写了一套逻辑,可以满足我们指定相对应的实例。
先上一个例子
@SPI
public interface Robot {
void sayHello();
}
public class Bumblebee implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Bumblebee.");
}
}
public class DubboSpiTest {
//测试dubbo spi机制
@Test
public void sayHello() throws Exception {
//1、获得接口的ExtentionLoader
ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
//2、根据指定的名字获(key)取对应的实例
Robot robot = extensionLoader.getExtension("optimusPrime");
robot.sayHello();
/*Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();*/
// Robot robot2 = extensionLoader.getDefaultExtension();
// robot2.sayHello();
}
}
bumblebee=com.itheima.spi.dubbo.impl.Bumblebee
optimusPrime=com.itheima.spi.dubbo.impl.OptimusPrime
wrapper=com.itheima.spi.dubbo.impl.RobotWrapper
我们就从getExtensionLoader开始一步步解析
点进去,我们可以看到先是判断类型不能为空,其次是是否为接口,最后是是否拥有拓展注解,其实就是@SPI;接着就是从EXTENSION_LOADERS中根据传入的接口获取相对应的ExtensionLoader,这个EXTENSION_LOADERS其实是一个全局的ConcurrentHashMap,用于存放每个接口的相对应的ExtensionLoader,如果其中没有该接口的拓展加载器,则创建一个。
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
/**
* 校验
* 1、不为空
* 2、是接口
* 3、接口上需要有@SPI注解
*/
if (type == null) {
throw new IllegalArgumentException("Extension type == null");
}
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
}
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type (" + type +
") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
}
/**
* 先从EXTENSION_LOADERS(已加载的ExtensionLoader) 中获取
* 每个接口type都对应一个 ExtensionLoader,该接口下会对应多个扩展点
*/
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type)); // 每个接口type都对应一个 ExtensionLoader
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
// 已加载的 Extension
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();
// 缓存所有的实例Class及对应的实例对象
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
到这里,怎么获取ExtensionLoader就讲完了,我们来看看怎么从加载器中获取实例的
先是进行了一下判空,然后判断了一下是否为true,如果为true的话则是会走@SPI中配置的默认实例;其次就是根据配置文件中的key,去cachedInstances中获取相对应的实例的Holder,可以看到cachedInstances其实就是一个ConcurrentHashMap作为一个缓存存放key相对应的实例的Holder的集合,Holder则是实例的封装。
接下来就是从holder中获取实例,如果为空,则将holder锁住,然后再次判空,根据key创建实例,并且将它放入holder中(这里的代码采用的是单例模式中的DCL,也就是Double Check Lock)
public T getExtension(String name) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
}
if ("true".equals(name)) {
return getDefaultExtension();
}
/**
* name为 META-INF/dubbo下配置文件中配置的key
*
* 1、获取key对应Extension实例的holder (双重锁校验)--->getOrCreateHolder内部通过cachedInstances缓存所有实例key及对应的Holder
* 2、创建key对应的Extension实例并存入holder
* 3、返回对应的Extension实例
*/
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;
}
// 缓存 所有实例key及对应的Holder
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
private Holder<Object> getOrCreateHolder(String name) {
/**
* cachedInstances:缓存所有实例key及对应的Holder
*/
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<>());
holder = cachedInstances.get(name);
}
return holder;
}
/**
* Helper Class for hold a value.
*/
public class Holder<T> {
private volatile T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
我们接着看创建实例的方法createExtension,首先是获取该接口下所有的拓展点的实例的Class,然后根据key从中获取我们需要的Class,我们可以看下获取的过程,从缓存cachedClasses中获取,如果该缓存为空,则进行拓展点Class的加载(执行loadExtensionClasses,此处又是一个DCL)
/**
* 获取扩展Class
* @return
*/
private Map<String, Class<?>> getExtensionClasses() {
// Holder<Map<String, Class<?>>> cachedClasses
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
接着我们来看加载的过程,可以看到dubbo设置了三种路径,一个是内部的加载,一个是对外使用的,最后一个是也提供了Java中SPI的路径支持,我们来看下(代码中的type为我们项目中的接口的全路径名)
private Map<String, Class<?>> loadExtensionClasses() {
cacheDefaultExtensionName();
/**
* loadDirectory方法从指定位置中加载拓展类配置
* "META-INF/dubbo/internal/" DubboInternalLoadingStrategy
* “META-INF/dubbo/”, DubboLoadingStrategy
* "META-INF/services/", ServicesLoadingStrategy
*/
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;
}
点进去我们看一下,fileName就是路径+接口的全路径名,正好构成我们配置文件的路径名,接下来获取了类加载器,然后进行了迭代,看下解析文件的代码
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {
// dir 有三种目录 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);
}
}
这里就是对配置文件内容的一些切割并取值,拿到key以及相对应的实例路径,然后根据实例的路径获取它的Class文件并加载,我们看下加载流程
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;
//按=切割 得到key和value
int i = line.indexOf('=');
if (i > 0) {
name = line.substring(0, i).trim(); // 自定义的key
line = line.substring(i + 1).trim(); // value 即扩展点类的全路径
}
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);
}
}
拿到Class文件会有三种情况:
- 第一:判断它有没有Adaptive注解,如果有并且cachedAdaptiveClass为空,则放到缓存cachedAdaptiveClass中去
- 第二:判断是否是Wrapper类型的Class,是通过查看该拓展点中是否有该接口类型的构造函数,如果有,也是将它放入到缓存cachedWrapperClasses中
- 第三:则是普通的拓展实例
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)) {
// 扩展点Class上有 Adaptive注解
cacheAdaptiveClass(clazz);
} else if (isWrapperClass(clazz)) { // 扩展点类有接口类型的构造函数,表明是Wrapper
// 添加到Set<Class<?>> cachedWrapperClasses 缓存起来
cacheWrapperClass(clazz);
} else { // 证明是普通 extensionClasses
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);
}
}
}
}
private volatile Class<?> cachedAdaptiveClass = null;
private void cacheAdaptiveClass(Class<?> clazz) {
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());
}
}
// 缓存WrapperClasses
private Set<Class<?>> cachedWrapperClasses;
private void cacheWrapperClass(Class<?> clazz) {
if (cachedWrapperClasses == null) {
cachedWrapperClasses = new ConcurrentHashSet<>();
}
cachedWrapperClasses.add(clazz);
}
private boolean isWrapperClass(Class<?> clazz) {
try {
clazz.getConstructor(type);
return true;
} catch (NoSuchMethodException e) {
return false;
}
}
这时候我们在回到这个地方,拿到了字节码文件,从缓存了所有实例的Class以及对应的实例对象的缓存中根据Class获取,如果没有,则给它创建实例并添加到缓存当中
// 缓存所有的实例Class及对应的实例对象
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
dubbo的IOC
到这里,最普通的拓展点实例就讲完了,然后dubbo的SPI还远不止于此,它还可以像IOC一样,将一个拓展点放入另一个拓展点之中,不过需要在实例之中放入另一个拓展点的set方法,就像这样,下图为执行结果,那我们来看下是怎么做到的。
public class OptimusPrime implements Robot {
private Protocol protocol;
public void setProtocol(Protocol protocol) {
this.protocol = protocol;
}
@Override
public void sayHello() {
System.out.println(protocol);
System.out.println("Hello, I am Optimus Prime.");
}
}
在创建完拓展实例之后,dubbo又进行了一个操作,就是将它依赖的拓展点注入到这个实例当中去
可以看到先是获取实例的所有方法,然后先判断是否为set方法,根据开头是否已“set”开始,方法的入参是否只有一个并且是否是public修饰的;接着判断方法上有没有@DisableInject,没有的话,就继续往下;因为只有一个入参,所以直接拿0号位置的类型,接着判断是否入参类型是否为基本类型,因为本身不支持基本类型的注入;然后根据方法拿到属性名,根据类型以及属性名,我们就可以拿到待注入的拓展点,并把它注入到现在实例的方法之上
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
for (Method method : instance.getClass().getMethods()) {
/**
* 通过set方法注入
*/
if (isSetter(method)) {
/**
* Check {@link DisableInject} to see if we need auto injection for this property
*/
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
// set方法只能有一个参数
Class<?> pt = method.getParameterTypes()[0];
// set方法参数的类型如果是基本数据类型则跳过,即不支持基本数据类型的注入
if (ReflectUtils.isPrimitives(pt)) {
continue;
}
try {
// 获取set方法对应的属性名称
String property = getSetterProperty(method);
/**
* pt:属性类型Class
* property:属性名称
*
* 根据类型和名称获取待注入的Extension实例
* ExtensionFactory objectFactory;
* 实现有很多比如:
* SpiExtensionFactory
* SpringExtensionFactory
*/
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("Failed to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
/**
* return true if and only if:
* <p>
* 1, public
* <p>
* 2, name starts with "set"
* <p>
* 3, only has one parameter
*/
private boolean isSetter(Method method) {
return method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers());
}
dubbo的AOP
熟悉Spring的都知道,AOP可以对方法进行切面增强,dubbo的SPI也是可以的,先上案例
bumblebee=com.itheima.spi.dubbo.impl.Bumblebee
optimusPrime=com.itheima.spi.dubbo.impl.OptimusPrime
wrapper=com.itheima.spi.dubbo.impl.RobotWrapper
public class RobotWrapper implements Robot {
private Robot robot;
public RobotWrapper(Robot robot) {
this.robot = robot;
}
@Override
public void sayHello() {
System.out.println("----提前准备----");
robot.sayHello();
System.out.println("----收尾工作----");
}
}
执行结果
可以看到实现了跟AOP一样的效果,我们来看下实现原理,之前在拿到Class文件之后呢,就有对这个Class判断是否为Wrapper的逻辑,记不得的同志可以看下上面的讲解。在这里注入完依赖的实例后,我们从缓存中拿到wrapper类型的Class的set集合,然后遍历WrapperClass的集合。
然后通过WrapperClass的构造函数将上面的实例进行注入,另外它本身可能还会有依赖其他拓展点,所以还需要injectExtension()一下。,如果有多个wrapper,则每次注入的都是之前的整体。
动态编译
dubbo的SPI还有最后一个知识点,就是拓展点有多个实例,它又是如何自己去动态加载我们需要的实例的。我们回到这个地方,点进去
可以看到本身就是一个拓展点,拥有多个实例,可以看到甚至可以从IOC中拿到实例当做拓展点实例。
@SPI
public interface ExtensionFactory {
/**
* Get extension.
*
* @param type object type.
* @param name object name.
* @return object instance.
*/
<T> T getExtension(Class<T> type, String name);
}
我们点进SpiExtensionFactory,可以看到先是对Class进行基本的判断,然后获取该拓展点加载器的自适应拓展实例。
public class SpiExtensionFactory implements ExtensionFactory {
@Override
public <T> T getExtension(Class<T> type, String name) {
if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
if (!loader.getSupportedExtensions().isEmpty()) {
return loader.getAdaptiveExtension();
}
}
return null;
}
}
又是DCL形式的,从缓存中拿,没有就创建,创建之后放入缓存
public T getAdaptiveExtension() {
// Holder<Object> cachedAdaptiveInstance
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
if (createAdaptiveInstanceError == null) {
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
// 创建接口的自适应实例
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
}
}
}
} else {
throw new IllegalStateException("Failed to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
}
}
return (T) instance;
}
我们看下创建自适应实例的过程,其实这里并不是手写代码执行的,而是通过框架javassist生成代码存到下面的String code中
/**
* 创建接口的自适应实例
*/
private T createAdaptiveExtension() {
try {
// getAdaptiveExtensionClass()是核心
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
} catch (Exception e) {
throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
}
}
private Class<?> getAdaptiveExtensionClass() {
getExtensionClasses();
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
/**
* 获取接口自适应实例Class
*/
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
private Class<?> createAdaptiveExtensionClass() {
/**
* 首先会生成自适应类的Java源码,然后再将源码编译成Java的字节码,加载到JVM中
* 使用一个StringBuilder来构建自适应类的Java源码;
* 这种生成字节码的方式也挺有意思的,先生成Java源代码,然后编译,加载到jvm中。
* 通过这种方式,可以更好的控制生成的Java类。而且这样也不用care各个字节码生成框架的api等。
* 因为xxx.java文件是Java通用的,也是我们最熟悉的。只是代码的可读性不强,需要一点一点构建xx.java的内容
*/
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
ClassLoader classLoader = findClassLoader();
/**
* @SPI("javassist")
* public interface Compiler
*/
org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
}
将其中的代码截出来,可以看到如果没有配置,则会去选取“dubbo”实例,有的话则用我们配置的,最后是通过它的接口获取拓展点加载器,再勇气获取拓展点实例,最后用这个实例执行
public org.apache.dubbo.rpc.Invoker
refer(java.lang.Class arg0, org.apache.dubbo.common.URL
arg1) throws org.apache.dubbo.rpc.RpcException {
if (arg1 == null) throw new
IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg1;
String extName = ( url.getProtocol() == null ?
"dubbo" : url.getProtocol() );
if(extName == null) throw new
IllegalStateException("Failed to get extension
(org.apache.dubbo.rpc.Protocol) name from url (" +
url.toString() + ") use keys([protocol])");
org.apache.dubbo.rpc.Protocol extension =
(org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensio
nLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(
extName);
return extension.refer(arg0, arg1);
}
以上,就是dubbo的SPI的全部内容,有错误麻烦指出。