前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >dubbo的SPI 机制与运用实现

dubbo的SPI 机制与运用实现

作者头像
政采云前端团队
发布于 2023-12-11 10:58:16
发布于 2023-12-11 10:58:16
35800
代码可运行
举报
文章被收录于专栏:采云轩采云轩
运行总次数:0
代码可运行

前言

SPI(Service Provider Interface),是 JDK 内置的一种服务发现机制,可以用来启用框架扩展和替换组件,主要被开发人员使用。比如 java.sql.Driver 接口,不同厂商可以针对同一接口提供不同实现,MySQLPostgreSQL 都有各自不同实现提供给用户。Java SPI 机制可以为某个接口寻找服务实现。Java 中 SPI 机制主要思想是将装配控制权移到程序之外,模块化设计中这个机制尤其重要,其核心思想就是 解耦

其流程可以参考下图

Dubbo 并未使用 Java 原生 SPI 机制,而是对其进行增强,能够更好满足需求。

在学习 Dubbo SPI 之前,需要先了解 Java SPI 运行原理。

Java SPI

SPI 简单示例

  1. 这是一个搜索接口(行为)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface Search {
  List<String> searchData(String query);
}
  • 文件搜索实现
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class FileSearch implements Search {
  @Override
  public List<String> searchData(String query) {
    return Lists.newArrayList("这里是文件搜索");
  }
}
  • DB 搜索实现
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class DbSearch implements Search {
  @Override
  public List<String> searchData(String query) {
    return Lists.newArrayList("这里 db 搜索");
  }
}
  • ES 搜索实现
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ElasticSearchSearch implements Search {
  @Override
  public List<String> searchData(String query) {
    return Lists.newArrayList("这里是 es 搜索");
  }
}
  1. 在resources下新建 META-INF/services/ 目录,新建接口全限定名的文件: cn.zcy.spi.Search,文件内加上需要用到的实现类
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
cn.zcy.spi.DbSearch
cn.zcy.spi.FileSearch
cn.zcy.spi.ElasticSearchSearch
  1. 测试
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class start {

  public static void main(String[] args) {
    ServiceLoader<Search> s = ServiceLoader.load(Search.class);
    Iterator<Search> iterator = s.iterator();
    while (iterator.hasNext()) {
      Search next = iterator.next();
        System.out.println(next.searchData());
    }
  }
}

结果:Main 方法会将文件内部定义的所有实现类实例化,并调用其 SearchData 方法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[这里是 db 搜索]
[这里是文件搜索]
[这里是 es 搜索]

这就是 SPI 思想,接口实现由 Provider 提供,Provider 只用在提供的 Jar 包 META-INF/services 下根据平台定义的接口新建文件,并添加进相应的实现类内容就好。

SPI 实现

通过上述例子,可以简单猜想一下 ServiceLoader.Load 的实现方式:

  1. META-INF/services 目录下创建的文件名,是需要 Load 方法加载的接口的全限定名, 即系统可以通过 Load 方法入参, 找到定义的文件。
  2. 通过读取文件内容找到对应实现类的全限定名。
  3. 知道全限定名后, 很容易通过反射的方式来拿到对应的实现, 然后执行对应的方法。

根据以上猜想看 ServiceLoader.Load 具体实现

省略部分代码, 只关注关键处理逻辑

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//ServiceLoader实现了Iterable接口,可以遍历所有的服务实现者
public final class ServiceLoader<S> implements Iterable<S>{
  //查找配置文件的目录
  private static final String PREFIX = "META-INF/services/";

