上一章,我简单演示了 Dubbo SPI 的使用方法。我们首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。这其中,getExtensionLoader 用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例。该方法的逻辑比较简单,本章就不就行分析了。下面我们从 ExtensionLoader 的 getExtension 方法作为入口,对拓展类对象的获取过程进行详细的分析。
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) {// 创建拓展实例,并设置到 holder 中instance = createExtension(name);holder.set(instance);}}}return (T) instance;
}
上面代码的逻辑比较简单,首先检查缓存,缓存未命中则创建拓展对象。下面我们来看一下创建拓展对象的过程是怎样的。
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 实例赋值给 instanceinstance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));}}return instance;} catch (Throwable t) {throw new IllegalStateException("...");}
}
createExtension 方法的逻辑稍复杂一下,包含了如下的步骤:
- 通过 getExtensionClasses 获取所有的拓展类
- 通过反射创建拓展对象
- 向拓展对象中注入依赖
- 将拓展对象包裹在相应的 Wrapper 对象中
以上步骤中,第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。在接下来的章节中,我将会重点分析 getExtensionClasses 方法的逻辑,以及简单分析 Dubbo IOC 的具体实现。
3.1 获取所有的拓展类
我们在通过名称获取拓展类之前,首先需要根据配置文件解析出名称到拓展类的映射,也就是 Map<名称, 拓展类>。之后再从 Map 中取出相应的拓展类即可。相关过程的代码分析如下:
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 加锁。加锁后再次检查缓存,并判空。此时如果 classes 仍为 null,则加载拓展类。以上代码的写法是典型的双重检查锁,前面所分析的 getExtension 方法中有相似的代码。关于双重检查就说这么多,下面分析 loadExtensionClasses 方法的逻辑。
private Map<String, Class<?>> loadExtensionClasses() {// 获取 SPI 注解,这里的 type 是在调用 getExtensionLoader 方法时传入的final SPI defaultAnnotation = type.getAnnotation(SPI.class);if (defaultAnnotation != null) {String value = defaultAnnotation.value();if ((value = value.trim()).length() > 0) {// 对 SPI 注解内容进行切分String[] names = NAME_SEPARATOR.split(value);// 检测 SPI 注解内容是否合法,不合法则抛出异常if (names.length > 1) {throw new IllegalStateException("...");}// 设置默认名称,cachedDefaultName 用于加载默认实现,参考 getDefaultExtension 方法if (names.length == 1) {cachedDefaultName = names[0];}}}Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();// 加载指定文件夹配置文件loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);loadDirectory(extensionClasses, DUBBO_DIRECTORY);loadDirectory(extensionClasses, SERVICES_DIRECTORY);return extensionClasses;
}
loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 loadDirectory 做了哪些事情。
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {// fileName = 文件夹路径 + type 全限定名 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 resourceURL = urls.nextElement();// 加载资源loadResource(extensionClasses, classLoader, resourceURL);}}} catch (Throwable t) {logger.error("...");}
}
loadDirectory 方法代码不多,理解起来不难。该方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现。
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;int i = line.indexOf('=');if (i > 0) {// 以 = 为界,截取键与值。比如 dubbo=com.alibaba....DubboProtocolname = line.substring(0, i).trim();line = line.substring(i + 1).trim();}if (line.length() > 0) {// 加载解析出来的限定类名loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);}} catch (Throwable t) {IllegalStateException e = new IllegalStateException("...");}}}} finally {reader.close();}} catch (Throwable t) {logger.error("...");}
}
loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法有点名不副实,它的功能只是操作缓存,而非加载类。该方法的逻辑如下:
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {if (!type.isAssignableFrom(clazz)) {throw new IllegalStateException("...");}if (clazz.isAnnotationPresent(Adaptive.class)) { // 检测目标类上是否有 Adaptive 注解if (cachedAdaptiveClass == null) {// 设置 cachedAdaptiveClass缓存cachedAdaptiveClass = clazz;} else if (!cachedAdaptiveClass.equals(clazz)) {throw new IllegalStateException("...");}} else if (isWrapperClass(clazz)) { // 检测 clazz 是否是 Wrapper 类型Set<Class<?>> wrappers = cachedWrapperClasses;if (wrappers == null) {cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();wrappers = cachedWrapperClasses;}// 存储 clazz 到 cachedWrapperClasses 缓存中wrappers.add(clazz);} else { // 程序进入此分支,表明是一个普通的拓展类// 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常clazz.getConstructor();if (name == null || name.length() == 0) {// 如果 name 为空,则尝试从 Extension 注解获取 name,或使用小写的类名作为 namename = findAnnotationName(clazz);if (name.length() == 0) {throw new IllegalStateException("...");}}// 切分 nameString[] names = NAME_SEPARATOR.split(name);if (names != null && names.length > 0) {Activate activate = clazz.getAnnotation(Activate.class);if (activate != null) {// 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键,// 存储 name 到 Activate 注解对象的映射关系cachedActivates.put(names[0], activate);}for (String n : names) {if (!cachedNames.containsKey(clazz)) {// 存储 Class 到名称的映射关系cachedNames.put(clazz, n);}Class<?> c = extensionClasses.get(n);if (c == null) {// 存储名称到 Class 的映射关系extensionClasses.put(n, clazz);} else if (c != clazz) {throw new IllegalStateException("...");}}}}
}
如上,loadClass 方法操作了不同的缓存,比如 cachedAdaptiveClass、cachedWrapperClasses 和 cachedNames 等等。除此之外,该方法没有其他什么逻辑了,就不多说了。
到此,关于缓存类加载的过程就分析完了。