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

时间:2022-07-24
本文章向大家介绍【七夕特殊礼物】Dubbo学习之SPI实战与debug源码,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

目录

  • 绪论
  • 环境搭建
    • 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 {
    @Override
    public 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 {
    @Override
    public 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.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
       http://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 first
            if (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,或使用小写的类名作为 name
                name = 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 开头,且方法仅有一个参数,且方法访问级别为 public
                if (!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相关机制,倘若能够帮助大家学习,希望大家多多支持。

学海无涯苦做舟~