  //解析服务提供者配置文件中的一行
  //首先去掉注释校验,然后保存
  //返回下一行行号
  //重复的配置项和已经被实例化的配置项不会被保存
  private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names) throws IOException, ServiceConfigurationError {
        //读取一行
    String ln = r.readLine();
    if (ln == null) {
        return -1;
    }
    //#号代表注释行
    int ci = ln.indexOf('#');
    if (ci >= 0) ln = ln.substring(0, ci);
    ln = ln.trim();
    int n = ln.length();
    if (n != 0) {
        if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
           fail(service, u, lc, "Illegal configuration-file syntax");
        int cp = ln.codePointAt(0);
        if (!Character.isJavaIdentifierStart(cp))
           fail(service, u, lc, "Illegal provider-class name: " + ln);
        for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
            cp = ln.codePointAt(i);
            if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
              fail(service, u, lc, "Illegal provider-class name: " + ln);
        }
        if (!providers.containsKey(ln) && !names.contains(ln))
          names.add(ln);
    }
    return lc + 1;
}

//解析配置文件,解析指定的url配置文件
//使用parseLine方法进行解析,未被实例化的服务提供者会被保存到缓存中去
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
  InputStream in = null;
  BufferedReader r = null;
  ArrayList<String> names = new ArrayList<>();
  try {
    in = u.openStream();
    r = new BufferedReader(new InputStreamReader(in, "utf-8"));
    int lc = 1;
    while ((lc = parseLine(service, u, r, lc, names)) >= 0);
  }
  return names.iterator();
}

//服务提供者查找的迭代器
private class LazyIterator implements Iterator<S>{

  Class<S> service;//服务提供者接口
  ClassLoader loader;//类加载器
  Enumeration<URL> configs = null;//保存实现类的url
  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 {
        String fullName = PREFIX + service.getName();
        if (loader == null)
          configs = ClassLoader.getSystemResources(fullName);
        else
          configs = loader.getResources(fullName);
        }
      }
      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 {
        c = Class.forName(cn, false, loader);
      }
      if (!service.isAssignableFrom(c)) {
        fail(service, "Provider " + cn  + " not a subtype");
      }
      try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
      }
   }

  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();
  }
}

//获取迭代器
//返回遍历服务提供者的迭代器
//以懒加载的方式加载可用的服务提供者
//懒加载的实现是:解析配置文件和实例化服务提供者的工作由迭代器本身完成
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();
   }
  };
}

  //使用扩展类加载器为指定的服务创建ServiceLoader
  //只能找到并加载已经安装到当前Java虚拟机中的服务提供者,应用程序类路径中的服务提供者将被忽略
  public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
    ClassLoader cl = ClassLoader.getSystemClassLoader();
    ClassLoader prev = null;
    while (cl != null) {
      prev = cl;
      cl = cl.getParent();
    }
    return ServiceLoader.load(service, prev);
  }
}
  1. 首先ServiceLoader 实现 Iterable 接口,所以具有迭代器属性,主要实现了迭代器的 HasNextNext 方法,调用 LookupIteratorHasNextNext 方法,LookupIterator 是懒加载迭代器。
  2. 其次LazyIteratorHasNext 方法,静态变量 Prefix 就是 META-INF/services/ 目录,这就是为什么需要在 ClasspathMETA-INF/services/目录创建以服务接口命名的文件。
  3. 最后 ,通过反射方法 Class.ForName()加载类对象,并用 NewInstance 方法将类实例化,并把实例化后的类缓存到 Providers对象中,(LinkedHashMap<String,S>类型)然后返回实例对象。

所以可以看到 ServiceLoader 不是实例化后就读取配置文件中具体实现并进行实例化,而是等到使用迭代器去遍历的时候,才会加载对应配置文件去解析,调用 HasNext 方法的时候会去加载配置文件进行解析,调用 Next 方法的时候进行实例化并缓存。

所有配置文件只会加载一次,服务提供者也只会被实例化一次,重新加载配置文件可使用 Reload 方法。

SPI 机制缺陷

通过阅读 ServiceLoader 源码,可以反向推断出基础 SPI 在使用上存在部分缺陷:

  1. 无法按需来获取。遍历所有实现,并实例化,然后在循环中才能找到需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,也会被载入并实例化了,造成了浪费。
  2. 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
  3. 多个并发线程使用 ServiceLoader 类的实例不安全。

