目录

  • 绪论
  • 环境搭建
    • dubbo-demo-interface
    • dubbo-demo-xml
      • dubbo-demo-xml-provider
  • 源码跟踪
    • getExtension
    • createExtension
    • getExtensionClasses
    • loadDirectory
    • loadResource
    • injectExtension
  • 总结

绪论

上篇文章《dubbo学习之源码创建属于自己的dubbo-demo》溪源带着大家简单搭建了自己的demo,基础环境已经搭建完成,从这篇文章开始,溪源便开始学习并总结Dubbo的相关机制,此篇文章的核心是实践SPI机制和SPI源码跟踪。时间宝贵,下面步入正题:
对于SPI机制,溪源不再做介绍了,大家有兴趣的可以自行谷歌,同时分享溪源之前总结的:java实践SPI机制及浅析源码。

环境搭建

上篇溪源基本上已经把环境搭建完成,本篇只需要将代码稍作改动就可以完成;
没有看过上篇文章的伙伴也没有关系,溪源也会在这里详细介绍环境的搭建,涉及实体类等相关引用,大家可能需要移步上篇文章中获取。

dubbo-demo-interface

目录结构:

由于本篇文章主要介绍SPI机制,故服务接口只需要定义UserService接口即可。

  • UserService
@SPI
public interface UserService {List<User> getUserAddressList();
}

注意:UserService服务接口只是稍作改动加了@SPI注解;
如果无法使用该注解,大家可以根据自己编译器使用快捷键Alt+Enter引入相关依赖;

dubbo-demo-xml

dubbo-demo-xml-provider

该模块下目录结构如图:

主要涉及服务实现类:GeneralServiceImpl,MemberServiceImpl;主启动类ProviderApplication,SPI配置文件;

  • GeneralServiceImpl
@Service
public class GeneralServiceImpl implements UserService, Serializable {@Overridepublic List<User> getUserAddressList() {List<User> userList = new LinkedList<>();for (int i = 0; i < 2; i++) {User user = new User();user.setUserLevel("general");user.setUserAddress("杭州西湖 " + i);userList.add(user);}return userList;}
}
  • UserServiceImpl
@Service
public class MemberServiceImpl implements UserService, Serializable {@Overridepublic List<User> getUserAddressList() {List<User> userList = new LinkedList<>();for (int i = 0; i < 2; i++) {User user = new User();user.setUserLevel("member");user.setUserAddress("杭州拱墅 " + i);userList.add(user);}return userList;}
}
  • dubbo-provider.xml
<?xml version="1.0" encoding="UTF-8"?><beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"xmlns="http://www.springframework.org/schema/beans"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsdhttp://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd"><dubbo:application metadata-type="remote" name="user-provider"/>
<!--    <dubbo:metadata-report address="zookeeper://120.55.195.153:2181"/>--><!--由于服务提供者需要暴露服务接口实现,自动注入容器中,则无须包扫描--><context:component-scan base-package="org.apache.dubbo.demo.provider"/><dubbo:registry address="zookeeper://120.55.195.153:2181" timeout="10000"/><dubbo:protocol name="dubbo" port="20881"/><!--暴露服务接口:多个服务接口实现类,均需要暴露--><dubbo:service interface="org.apache.dubbo.demo.UserService" ref="memberServiceImpl"/><dubbo:service interface="org.apache.dubbo.demo.UserService" ref="generalServiceImpl"/>
</beans>
  • SPI配置文件
    新建META-INF/dubbo文件夹:注意建立子文件必须以dubbo为命名,可以理解为约束规范开发吧。
