我是靠谱客的博主 潇洒银耳汤,最近开发中收集的这篇文章主要介绍一文带你彻底了解Dubbo的SPI机制,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

文章目录

      • Java SPI使用
      • Dubbo为何自己实现一套SPI
      • Dubbo SPI初体验
      • SPI
      • IOC
      • Aop
        • 什么是包装类?
        • AOP增强
      • @Adaptive
        • 一些需要注意的地方
      • @Activate
      • 结尾

在分布式系统中服务的调用,就要涉及到RPC。而提起RPC,想到最多的就是dubbo。了解dubbo的工作原理,有助于我们更好的使用它。

打开下载的dubbo工程,我发现在dubbo的各个子模块,有很多这样的类似代码:ExtensionLoader.getExtensionLoader(xxx.class),经过我一番百度后发现他们指向一个名次:SPI。官网中表示Dubbo采用微内核+SPI,使得有特殊需求的接入方可以自定义扩展,做定制的二次开发。好像解释了什么,但是又好像还不清楚它是干什么的,不过不要急,接下来我们就从例子使用到dubbo的源码来剖析spi的神秘面纱。

Java SPI使用

在了解dubbo spi之前,我们就不得不提一下Java SPI。SPI的全称是service provider interface,起初是提供给厂商做插件开发的 ,它使用了策略模式, 一个接口多种实现。 我们只声明接口, 具体的实现并不在程序中直接确定, 而是由程序之外的配置掌控 。啥意思呢?我们搞个例子就明白了:

//定义一个接口
public interface DoWork {
    void doWork();
}
//两个实现类
public class WriteBug implements DoWork {
    @Override
    public void doWork() {
        System.out.println("写bug");
    }
}
public class WriteCode implements DoWork {

    @Override
    public void doWork() {
        System.out.println("写程序");
    }
}

在META-INF.services目录下创建一个文件,文件名即为接口的全路径名:com.alibaba.dubbo.demo.provider.JavaSPI.DoWork:这个样子;

在这里插入图片描述

文件中的内容:

com.alibaba.dubbo.demo.provider.JavaSPI.WriteBug
com.alibaba.dubbo.demo.provider.JavaSPI.WriteCode

准备工作已经做完了,接下来测试一下效果如何:

  public static void main(String[] s){
        //获取到DoWork的所有实现
        ServiceLoader<DoWork> serviceServiceLoader = ServiceLoader.load(DoWork.class);
        for (DoWork doWork : serviceServiceLoader){
                doWork.doWork();
        }
    }

运行结果:

写bug
写程序

可以看到执行了全部实现类的方法,因此我们可以理解为spi实际上就是在文件中记录接口都有哪些实现类,然后根据接口名来实例化它的实现类。

在这里插入图片描述

Dubbo为何自己实现一套SPI

但是实际上dubbo并没有采用Java的SPI,而是自己实现了一套SPI机制。既然有Java的SPI,为什么dubbo不用呢?

我们观察上面的例子,可以发现其实Java SPI实际上是将接口所有的实现类都加载出来了,如果项目过大,那么会加载全部的实现类,那肯定会有一些实现类实现类没有用上,这样就造成了浪费。

另一方面,Java的SPI功能也比较单一,dubbo的spi在此基础上还实现了ioc、aop等功能,这些我会在下面从源码的角度来分析这些功能。

Dubbo SPI初体验

既然我们上面看了Java的SPI如何使用,那这个dubbo的spi我们也是需要看看的。

在dubbo中,约定文件是放在以下三个目录中:

  1. META-INF/services/
  2. META-INF/dubbo/
  3. META-INF/dubbo/internal/

大部分还是比较类似的,先定义一个接口、两个实现类,要注意接口上面需要加@SPI注解的。

//注意加上@SPI注解
@SPI
public interface DoWork {
    void doWork();
}
public class WriteBug implements DoWork {

    @Override
    public void doWork() {
        System.out.println("写bug");
    }
}
public class WriteCode implements  DoWork{

    @Override
    public void doWork() {
        System.out.println("写程序");
    }
}

dubbo spi文件内容与java spi文件内容略微不太相同,可以理解为key-value的形式,value就是实现类的全路径名,这个key呢可以理解为这个类名的简称,这个要记得,下面会用得到:

bug=com.alibaba.dubbo.demo.provider.DubboSPI.WriteBug
code=com.alibaba.dubbo.demo.provider.DubboSPI.WriteCode
public static void main(String[] args) {        
    ExtensionLoader<DoWork> extensionLoader = ExtensionLoader.getExtensionLoader(DoWork.class);  
    //这个code就是上面说的"简称"       
      DoWork code = extensionLoader.getExtension("code");        
      code.doWork();       
      DoWork bug = extensionLoader.getExtension("bug");        
      bug.doWork();    
}