Dubbo SPI 机制

Dubbo SPI 并非原创一种新的加载机制,它是在 Java 原生 SPI 基础上加以改进,解决原生 SPI 一些缺陷。

Dubbo SPI 机制简单示例

  1. 同样 Search 接口
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@SPI
public interface Search {
  List<String> searchData();
}
  • 文件搜索实现
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class FileSearch implements Search {
  @Override
  public List<String> searchData() {
    return Lists.newArrayList("这里是文件搜索");
  }
}
  • DB 搜索实现
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class DbSearch implements Search {
  @Override
  public List<String> searchData() {
    return Lists.newArrayList("这里是 db 搜索");
  }
}
  • ES 搜索实现
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ElasticSearchSearch implements Search {
  @Override
  public List<String> searchData() {
    return Lists.newArrayList("这里是 es 搜索");
  }
}
  1. 接下来同样是在 META-INF/services/ 目录下,新建接口全限定名的文件: cn.zcy.spi.Search,里面加上我们需要用到的实现类
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
db=cn.lzy.playwright.spi.dubbo.DbSearch
file=cn.lzy.playwright.spi.dubbo.FileSearch
es=cn.lzy.playwright.spi.dubbo.ElasticSearchSearch
  1. 测试
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Slf4j
public class start {

  public static void main(String[] args) {
    ExtensionLoader<Search> extensionLoader = ExtensionLoader.getExtensionLoader(Search.class);
    Search dbSearch = extensionLoader.getExtension("db");
    log.info("dbSearch.method : {}", dbSearch.searchData());
    Search fileSearch = extensionLoader.getExtension("file");
    log.info("fileSearch.method : {}", fileSearch.searchData());
    Search esSearch = extensionLoader.getExtension("es");
    log.info("esSearch.method : {}", esSearch.searchData());
  }
}

结果

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
dbSearch.method : [这里是 db 搜索]
fileSearch.method : [这里是文件搜索]
esSearch.method : [这里是 es 搜索]

分析一下,Dubbo SPI 和 Java 原生 SPI 实现上,有三个明显不同:

  • Interface 上加入了 Dubbo 内部 @SPI 。
  • 全限定名文件的内容,不再仅仅只是对应实现的全限定名, 而是类似 K-V 格式一种编写方式。
  • Main 方法内部,不是通过原生 ServiceLoader 加载,而是通过 Dubbo 自定义的 ExtensionLoader 加载。

通过示例很明显发现:Dubbo SPI 机制,通过改变全限定名文件内容格式,解决了原生 SPI 在使用上无法按需实例化这个缺陷。

那么 Dubbo SPI 如何实现,接下来看一下对应源码。

Dubbo SPI 实现

从入口 ExtensionLoader 来查看 GetExtensinLoader 方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
  if (type == null) { // 规避了入参 NPE 问题
    throw new IllegalArgumentException("Extension type == null");
  }
  if (!type.isInterface()) { // 规避入参非接口的问题
    throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
  }
  if (!withExtensionAnnotation(type)) { // 方法内部判读接口是否存在 @SPI 注解
    throw new IllegalArgumentException("Extension type(" + type +
        ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
  }

  ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    if (loader == null) { // 懒加载的实现逻辑
      EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
      loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    }
    return loader;
  }

方法内容很简单,除去一些判断外, 真正有价值的逻辑处理只有一个懒加载的实现,而关于代码上中 EXTENSION_LOADERS 其实是一个 ConcurrentHashMap

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();

这在一定程度上解决了重复读取和并发问题。关键读取处理逻辑是在 new ExtensionLoader<T>(type) 这个构造方法内部,这个构造方法十分简单,是一个三元表达式

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private ExtensionLoader(Class<?> type) {
  this.type = type;
  objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}

