3.1 获取所有的拓展类

我们在通过名称获取拓展类之前,首先需要根据配置文件解析出拓展项名称到拓展类的映射关系表(Map<名称, 拓展类>),之后再根据拓展项名称从映射关系表中取出相应的拓展类即可。相关过程的代码分析如下:

  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 加锁。加锁后再次检查缓存,并判空。此时如果 classes 仍为 null,则通过 loadExtensionClasses 加载拓展类。下面分析 loadExtensionClasses 方法的逻辑。

  1. private Map<String, Class<?>> loadExtensionClasses() {
  2. // 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的
  3. final SPI defaultAnnotation = type.getAnnotation(SPI.class);
  4. if (defaultAnnotation != null) {
  5. String value = defaultAnnotation.value();
  6. if ((value = value.trim()).length() > 0) {
  7. // 对 SPI 注解内容进行切分
  8. String[] names = NAME_SEPARATOR.split(value);
  9. // 检测 SPI 注解内容是否合法,不合法则抛出异常
  10. if (names.length > 1) {
  11. throw new IllegalStateException("more than 1 default extension name on extension...");
  12. }
  13. // 设置默认名称,参考 getDefaultExtension 方法
  14. if (names.length == 1) {
  15. cachedDefaultName = names[0];
  16. }
  17. }
  18. }
  19. Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
  20. // 加载指定文件夹下的配置文件
  21. loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
  22. loadDirectory(extensionClasses, DUBBO_DIRECTORY);
  23. loadDirectory(extensionClasses, SERVICES_DIRECTORY);
  24. return extensionClasses;
  25. }

loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 loadDirectory 做了哪些事情。

  1. private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
  2. // fileName = 文件夹路径 + type 全限定名
  3. String fileName = dir + type.getName();
  4. try {
  5. Enumeration<java.net.URL> urls;
  6. ClassLoader classLoader = findClassLoader();
  7. // 根据文件名加载所有的同名文件
  8. if (classLoader != null) {
  9. urls = classLoader.getResources(fileName);
  10. } else {
  11. urls = ClassLoader.getSystemResources(fileName);
  12. }
  13. if (urls != null) {
  14. while (urls.hasMoreElements()) {
  15. java.net.URL resourceURL = urls.nextElement();
  16. // 加载资源
  17. loadResource(extensionClasses, classLoader, resourceURL);
  18. }
  19. }
  20. } catch (Throwable t) {
  21. logger.error("...");
  22. }
  23. }

loadDirectory 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现。

  1. private void loadResource(Map<String, Class<?>> extensionClasses,
  2. ClassLoader classLoader, java.net.URL resourceURL) {
  3. try {
  4. BufferedReader reader = new BufferedReader(
  5. new InputStreamReader(resourceURL.openStream(), "utf-8"));
  6. try {
  7. String line;
  8. // 按行读取配置内容
  9. while ((line = reader.readLine()) != null) {
  10. // 定位 # 字符
  11. final int ci = line.indexOf('#');
  12. if (ci >= 0) {
  13. // 截取 # 之前的字符串,# 之后的内容为注释,需要忽略
  14. line = line.substring(0, ci);
  15. }
  16. line = line.trim();
  17. if (line.length() > 0) {
  18. try {
  19. String name = null;
  20. int i = line.indexOf('=');
  21. if (i > 0) {
  22. // 以等于号 = 为界,截取键与值
  23. name = line.substring(0, i).trim();
  24. line = line.substring(i + 1).trim();
  25. }
  26. if (line.length() > 0) {
  27. // 加载类,并通过 loadClass 方法对类进行缓存
  28. loadClass(extensionClasses, resourceURL,
  29. Class.forName(line, true, classLoader), name);
  30. }
  31. } catch (Throwable t) {
  32. IllegalStateException e = new IllegalStateException("Failed to load extension class...");
  33. }
  34. }
  35. }
  36. } finally {
  37. reader.close();
  38. }
  39. } catch (Throwable t) {
  40. logger.error("Exception when load extension class...");
  41. }
  42. }

loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于主要用于操作缓存,该方法的逻辑如下:

  1. private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL,
  2. Class<?> clazz, String name) throws NoSuchMethodException {
  3. if (!type.isAssignableFrom(clazz)) {
  4. throw new IllegalStateException("...");
  5. }
  6. // 检测目标类上是否有 Adaptive 注解
  7. if (clazz.isAnnotationPresent(Adaptive.class)) {
  8. if (cachedAdaptiveClass == null) {
  9. // 设置 cachedAdaptiveClass缓存
  10. cachedAdaptiveClass = clazz;
  11. } else if (!cachedAdaptiveClass.equals(clazz)) {
  12. throw new IllegalStateException("...");
  13. }
  14. // 检测 clazz 是否是 Wrapper 类型
  15. } else if (isWrapperClass(clazz)) {
  16. Set<Class<?>> wrappers = cachedWrapperClasses;
  17. if (wrappers == null) {
  18. cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
  19. wrappers = cachedWrapperClasses;
  20. }
  21. // 存储 clazz 到 cachedWrapperClasses 缓存中
  22. wrappers.add(clazz);
  23. // 程序进入此分支,表明 clazz 是一个普通的拓展类
  24. } else {
  25. // 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常
  26. clazz.getConstructor();
  27. if (name == null || name.length() == 0) {
  28. // 如果 name 为空,则尝试从 Extension 注解中获取 name,或使用小写的类名作为 name
  29. name = findAnnotationName(clazz);
  30. if (name.length() == 0) {
  31. throw new IllegalStateException("...");
  32. }
  33. }
  34. // 切分 name
  35. String[] names = NAME_SEPARATOR.split(name);
  36. if (names != null && names.length > 0) {
  37. Activate activate = clazz.getAnnotation(Activate.class);
  38. if (activate != null) {
  39. // 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键,
  40. // 存储 name 到 Activate 注解对象的映射关系
  41. cachedActivates.put(names[0], activate);
  42. }
  43. for (String n : names) {
  44. if (!cachedNames.containsKey(clazz)) {
  45. // 存储 Class 到名称的映射关系
  46. cachedNames.put(clazz, n);
  47. }
  48. Class<?> c = extensionClasses.get(n);
  49. if (c == null) {
  50. // 存储名称到 Class 的映射关系
  51. extensionClasses.put(n, clazz);
  52. } else if (c != clazz) {
  53. throw new IllegalStateException("...");
  54. }
  55. }
  56. }
  57. }
  58. }

如上,loadClass 方法操作了不同的缓存,比如 cachedAdaptiveClass、cachedWrapperClasses 和 cachedNames 等等。除此之外,该方法没有其他什么逻辑了。

到此,关于缓存类加载的过程就分析完了。整个过程没什么特别复杂的地方,大家按部就班的分析即可,不懂的地方可以调试一下。接下来,我们来聊聊 Dubbo IOC 方面的内容。