SPI

ExtensionLoader类包含了整个SPI的核心方法,包括像下面的获取@Adaptive、@Active注解的信息等,都是在这个类中实现的。

getExtensionLoader方法可以获得对应接口的Extension加载器,这个方法也是比较简单的;

@SuppressWarnings("unchecked")
    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        if (type == null)
            throw new IllegalArgumentException("Extension type == null");
        if (!type.isInterface()) {
            throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
        }
        if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type(" + type +
                    ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
        }

        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);//从缓存中拿
        if (loader == null) {
            //缓存中没有的话,就去new一个放到这个map中,然后再获取返回
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }

其实就是从缓存中拿,如果缓存中有,那么就取缓存中的,缓存中没有,那么就new一个放进去。

ok,现在获取到了扩展实现类加载器,接下来就看如何通过执行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<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) {
                    instance = createExtension(name);
                    holder.set(instance);
                }
            }
        }
        return (T) instance;
    }

代码行数虽然比较多,实际上都是从缓存中取,看是否有值,没有值的话就去创建。因为我们现在是首次加载,那肯定是没值的,所以就要到这个createExtension方法中:

private T createExtension(String name) {
        Class<?> clazz = getExtensionClasses().get(name);//获取名称对应的class
        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()) {
                for (Class<?> wrapperClass : wrapperClasses) {//对于那些wrapper类型的,进行包装一下
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }
            return instance;
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                    type + ")  could not be instantiated: " + t.getMessage(), t);
        }
    }

这里的核心方法就两个:getExtensionClasses、injectExtension。这两个方法在后面的@Adaptive、@Active注解中都会用到。

首先是这个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;
    }

可以看到他其实也是这个从缓存中拿,没有的话就创建,核心方法就是这个loadExtensionClasses方法。

private Map<String, Class<?>> loadExtensionClasses() {
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if (defaultAnnotation != null) {
            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];
            }
        }

        Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
        loadDirectory(extensionClasses, DUBBO_DIRECTORY);
        loadDirectory(extensionClasses, SERVICES_DIRECTORY);
        return extensionClasses;
    }

还记得最开始的时候说的dubbo会扫描三个目录下的文件么?没错就是这三个loadDirectory方法里面的目录。

最终调用的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("Error when load extension class(interface: " +
                    type + ", class line: " + clazz.getName() + "), class "
                    + clazz.getName() + "is not subtype of interface.");
        }
        if (clazz.isAnnotationPresent(Adaptive.class)) {//如果是自适应的话,只会有一个
            if (cachedAdaptiveClass == null) {
                cachedAdaptiveClass = clazz;
            } else if (!cachedAdaptiveClass.equals(clazz)) {
                throw new IllegalStateException("More than 1 adaptive class found: "
                        + cachedAdaptiveClass.getClass().getName()
                        + ", " + clazz.getClass().getName());
            }
        } else if (isWrapperClass(clazz)) {//包装类的话,可以放到set中,这个是可以有多个的。
            Set<Class<?>> wrappers = cachedWrapperClasses;
            if (wrappers == null) {
                cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
                wrappers = cachedWrapperClasses;
            }
            wrappers.add(clazz);
        } else {//说明既不是@Adaptive,也不是装饰类,他只是普通的或者是active。
            clazz.getConstructor();//如果没有默认构造,那么就报错
            if (name == null || name.length() == 0) {
                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 (names != null && names.length > 0) {
                Activate activate = clazz.getAnnotation(Activate.class);
                if (activate != null) {
                    cachedActivates.put(names[0], activate);
                }
                for (String n : names) {
                    if (!cachedNames.containsKey(clazz)) {
                        cachedNames.put(clazz, n);//设置class与名字的映射
                    }
                    Class<?> c = extensionClasses.get(n);
                    if (c == null) {
                        extensionClasses.put(n, clazz);//记录名字与类的映射
                    } else if (c != clazz) {
                        throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
                    }
                }
            }
        }
    }

在这里我给大家总结一下这个方法:先看这个类上面有没有@Adaptive注解。如果有@Adaptive注解,那么就将它缓存起来,原则上一个接口只允许有一个@Adaptive,这是为啥呢?我们可以看上面的代码,如果有多个@Adaptive注解的话,他是会抛出异常的。

然后再看这个接口的实现类是否有以Wrapper结尾的实现类,比如Protocol的其中一个实现类:ProtocolFilterWrapper,如果是的话,就会放到一个set集合中。如果这两种都不是的话,那么就是普通的扩展类,将他们存到一个map中返回就可以了。在存到这个map的时候,是以类对应的简写为key,类的路径为value。