所以要关注的就是 GetAdaptiveExtension 这个方法实现

如下是具体实现逻辑

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public T getAdaptiveExtension() {
  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("fail to create adaptive instance: " + t.toString(), t);
        }
      }
    }
  } else {
    throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
    }
  }
  return (T) instance;
}

CreateAdaptiveExtension 关键是调用 GetAdaptiveExtensionClass 方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private T createAdaptiveExtension() {
  try {
    return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    } catch (Exception e) {
    throw new IllegalStateException("Can not create adaptive extension " + type + ", cause: " + e.getMessage(), e);
    }
 }

继续看内部逻辑, 查到 GetExtensionClasses 方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private Class<?> getAdaptiveExtensionClass() {
  getExtensionClasses();
  if (cachedAdaptiveClass != null) {
    return cachedAdaptiveClass;
  }
  return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

最后看到读取 Interface 对应实现类的方法是 LoadExtensionClasses , 具体实现逻辑如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 in getExtensionClasses
private Map<String, Class<?>> loadExtensionClasses() {
   final SPI defaultAnnotation = type.getAnnotation(SPI.class);
   if (defaultAnnotation != null) {
     String value = defaultAnnotation.value();
     if ((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<?>>();
  // 指定地址读取文件内容, 兼容原生的 SPI 地址和默认的 duboo 文件地址
  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;
}

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 when load extension class(interface: " +type + ", description file: " + fileName + ").", t);
  }
}

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
  try {
    BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8"));
    try {
      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;
          // 根据 = 分割内容, 拆解成 K-V 格式数据
          int i = line.indexOf('=');
          if (i > 0) {
            name = line.substring(0, i).trim();
            line = line.substring(i + 1).trim();
          }
          if (line.length() > 0) {
            // 加载 Class
            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);
        }
      }
    }
  } finally {
    reader.close();
  }
  } catch (Throwable t) {
    logger.error("Exception when load extension class(interface: " +type + ", class file: " + resourceURL + ") in " + resourceURL, t);
  }
}

由上述代码逻辑可以了解,Dubbo SPI 加载 Class 信息过程主要分成以下几步:

  1. 首先:通过名称获取拓展类之前,需要根据配置文件解析出拓展项名称到拓展类的映射关系表(Map<名称, 拓展类>),之后再根据拓展项名称从映射关系表中取出相应的拓展类
  2. 其次GetExtensionClasses 内部先检查缓存,若缓存未命中,则通过 synchronized 加锁。加锁后再次检查缓存,并判空,此时如果 Classes 仍为 Null,则通过 LoadExtensionClasses 加载拓展类
  3. 然后LoadExtensionClasses 中的逻辑比较简单, 总共就两件事情:一是对 SPI 注解进行解析,二是调用 LoadDirectory 方法加载指定文件夹配置文件
  4. 接着 LoadDirectory 方法先通过ClassLoader 获取所有资源链接,然后再通过 LoadResource 方法加载资源
  5. 最后 LoadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 LoadClass 方法进行其他操作。LoadClass 方法用于主要用于操作缓存

加载完 Interface 对应实现 Class 之后就需要对其进行实例化,看一下源码实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public T getExtension(String name) {
  if (name == null || name.length() == 0) {
    throw new IllegalArgumentException("Extension name == null");
  }
  if ("true".equals(name)) {
     return getDefaultExtension();
  }
  // 缓存 Holder
  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;
 }

该方法内部是一个普通缓存读取内容,创建内容在方法 CreateExtension 内部

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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("Extension instance(name: " + name + ", class: " +
                    type + ")  could not be instantiated: " + t.getMessage(), t);
   }
 }

CreateExtension 方法逻辑稍复杂一下,包含了如下步骤:

  • 通过 GetExtensionClasses 获取所有的拓展类
  • 通过反射创建拓展对象
  • 向拓展对象中注入依赖
  • 将拓展对象包裹在相应的 Wrapper 对象中

