Dubbo源码分析系列-深入Dubbo SPI机制

灰太狼 2021-10-13 22:48 531阅读 0赞

导语
  在之前的博客中介绍过关于Java中SPI的机制,也简单的分析了关于Java中SPI怎么去使用。SPI的全称Service Provider Interface,是一种服务发现机制。SPI的本质就是将接口实现类的全类名配置到文件中,通过类加载器来读取配置文件,从而达到类加载的目的。这样在运行的时候可以替换接口实现类。所以在很多的第三方框架中都使用到了这个技术。最为典型的就是关于日志的处理、关于数据库的处理等等。

Dubbo SPI 源码分析

   在之前的博客中提到了Dubbo的扩展机制以及Java的SPI机制,其中提到了一个比较关键的类ExtensionLoader,这个类作为扩展类加载器的核心类。都知道在Java类加载机制中有四种类加载器,BootstrapClassLoader类加载器、ExtClassLoader扩展类加载器、ApplicationClassLoader 应用类加载器以及自定义类加载器。对于这几个类加载器在Java类加载机制中都有提到。这里我们通过Dubbo提供的ExtensionLoader扩展类加载器来深入了解一下Dubbo的SPI机制。
在ExtensionLoader类中怎么去获取一个ExtensionLoader实例
  在分析之前我们先来看一下,阿里把Dubbo项目给Apache之后,Apache给Dubbo做了很多的重构,在源码中很多阿里开发的功能被抛弃了。而这个类扩展功能org.apache.dubbo.common.extension.ExtensionLoader 也是被Apache重新编写。
  在ExtensionLoader中提供了一个getExtensionLoader()通过传入的类型来获取到Class的扩展类加载器

  1. @SuppressWarnings("unchecked")
  2. public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
  3. //判断传入的类型是否为空
  4. if (type == null) {
  5. throw new IllegalArgumentException("Extension type == null");
  6. }
  7. //判断是否为一个接口
  8. if (!type.isInterface()) {
  9. throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
  10. }
  11. //判断该类型是否标注了@SPI注解也就是说是否是一个扩展类型
  12. if (!withExtensionAnnotation(type)) {
  13. throw new IllegalArgumentException("Extension type (" + type +
  14. ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
  15. }
  16. //从缓存中获取到对应类型的扩展类加载器
  17. ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
  18. //如果加载器为空,则先进行创建,然后再次从缓存中获取
  19. if (loader == null) {
  20. EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
  21. loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
  22. }
  23. return loader;
  24. }

  在那么当获取到扩展类加载器之后接下来的操作是什么呢?对于这些框架的底层实现来说有一个对应的获取扩展类的方法就会有一个对应的实现扩展的方法。下面就来看看这个实现扩展的方法。

  1. public T getExtension(String name) {
  2. //获取通过名称进行获取
  3. if (StringUtils.isEmpty(name)) {
  4. throw new IllegalArgumentException("Extension name == null");
  5. }
  6. if ("true".equals(name)) {
  7. //获取默认的扩展实现类
  8. return getDefaultExtension();
  9. }
  10. //创建一个目标持有对象
  11. final Holder<Object> holder = getOrCreateHolder(name);
  12. //获取对象实例
  13. Object instance = holder.get();
  14. //如果获取到对象为空,启动一个双重检查,
  15. if (instance == null) {
  16. synchronized (holder) {
  17. instance = holder.get();‘
  18. //如果确实没有对应的实例
  19. if (instance == null) {
  20. //创建一个扩展实例
  21. instance = createExtension(name);
  22. //将实例设置到holder
  23. holder.set(instance);
  24. }
  25. }
  26. }
  27. return (T) instance;
  28. }

  可以看到其中有一个方法通过名称创建一个扩展实例。下面就来进入到这方法中来看看其中的实现逻辑。

  1. @SuppressWarnings("unchecked")
  2. private T createExtension(String name) {
  3. //获取扩展类
  4. Class<?> clazz = getExtensionClasses().get(name);
  5. if (clazz == null) {
  6. throw findException(name);
  7. }
  8. try {
  9. //从扩展实例缓存中获取对应实例
  10. T instance = (T) EXTENSION_INSTANCES.get(clazz);
  11. if (instance == null) {
  12. //通过反射创建实例
  13. EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
  14. instance = (T) EXTENSION_INSTANCES.get(clazz);
  15. }
  16. //注入扩展实例
  17. injectExtension(instance);
  18. //获取包装类
  19. Set<Class<?>> wrapperClasses = cachedWrapperClasses;
  20. if (CollectionUtils.isNotEmpty(wrapperClasses)) {
  21. //循环创建Wrapper
  22. for (Class<?> wrapperClass : wrapperClasses) {
  23. //将当前instance 作为参数传递而Wrapper的构造方法,并通过反射创建Wrapper实例。
  24. //向Wrapper实例中注入依赖,最后将Wrapper实例再次赋值给instance变量
  25. instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
  26. }
  27. }
  28. //返回实例
  29. return instance;
  30. } catch (Throwable t) {
  31. throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
  32. type + ") couldn't be instantiated: " + t.getMessage(), t);
  33. }
  34. }

  在这里会看到其中有几个关键方法