general=org.apache.dubbo.demo.provider.impl.GeneralServiceImpl
member=org.apache.dubbo.demo.provider.impl.MemberServiceImpl
  • ProviderApplication
 public static void main(String[] args) throws Exception {providerSpiTest();}/*** SPI接口调用方式*/private static void providerSpiTest() throws IOException {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-provider.xml");context.start();String spiKey = "member";ExtensionLoader<UserService> extensionLoader = ExtensionLoader.getExtensionLoader(UserService.class);UserService extension = extensionLoader.getExtension(spiKey);System.out.println(extension.getUserAddressList().get(0).getUserAddress());System.in.read();}

源码跟踪

SPI实战用例已经准备完成,下面可以开始着手跟进dubbo源码中。

从这行代码开始进入SPI相关代码

总之,getExtensionLoader()方法主要验证加载扩展对象的必备条件,然后创建ExtensionLoader对象并返回;

getExtension

  • ExtensionLoader#getExtension(String name, boolean wrap)
public T getExtension(String name, boolean wrap) {if (StringUtils.isEmpty(name)) {throw new IllegalArgumentException("Extension name == null");}// 获取默认的拓展实现类;这里意思是指如果参数name=true,则默认加载@SPI("general")注解内的属性值即general对应的接口实现类if ("true".equals(name)) {return getDefaultExtension();}final Holder<Object> holder = getOrCreateHolder(name);Object instance = holder.get();//单例设计模式:双重验证+锁if (instance == null) {synchronized (holder) {instance = holder.get();if (instance == null) {//创建扩展对象instance = createExtension(name, wrap);holder.set(instance);}}}return (T) instance;}
  • ExtensionLoader#getOrCreateHolder()
private Holder<Object> getOrCreateHolder(String name) {Holder<Object> holder = cachedInstances.get(name);if (holder == null) {//主要是否能够命中缓存,否则创建对象并存入缓存中cachedInstances.putIfAbsent(name, new Holder<>());holder = cachedInstances.get(name);}return holder;}

createExtension

创建扩展实现类主要分为以下几个步骤:

  1. 加载所有的扩展实现类;
  2. 反射创建扩展实现类的实例对象;
  3. 实例对象属性注入;
  4. 遍历包装类列表 cachedWrapperClasses,创建包装类实例,并注入依赖。
  • ExtensionLoader#createExtension(String name, boolean wrap)
 @SuppressWarnings("unchecked")private T createExtension(String name, boolean wrap) {//1.从配置文件中加载所有的拓展类,可得到指定“配置项名称”到“配置类”的映射关系表即参数对应的服务接口实现类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);}// 2.向实例中注入依赖injectExtension(instance);if (wrap) {List<Class<?>> wrapperClassesList = new ArrayList<>();if (cachedWrapperClasses != null) {wrapperClassesList.addAll(cachedWrapperClasses);wrapperClassesList.sort(WrapperComparator.COMPARATOR);Collections.reverse(wrapperClassesList);}if (CollectionUtils.isNotEmpty(wrapperClassesList)) {for (Class<?> wrapperClass : wrapperClassesList) {Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);if (wrapper == null|| (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));}}}}initExtension(instance);return instance;} catch (Throwable t) {throw new IllegalStateException("Extension instance (name: " + name + ", class: " +type + ") couldn't be instantiated: " + t.getMessage(), t);}}

下面主要分析一下如何加载扩展类

getExtensionClasses

  • ExtensionLoader#getExtensionClasses()
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;}

此方法中又是使用的单例模式,大家如果看过源码次数比较多的话,dubbo用的单例模式次数还是较多的,原理相同,溪源这里就不再介绍了。主要看下loadExtensionClasses方法具体的实现逻辑。

  • ExtensionLoader#loadExtensionClasses()
    大家看源码的时候会发现,for循环内会调用两次loadDirectory方法,这里主要是向下兼容吧,由于 Dubbo 现在的包前缀变为了 “org.apache”,之前为 “com.alibaba”,因此会根据该路径再加载一次,即:type.getName().replace(“org.apache”, “com.alibaba”)。
    此外,Dubbo 中指定的文件夹主要分为:
    **
    1.META-INF/dubbo/internal/
    2.META-INF/dubbo/
    3.META-INF/services/
    **
    所以,溪源在搭建SPI环境时,特意说明dubbo子目录的命名,此方法内的重点放在for循环中的方法loadDirectory;
private Map<String, Class<?>> loadExtensionClasses() {//缓存默认扩展类实现类cacheDefaultExtensionName();Map<String, Class<?>> extensionClasses = new HashMap<>();for (LoadingStrategy strategy : strategies) {loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());}return extensionClasses;}

- cacheDefaultExtensionName

此方法主要用于获取并缓存接口默认实现类。SPI 注解如果存在且配置了 value 属性,则缓存到 cachedDefaultName 中。方法主要逻辑:
1.@SPI 配置的 value,只能有 1 个名称,否则抛出 IllegalStateException 异常;
2.前面调用 getExtension 时判断,如果传入的别名为 “true”,获取的默认扩展实现类,即通过此处获取。

private void cacheDefaultExtensionName() {//获取SPI注解final SPI defaultAnnotation = type.getAnnotation(SPI.class);if (defaultAnnotation == null) {return;}
//获取默认扩展实现类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];}}}

loadDirectory

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type,boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages) {String fileName = dir + type;try {Enumeration<java.net.URL> urls = null;ClassLoader classLoader = findClassLoader();// try to load from ExtensionLoader's ClassLoader firstif (extensionLoaderClassLoaderFirst) {ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {urls = extensionLoaderClassLoader.getResources(fileName);}}if (urls == null || !urls.hasMoreElements()) {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, overridden, excludedPackages);}}} catch (Throwable t) {logger.error("Exception occurred when loading extension class (interface: " +type + ", description file: " + fileName + ").", t);}}

loadResource

  • loadResource()
    加载SPI配置文件
 private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,java.net.URL resourceURL, boolean overridden, String... excludedPackages) {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;// 以等于号 = 为界,截取键与值int i = line.indexOf('=');if (i > 0) {name = line.substring(0, i).trim();line = line.substring(i + 1).trim();}if (line.length() > 0 && !isExcluded(line, excludedPackages)) {// 加载类,并通过 loadClass 方法对类进行缓存loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name, overridden);}} 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);}}

解析SPI配置文件的逻辑大家可以仔细研究一下,可以通过改造该方法,优化提升JDK中的SPI加载性能。
loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于主要用于操作缓存。

  • loadClass()
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,boolean overridden) 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.");}// 检测目标类上是否有 Adaptive 注解if (clazz.isAnnotationPresent(Adaptive.class)) {// 设置 cachedAdaptiveClass缓存cacheAdaptiveClass(clazz, overridden);} else if (isWrapperClass(clazz)) {// 检测 clazz 是否是 Wrapper 类型cacheWrapperClass(clazz);} else {// 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常clazz.getConstructor();if (StringUtils.isEmpty(name)) {// 如果 name 为空,则尝试从 Extension 注解中获取 name,或使用小写的类名作为 namename = 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)) {// 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键,// 存储 name 到 Activate 注解对象的映射关系cacheActivateClass(clazz, names[0]);for (String n : names) {cacheName(clazz, n);saveInExtensionClass(extensionClasses, clazz, n, overridden);}}}}

injectExtension

Dubbo 通过 injectExtension 为扩展类的实例注入依赖:即实例化要注入的类,然后反射调用set方法注入实例中去。

private T injectExtension(T instance) {if (objectFactory == null) {return instance;}try {// 遍历目标类的所有方法for (Method method : instance.getClass().getMethods()) {// 检测方法是否以 set 开头,且方法仅有一个参数,且方法访问级别为 publicif (!isSetter(method)) {continue;}/*** 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 {String property = getSetterProperty(method);/*** objectFactory是AdaptiveExtensionFactory实例* 比如这里的pt是com.alibaba.dubbo.rpc.Protocol,property是protocol* objectFactory就会根据这两个参数去获取Protocol对应的扩展实现的实例*/Object object = objectFactory.getExtension(pt, property);if (object != null) {// 通过反射调用 setter 方法设置依赖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;}

总结

今天是一个特殊的节日—七夕情人节,溪源衷心的希望程序员脱离单身狗的队列。溪源为了整理这篇内容,放弃了陪女票逛街的要求,被暴击一顿,呜呜~。果然跟踪源码枯燥且无味,但是为了不让自己停止前进的脚步,溪源选择挑战。自己整理也是为了后期自己的理解,也方便新手学习dubbo相关机制,倘若能够帮助大家学习,希望大家多多支持。

学海无涯苦做舟~

【七夕特殊礼物】Dubbo学习之SPI实战与debug源码相关推荐

  1. 近期爬虫学习体会以及爬豆瓣Top250源码实战

    近期爬虫学习体会以及爬豆瓣Top250源码实战 我是在B站https://www.bilibili.com/video/BV12E411A7ZQ?p=25里学习的,至今已经可以手写爬豆瓣Top250代 ...

  2. 无人驾驶--实时定位与地图构建(SLAM)仿真与实战(附源码)

    无人驾驶–实时定位与地图构建(SLAM)仿真与实战(附源码) 一个SLAM的技术小结,供自己回顾也为后人学习提供参考. 另外建了一个无人驾驶方面的微信交流群,有兴趣的同行或者专家学者可以加我微信:wx ...

  3. 毕业设计-基于SSM框架大学教务管理平台项目开发实战教程(附源码)

    文章目录 1.项目简介 2.项目收获 3.项目技术栈 4.测试账号 5.项目部分截图 6.常见问题 毕业设计-基于SSM框架大学教务管理平台项目实战教程-附源码 课程源码下载地址:https://do ...

  4. c语言案例分析105,C语言实战105例源码

    C语言实战105例源码 以下程序大家如有兴趣可在文件夹下载即可 第1部分 基础篇 实例1 一个价值"三天"的BUG 2 实例2 灵活使用递增(递减)操作符 5 实例3 算术运算符计 ...

  5. PHP单页面加密视频教程附源码,thinkphp3.2最新版本项目实战视频教程(含源码)

    php教程 当前位置:主页 > php教程 > thinkphp3.2最新版本项目实战视频教程(含源码) thinkphp3.2最新版本项目实战视频教程(含源码) 教程大小:2.1GB   ...

  6. python人工智能项目实例-python人工智能项目实战,PDF+源码

    原标题:python人工智能项目实战,PDF+源码 <python人工智能项目 Intelligent Projects Using Python> 实施机器学习和深度学习方法,使用Pyt ...

  7. Mybatis底层原理学习(二):从源码角度分析一次查询操作过程

    在阅读这篇文章之前,建议先阅读一下我之前写的两篇文章,对理解这篇文章很有帮助,特别是Mybatis新手: 写给mybatis小白的入门指南 mybatis底层原理学习(一):SqlSessionFac ...

  8. Qt学习笔记,再次分析EVA源码之后得出的结论-QListView,QListViewItem(Qt3);Q3ListView,Q3ListViewItem(Qt4)...

    Qt学习笔记,再次分析EVA源码之后得出的结论-QListView,QListViewItem(Qt3);Q3ListView,Q3ListViewItem(Qt4) 今天再次分析了Eva的源码,也看 ...

  9. clickhouse原理解析与开发实战 pdf_Spring全家桶集合:SpringBoot+SpringCloud实战,Spring源码原理...

    一.Spring技术内幕(电子书籍赠送) 深入解析Spring架构与设计原理 本书探讨了Spring框架的设计原理.架构和运行机制.作为在Java领域最为成功的开源软件之一,Spring在Java E ...

最新文章

  1. golang odbc mysql_golang使用odbc链接hive
  2. Lua table笔记
  3. shebang_Shebang来Java了吗?
  4. HDU-3664-Permutation Counting
  5. mnesia mysql性能,Mnesia数据库的存储容量是多少?
  6. windows7正版验证_Windows7 寿终正寝:那些一并消逝的软件你知多少?
  7. 应用交付工程师Troubleshooting经验分享2
  8. KEIL中无IAP或者STC芯片型号怎么办
  9. java解析json字符串详解(两种方法)
  10. 领英辅助工具领英精灵自动加好友功能讲解
  11. 快搜搜:在网上找工作如何防骗!
  12. 网易云音乐api,硅谷云音乐调用登录API出现,网络太拥挤,登录失败(最简单的解决方案,有效哦)
  13. FIP: A fast overlapping community-based influence maximization algorithm using probability coefficie
  14. 微信小程序使用canvas画图并保存到手机相册踩坑总结
  15. python调用linux的命令
  16. 马克思 第四章 资本主义的形成及其本质
  17. 第七周【任务2】长短期记忆网络LSTM的前向、后向传播
  18. 将图片文件以二进制方式保存和恢复
  19. ORA-39002: 操作无效 ORA-39070: 无法打开日志文件
  20. 0基础入门学PLC太难?谈谈PLC的学习方法

热门文章

  1. [置顶] 火车票余票接口API使用方法
  2. Java中的新生代、老年代和永久代
  3. ShardingSphere(四) 垂直分库配置搭建,实现写入读取
  4. gpt efi win7 linux,在EFI+GPT硬盘上利用grub2实现Linux/Win7/win8等多重启动
  5. 中国未来的可能性思考- 系统化思维-公司培训
  6. mysql的utf8与utf8mb4 异同;utf8mb4_unicode_ci 与 utf8mb4_general_ci 如何选择
  7. 总结一下适合自己的看书方式
  8. 昨天的一个披扣的问题处理到很晚没搞定的原因
  9. Spring Cloud 微服务实战系列-Eureka注册中心(二)
  10. Mybatis面试题-日更