以上步骤中,第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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())) {
          /**
          * Check {@link DisableInject} to see if we need auto injection for this property
          */
          if (method.getAnnotation(DisableInject.class) != null) {
            continue;
          }
          // 获取 setter 方法参数类型
          Class<?> pt = method.getParameterTypes()[0];
          if (ReflectUtils.isPrimitives(pt)) {
            continue;
          }
          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 " + method.getName()
                                    + " of interface " + type.getName() + ": " + e.getMessage(), e);
         }
       }
     }
   }
 } catch (Exception e) {
   logger.error(e.getMessage(), e);
 }
 return instance;
}

在上面代码中,ObjectFactory 变量类型为 AdaptiveExtensionFactoryAdaptiveExtensionFactory 内部维护了一个 ExtensionFactory 列表,用于存储其他类型的 ExtensionFactory。Dubbo 目前提供两种 ExtensionFactory,分别是 SpiExtensionFactorySpringExtensionFactory。前者用于创建自适应的拓展,后者用于从 Spring IOC 容器中获取所需拓展。这两个类代码不是很复杂,这里就不一一分析了。

可以看出,Dubbo SPI 在原生 SPI 基础上,通过对配置文件格式改造,生成了一个映射关系表(Map<名称, 拓展类>),这样能够按需去实例化需要的实现类。解决原生 SPI 必须全部遍历然后遍历查询实现实例的问题.

同时无论是读取配置 Class 亦或是实例化对应的实现实例,都采用了懒加载的双重锁校验,解决了原生 SPI 在多线程情况下,可能存在的并发问题。

总结

自 JDK 1.6 以后,Java 提出了 SPI,一种服务提供发现机制,可以用来启用框架扩展和替换组件。其主要目的是将装配控制权移到程序之外,模块化设计中这个机制尤其重要。其核心思想是 解耦

这种原生 SPI 机制并不完美,其实现存在若干问题

  • 无法按需进行实例化,只能全部实例化之后,遍历去获取需要的实现,造成内存浪费以及无谓的实例化资源消耗。
  • 并没有解决多线程下的并发问题。