当进入到该方法的时候进行一个扩展类的获取

Class<?> clazz = getExtensionClasses().get(name);

通过扩展类获取到对应的实例

EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());

这是比较关键的一点,这里其实就是Dubbo的IOC以及AOP的具体实现。下面就来看看这几个方法的具体实现

instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));

getExtensionClasses()

  1. private Map<String, Class<?>> getExtensionClasses() {
  2. //从缓存中获取已加载的扩展类
  3. Map<String, Class<?>> classes = cachedClasses.get();
  4. //进行一个同步双重检查
  5. if (classes == null) {
  6. synchronized (cachedClasses) {
  7. classes = cachedClasses.get();
  8. if (classes == null) {
  9. //加载扩展类
  10. classes = loadExtensionClasses();
  11. cachedClasses.set(classes);
  12. }
  13. }
  14. }
  15. return classes;
  16. }

  首先检查缓存中是否存在对应的加载类。如果缓存中没有的话,通过synchronized加锁之后再次进行检查,如果加锁之后的检查还是为空,这时就通过loadExtensionClasses()方法加载扩展类。那么loadExtensionClasses()方法的内部实现又是什么样子的呢?

  1. // synchronized in getExtensionClasses
  2. private Map<String, Class<?>> loadExtensionClasses() {
  3. //缓存默认扩展类加载
  4. cacheDefaultExtensionName();
  5. //创建映射对象
  6. Map<String, Class<?>> extensionClasses = new HashMap<>();
  7. //把阿里的目录替换成了Apache的目录进行获取
  8. loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
  9. loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
  10. loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
  11. loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
  12. loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
  13. loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
  14. return extensionClasses;
  15. }

  在这里会看到Apache官方提供了一个新的扩展加载方式,所以说在保留原来的基础上对于该方法进行了修改。这里我们来查看cacheDefaultExtensionName()方法,这个方法就是阿里原生提供的方法逻辑

  1. private void cacheDefaultExtensionName() {
  2. //首先获取到SPI注解
  3. final SPI defaultAnnotation = type.getAnnotation(SPI.class);
  4. //如果获取到的注解不为空
  5. if (defaultAnnotation != null) {
  6. //获取到注解对应的value
  7. String value = defaultAnnotation.value();
  8. if ((value = value.trim()).length() > 0) {
  9. //按照value进行分隔。
  10. String[] names = NAME_SEPARATOR.split(value);
  11. //判断SPI内是否有非法内容如果存在非法内容则抛出异常。
  12. if (names.length > 1) {
  13. throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
  14. + ": " + Arrays.toString(names));
  15. }
  16. //设置默认名称,
  17. if (names.length == 1) {
  18. cachedDefaultName = names[0];
  19. }
  20. }
  21. }
  22. }