IOC

我们可以先看下这个injectExtension中有这么一段方法:

    private T injectExtension(T instance) {
        try {
            if (objectFactory != null) {
                for (Method method : instance.getClass().getMethods()) {
                    if (method.getName().startsWith("set")
                            && method.getParameterTypes().length == 1
                            && Modifier.isPublic(method.getModifiers())) {
  /

可以看到,它是先获取到这个实例对应类中的全部方法,然后呢,依次循环遍历,对于那些以set开头、只有一个参数、并且为public的方法,我们就认为这是需要对这个实例进行属性注入的部分。

Class<?> pt = method.getParameterTypes()[0];
try {
  String property = method.getName().length() > 3 ? 		  method.getName().substring(3,4).toLowerCase() + method.getName().substring(4) : "";
  Object object = objectFactory.getExtension(pt, property);
  if (object != null) {
    method.invoke(instance, object);
  }
} 

​ 这个其实就是要获取到需要注入的属性,就是将第四个字符设置为小写,后面的不动进行分割。比如setUserName,分割完毕后就是userName,这样有属性,又有类路径,就可以通过反射对原先的实例进行属性注入,也就完成了依赖注入。

Aop

这里分为两部分:判断是否为包装类以及对目标对象进行增强。

什么是包装类?

举个简单的栗子:

public class Son implements Parent {
    private Parent parent;

    public Son(Parent parent) {
        this.parent = parent;
    }
}

在dubbo中是这个样子的:

在这里插入图片描述

也就是实现类的构造方法的参数,为他自己所实现的接口。而且在dubbo中。包装类都是以Wrapper结尾的。

缓存包装类

缓存包装类是在ExtensionLoader.loadClass中:

else if (isWrapperClass(clazz)) {//包装类的话,可以放到set中,这个是可以有多个的。
            Set<Class<?>> wrappers = cachedWrapperClasses;
            if (wrappers == null) {
                cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
                wrappers = cachedWrapperClasses;
            }
            wrappers.add(clazz);
        }



private boolean isWrapperClass(Class<?> clazz) {
        try {
            Constructor<?> constructor = clazz.getConstructor(type);//看类的构造参数中,有没有对应接口的参数,如果有,就说明是包装类
            return true;
        } catch (NoSuchMethodException e) {
            return false;
        }
    }

通过遍历所有的实现类,找出这些包装类,然后放入缓存中。

AOP增强

@SuppressWarnings("unchecked")
    private T createExtension(String name) {
        Map<String, Class<?>> extensionClasses = getExtensionClasses();
        Class<?> clazz = extensionClasses.get(name);//获取名称对应的class
        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()) {
                //遍历
                for (Class<?> wrapperClass : wrapperClasses) {
                    //对返回的对象进行包装增强
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }
            return instance;
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                    type + ")  could not be instantiated: " + t.getMessage(), t);
        }
    }

@Adaptive

@SPI注解那里存的是默认值,这个会在动态生成编译的类中,会设置默认值

从上面的例子中可以看到,通过SPi,我们可以每次只加载对应接口的实现类,这样可以减少加载全部实现类带来的开销。但是我又有一个问题了,我不想在启动项目的时候加载这些扩展类,而是希望在运行时根据参数,来动态加载扩展类,这个该怎么做呢?

此时@Adaptive注解就派上用场了。

@Adaptive有两种用法:一种是添加到类上,另一种则是添加到方法上。

  1. 如果添加到类上,表示该类是接口的适配器。但是实际上,注解添加到类上的是很少的,大部分都是添加到方法上的。目前只有AdaptiveExtensionFactory和AdaptiveCompiler是添加到类上的。

在这里我们就以这个AdaptiveExtensionFactory为例,来看下这个:

在这里插入图片描述

在这里插入图片描述

AdaptiveExtensionFactory的类上有这个@Adaptive注解,在执行构造方法的时候,就会将ExtensionLoader对应的实现类加载到一个List中缓存起来,这样的话在getExtension的时候,传入要取的class,key,就可以直接从这个list中取出对应的值。

2.事实上大部分情况下都是注解到方法上的,他会根据接口的信息,来动态拼接成一个代理类。为了更清楚了解@Adaptive的实现,我们就从源码的角度,以RegistryFactory为例子来看下这个过程到底是啥样的:

在这里插入图片描述

方法起始于ExtensionLoader.getAdaptiveExtension:

public T getAdaptiveExtension() {
       //从缓存中拿,没有的话就创建
       instance = createAdaptiveExtension();
      cachedAdaptiveInstance.set(instance);
    
        return (T) instance;
    }


 private T createAdaptiveExtension() {
     //主要方法为getAdaptiveExtensionClass
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    }


 private Class<?> getAdaptiveExtensionClass() {
        getExtensionClasses();//通过这个方法来获取到Adaptive注解的类。
        if (cachedAdaptiveClass != null) {//如果有,那么就说明有@Adaptive注解添加到类上了。
            return cachedAdaptiveClass;
        }
        return cachedAdaptiveClass = createAdaptiveExtensionClass();//没有的话就创建。注解在方法上的话,那么就需要进行编译。
    }

这里我省略了一些无关紧要的代码,保留了核心代码,getExtensionClasses这个方法是不是感觉很熟悉呢?没错,在上面我们也介绍过他的源码了,这里就不多说了,直接ctrl+f搜索就可以了。

因为这个接口上的@Adaptive是在接口上的,所以cachedAdaptiveClass是为空的,这里就需要执行createAdaptiveExtensionClass去创建实现类了。

private Class<?> createAdaptiveExtensionClass() {
        String code = createAdaptiveExtensionClassCode();
        ClassLoader classLoader = findClassLoader();
        com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
        return compiler.compile(code, classLoader);
    }

​ 这个code里面就是包含了代理实现类的所有信息,他长这个样子:

在这里插入图片描述

看起来不太直接,而且也没办法调试,我们可以将他复制下来,ctrl+alt+l进行格式化一下,这样在下次启动程序的时候,断点就可以进入这个类中了。

在这里插入图片描述

进入这个方法中,根据传入的URL中的参数值,来调用getExtension方法,最终得到需要的扩展类。

PS:在dubbo3.0的时候项目中就已经放了这些$Adaptive的类了。

一些需要注意的地方

  1. 当@Adaptive添加到方法上的时候,要保证方法中的参数至少有一个为URL。

    但是我们发现有的方法中的参数,只有一个invoker,比如这样的:

    在这里插入图片描述

    这个方法中就没有URL,那这是咋回事?

    通过查看拼接生成的代理类,我们发现,通过Invoker是可以得到URL的:

在这里插入图片描述

所以从某种意义上来说的话,填入了Invoker也相当于填入了URL。

  1. 关于SPI注解与@Adaptive注解中的值。

SPI注解后面的值是默认的值,当URL中的值为空的时候,就使用默认的值,比如上面图片中的:

String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());

这个dubbo就是默认值,也是@SPI中的值。

而@Adaptive添加到方法上时,后面的参数表示在加载时优先使用的值。

@Activate

这个注解是扩展点自动激活加载,在这个注解上可以设置group、value、order等值。

这些值主要表示注解在的类、方法上所属的分组、执行顺序等,当传入的参数符合直接中的值的时候,可以进行激活。

这个注解对应的源码,其实大部分在上面都已经说过了,总的来说,并不是特别的难,源码在这里,大家可以先看一下:

 public List<T> getActivateExtension(URL url, String[] values, String group) {
        List<T> exts = new ArrayList<T>();
        List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);
        if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
          //1
            getExtensionClasses();
            for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
                String name = entry.getKey();
                Activate activate = entry.getValue();
                if (isMatchGroup(group, activate.group())) {
                    if (!names.contains(name)
                            && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
                            && isActive(activate, url)) {
                      	//2
                        T ext = getExtension(name);
                        exts.add(ext);
                    }
                }
            }
            //排序
            Collections.sort(exts, ActivateComparator.COMPARATOR);
        }
        List<T> usrs = new ArrayList<T>();
        for (int i = 0; i < names.size(); i++) {
            String name = names.get(i);
            if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX)
                    && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) {
                if (Constants.DEFAULT_KEY.equals(name)) {
                    if (!usrs.isEmpty()) {
                        exts.addAll(0, usrs);
                        usrs.clear();
                    }
                } else {
                  //3
                    T ext = c(name);
                    usrs.add(ext);
                }
            }
        }
        if (!usrs.isEmpty()) {
            exts.addAll(usrs);
        }
        return exts;
    }

他的核心方法就是我在里面标记的这几个,其余的呢,都是循环啦,取值之类的操作,没有什么太多的复杂逻辑。

结尾

以上就是dubbo spi的核心内容了,因为spi贯穿了dubbo的所有核心功能,所以要想看懂dubbo的源码,那么就需要先了解SPI这个前置任务。大家可以一边对照着文章,一边打断点,这样对SPI也会了解更深入。如果文章对你有用,不妨点个赞再走哈,有问题也欢迎提出来指正。

在这里插入图片描述

最后

以上就是潇洒银耳汤为你收集整理的一文带你彻底了解Dubbo的SPI机制的全部内容,希望文章能够帮你解决一文带你彻底了解Dubbo的SPI机制所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(57)

评论列表共有 0 条评论

立即
投稿
返回
顶部