Duboo 在原生 SPI 基础上进行改良,通过改变配置文件内容格式,针对每个实现类划分出 K-V ,能够满足在业务上按需实例化的要求。同时采用懒加载的双重锁校验,解决多线程情况下可能存在的并发问题。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-12-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 政采云技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【Linux】进程控制,手搓简洁版shell
数据在默认不修改的情况下是共享的,不各自拷贝一份是因为父子进程间的数据大部分是重复的,一般只有少量数据需要修改,因为各自拷贝一份浪费空间。
_小羊_
2024/12/20
890
【Linux】进程控制,手搓简洁版shell
Linux进程控制
hello,my friend!今天,我们要开始学习新的内容了--->进程控制,进程控制涉及到操作系统如果管理和控制运行在计算机系统内的进程。我们将从fork函数,Linux进程退出,Linux进程等待,Linux进程替换等方面学习。那么接下来我们就开始敲黑板了!!
破晓的历程
2024/06/24
1000
Linux进程控制
【Linux】探索进程控制奥秘,解锁高效实战技巧
因为字符串具有常量属性,字符常量不可被修改。这里的问题是字符串为什么会有常量属性呢?
用户11316056
2024/10/16
540
【Linux】探索进程控制奥秘,解锁高效实战技巧
【Linux】进程控制
我们在前面的文章中多次使用过fork函数,我们在这里再来简单概括一下进程的创建 fork可以在已有的进程中创建出一个新进程,老进程为父进程,新进程为子进程
s-little-monster
2025/02/14
290
【Linux】进程控制
【Linux】进程控制
若父子进程数据都不修改,则父子进程指向同一个物理地址, 若子进程数据修改,则拷贝一个物理空间,将新的地址填到子进程对应的页表中,使子进程重新映射,访问到新的空间 进程的内核数据结构,父子各自有一套,彼此双方互不影响, 代码和数据通过写时拷贝的反方式,实现分开
lovevivi
2023/04/28
2K0
【Linux】进程控制
Linux之进程控制
此外还可以通过调用fork函数创建子进程,子进程和父进程共享fork之后的代码,可以采用对fork返回值进行判断的办法来让父子进程分别执行后续代码的一部分。
始终学不会
2023/04/17
8340
Linux之进程控制
【Linux系统编程】八、进程程序替换
​ 将磁盘中指定的程序加载到内存中,让指定的进程进行执行。不论是哪种后端语言写的程序,exec* 类的函数都可以调用。
利刃大大
2025/02/23
510
【Linux系统编程】八、进程程序替换
Linux进程控制【创建、终止、等待】
进程 创建后,需要对其进行合理管理,光靠 OS 是无法满足我们的需求的,此时可以运用 进程 控制相关知识,对 进程 进行手动管理,如创建 进程、终止 进制、等待 进程 等,其中等待 进程 可以有效解决僵尸 进程 问题
北 海
2023/07/01
3370
Linux进程控制【创建、终止、等待】
详谈 Linux进程控制(看这一篇就够了)
本文将系统介绍进程控制的基本要素,包括进程创建, 进程终止, 进程等待, 进程替换等方面。深入理解进程创建的相关知识, 帮助更好的构建知识架构!
用户11317877
2024/12/20
1360
详谈 Linux进程控制(看这一篇就够了)
Linux进程控制——Linux进程等待
前言:接着前面进程终止,话不多说我们进入Linux进程等待的学习,如果你还不了解进程终止建议先了解:
Eternity._
2024/06/14
1860
Linux进程控制——Linux进程等待
【Linux】万字解读<进程控制>:创建&中止&等待&替换
YY的秘密代码小屋
2024/09/11
1140
【Linux】万字解读<进程控制>:创建&中止&等待&替换
【Linux系统编程】—— 深度解析进程等待与终止:系统高效运行的关键
fork函数初识:在linux中fork函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为⼦进程,⽽原进程为⽗进程。
用户11286421
2025/01/20
1130
【Linux系统编程】—— 深度解析进程等待与终止:系统高效运行的关键
【Linux】Linux进程控制——进程创建、进程终止及进程等待详解
在Linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
P_M_P
2024/08/15
2000
【Linux】Linux进程控制——进程创建、进程终止及进程等待详解
【Linux】进程控制
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
秦jh
2024/09/24
1290
【Linux】进程控制
进程控制
在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
海盗船长
2020/08/27
7330
Linux进程控制
进程是操作系统中的一个重要概念,它是一个程序的一次执行过程,程序是进程的一种静态描述,系统中运行的每一个程序都是在它的进程中运行的。
xxpcb
2020/08/04
2K0
Linux-进程控制
  进程控制不仅仅是管理程序的执行顺序,还涉及到资源的分配等问题,那么话不多说,开始我们今天的话题!
用户11029129
2024/06/04
1180
Linux-进程控制
【Linux进程控制】五、wait()函数——子进程回收
僵尸进程:子进程结束,父进程没有回收子进程的资源(PCB),这个资源必须要由父进程回收,否则就形成僵尸进程。
mindtechnist
2024/08/08
1130
【Linux进程控制】五、wait()函数——子进程回收
【Linux】进程控制
在 Linux 中 fork 函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。我们在前面的学习中也遇到过,所以在此简单介绍一下。
YoungMLet
2024/03/01
1630
【Linux】进程控制
探索进程控制第一弹(进程终止、进程等待)
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
南桥
2024/04/12
1650
探索进程控制第一弹(进程终止、进程等待)
推荐阅读
相关推荐
【Linux】进程控制,手搓简洁版shell
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文