cahcedDefaultName使用的位置如图所示。
在这里插入图片描述
其中最为重要的就是在getDefaultExtension() 方法中的使用了了这个方法中会使用默认名称进行加载

  1. public T getDefaultExtension() {
  2. getExtensionClasses();
  3. if (StringUtils.isBlank(cachedDefaultName) || "true".equals(cachedDefaultName)) {
  4. return null;
  5. }
  6. return getExtension(cachedDefaultName);
  7. }

  在上面的一段代码的分析中,可以看到loadExtensionClasses() 方法提供了两个方面的内容一个是对于SPI注解的解析,一个调用laodDirectory方法加载类所指定的配置文件。上的分析可以看到解析SPI注解就是为了获取其中的value。下面来看一下loadDirectory() 方法的使用
  我们会注意到这些方法都是没有返回值的,其中操作的所有的内容都是被放到缓存中。这个点也是在Java编程中一个关键点,多使用缓存减少创建对象性能的消耗。

  1. private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {
  2. //拼接文件名称
  3. String fileName = dir + type;
  4. try {
  5. //定义一个枚举类型
  6. Enumeration<java.net.URL> urls;
  7. //获取类加载器
  8. ClassLoader classLoader = findClassLoader();
  9. if (classLoader != null) {
  10. //通过类名获取url
  11. urls = classLoader.getResources(fileName);
  12. } else {
  13. urls = ClassLoader.getSystemResources(fileName);
  14. }
  15. if (urls != null) {
  16. while (urls.hasMoreElements()) {
  17. //进行加载资源
  18. java.net.URL resourceURL = urls.nextElement();
  19. loadResource(extensionClasses, classLoader, resourceURL);
  20. }
  21. }
  22. } catch (Throwable t) {
  23. logger.error("Exception occurred when loading extension class (interface: " +
  24. type + ", description file: " + fileName + ").", t);
  25. }
  26. }

  可以看到loadDirectory() 方法先通过classLoader获取的所有资源的URL,然后通过loadResource进行资源的加载操作。获取资源的方法就不需要在看了,这里主要来跟一下怎么去加载这个资源。

  1. private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
  2. try {
  3. try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
  4. //获取到文件流对象
  5. String line;
  6. while ((line = reader.readLine()) != null) {
  7. //获取到#字符的定位位置 # 之后的都是注释
  8. final int ci = line.indexOf('#');
  9. if (ci >= 0) {
  10. //截取#之前的所有字符
  11. line = line.substring(0, ci);
  12. }
  13. //去除空格
  14. line = line.trim();
  15. if (line.length() > 0) {
  16. try {
  17. String name = null;
  18. //以等号为界限获取到键值对
  19. int i = line.indexOf('=');
  20. if (i > 0) {
  21. name = line.substring(0, i).trim();
  22. line = line.substring(i + 1).trim();
  23. }
  24. if (line.length() > 0) {
  25. //加载类 通过loadClass方法对其进行缓存
  26. loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
  27. }
  28. } catch (Throwable t) {
  29. IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
  30. exceptions.put(line, e);
  31. }
  32. }
  33. }
  34. }
  35. } catch (Throwable t) {
  36. logger.error("Exception occurred when loading extension class (interface: " +
  37. type + ", class file: " + resourceURL + ") in " + resourceURL, t);
  38. }
  39. }

  这里可以知道loadResource方法主要是用来读取和解析配置文件,并且通过反射加载类,最后调用loadClass方法进行其他操作,而loadClass方法主要的作用是什么?下面就来研究一下

  1. private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
  2. if (!type.isAssignableFrom(clazz)) {
  3. throw new IllegalStateException("Error occurred when loading extension class (interface: " +
  4. type + ", class line: " + clazz.getName() + "), class "
  5. + clazz.getName() + " is not subtype of interface.");
  6. }
  7. //检查目标类上是否有Adaptive 注解
  8. if (clazz.isAnnotationPresent(Adaptive.class)) {
  9. //设置Adaptive的缓存
  10. cacheAdaptiveClass(clazz);
  11. } else if (isWrapperClass(clazz)) {
  12. //设置Wrapper的缓存
  13. cacheWrapperClass(clazz);
  14. } else {
  15. //获取到构造函数
  16. clazz.getConstructor();
  17. if (StringUtils.isEmpty(name)) {
  18. //获取注解名,
  19. name = findAnnotationName(clazz);
  20. if (name.length() == 0) {
  21. throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
  22. }
  23. }
  24. //切分name
  25. String[] names = NAME_SEPARATOR.split(name);
  26. if (ArrayUtils.isNotEmpty(names)) {
  27. //如果类上面有Activate注解,使用name数组第一个作为键,
  28. //存储name到Activate 注解对象的映射关系
  29. cacheActivateClass(clazz, names[0]);
  30. for (String n : names) {
  31. //缓存名称
  32. cacheName(clazz, n);
  33. //在扩展类中进行存储
  34. saveInExtensionClass(extensionClasses, clazz, n);
  35. }
  36. }
  37. }
  38. }

  到这里关于缓存类以及类加载的过程就结束了,通过整个过程的分析可以看到Apache在阿里的基础上做到额改动还是很大的,但是基本的原理还是与Java的SPI机制是一样的。

总结

  在这里会看到通过Dubbo源码Apache对于Dubbo的SPI机制还是有很大的改动的,从类的路径获取上,注解的获取上都做了一定的优化,但是万变不离其宗。整个的设计原则还是按照JavaSPI提供的思路来设计的。所以首先要了解的就是Java的SPI机制以及类加载机制反射机制等等再去了解Dubbo的SPI机制会很容易理解。

发表评论

表情:
评论列表 (有 0 条评论,531人围观)

还没有评论,来说两句吧...

相关阅读

    相关 dubbo解析-SPI机制

     架构体系   框架介绍   概述   Dubbo是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spri

    相关 dubbo 自适应SPI机制分析

    在之前博客中,我们介绍了JAVA SPI 以及 Dubbo  SPI 的基本使用以及源码分析,通过指定扩展点类型,可以创建扩展点的实现类,但是在dubbo中,有些时候并不期望直