从源码分析Dubbo的SPI机制

Java SPI

在进行分析Dubbo SPI机制之前,我们还是从我们熟悉的java spi机制入手,其实在我们平常使用的开发框架中, 处处都是使用了SPI机制,比如我们使用的JDBC,日志框架等,我们可以根据配置集成我们需要的数据库例如mysql、oracle 等,下面从一个简单的例子来看一下Java  SPI;

Java Spi  demo:

public interface Tea {

    String getTeaName();

}

public class GreenTea implements Tea {

    @Override
    public String getTeaName() {
        return "green";
    }
}

public class RedTea implements Tea {

    @Override
    public String getTeaName() {
        return "red";
    }
}



import java.util.ServiceLoader;

public class JavaSpiTest {

    public static void main(String[] args) {
        ServiceLoader<Tea> load = ServiceLoader.load(Tea.class);
        for (Tea tea : load) {
            System.out.println(tea.getTeaName());
        }
    }
}

还有一个最重要的文件:文件路径为 resources/MET-INF/services。文件名称为接口的全限定名,在本demo中就是com.tuling.javaspi.Tea。文件内容如下:

com.tuling.javaspi.GreenTea
com.tuling.javaspi.RedTea

这样我们在文件中写入Tea接口的具体实现类,就可以得到不同的实现,所以JDBC只需要暴露一个标准接口,具体的实现有各自的厂商自定义。然后用户项目中进行对应jar的和配置即可。这种 机制给各个框架提供了非常好可 扩展机制。

缺点

通过上面的代码我们会发现一个缺点,就是我们不能实现按需加载,也就说系统会一次性将文件中的所有的实现类都实例化了,这在性能和资源上都是一种浪费。

思考:我们如何实现按需加载呢?

思路:如果我们在文件中配置成key=value的规则,但时候我们在获取的时候,传入key 就可以实现了。

Dubbo SPI

熟悉Dubbo的童鞋们都知道,Dubbo提供了极高的可扩展机制,所以说SPI贯穿了整个dubbo的核心 。

下面我们先来从一张图整体认识一下,Dubbo的SPI机制整体源码流程图:

 上图是小编在读SPI机制源码的时候根据自己的总结画出的源码中核心的方法流转图,各位小可爱可以根据上面的流程图阅读源码,效率会更高一些。

SPI核心代码详解

在我们看Dubbo的源码的时候经常看到上面的这种代码的写法,他们分别对应自适应扩展点(默认扩展点)、指定名称的扩展点、激活扩展点,在这里我们先对这些概念有一个认识,后面我们会详细介绍

ExtensionLoader.getExtensionLoader(xxx.class).getAdaptiveExtension();
ExtensionLoader.getExtensionLoader(xxx.class).getExtension(name);
ExtensionLoader.getExtensionLoader(xxx.class).getActivateExtension(url, key);

例如:

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

Protocol接口,在运行的时候dubbo会判断一下应该选用这个Protocol接口的哪个实现类来实例化对象。

它会去找你配置的Protocol,将你配置的Protocol实现类加载到JVM中来,然后实例化对象,就用你配置的那个Protocol实现类就可以了。

上面那行代码就是dubbo里面大量使用的,就是对很多组件,都是保留一个接口和多个实现,然后在系统运行的时候动态的根据配置去找到对应的实现类。如果你没有配置,那就走默认的实现类。


@SPI("dubbo")  
public interface Protocol {  
      
    int getDefaultPort();  
  
    @Adaptive  
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;  
  
    @Adaptive  
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;  

    void destroy();  
  
} 

在dubbo自己的jar中,在META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol文件中:

filter=org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=org.apache.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=org.apache.dubbo.rpc.support.MockProtocol
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
injvm=org.apache.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=org.apache.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=org.apache.dubbo.rpc.protocol.hessian.HessianProtocol
http=org.apache.dubbo.rpc.protocol.http.HttpProtocol

org.apache.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=org.apache.dubbo.rpc.protocol.thrift.ThriftProtocol
native-thrift=org.apache.dubbo.rpc.protocol.nativethrift.ThriftProtocol
memcached=org.apache.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=org.apache.dubbo.rpc.protocol.redis.RedisProtocol
rest=org.apache.dubbo.rpc.protocol.rest.RestProtocol
xmlrpc=org.apache.dubbo.xml.rpc.protocol.xmlrpc.XmlRpcProtocol
registry=org.apache.dubbo.registry.integration.RegistryProtocol
qos=org.apache.dubbo.qos.protocol.QosProtocolWrapper

所以这就看到了dubbo的SPI机制默认是怎么玩的了,其实就是Protocol接口,@SPI(“dubbo”) 说的是,通过 SPI 机制来提供实现类,实现类是通过 dubbo 作为默认 key 去配置文件里找到的,配置文件名称与接口全限定名一样的,通过 dubbo 作为 key 可以找到默认的实现类就是 org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

如果想要动态替换掉默认的实现类,需要使用 @Adaptive 接口,Protocol 接口中,有两个方法加了 @Adaptive 注解,就是说那俩接口会被代理实现。

比如这个 Protocol 接口搞了俩 @Adaptive 注解标注了方法,在运行的时候会针对 Protocol 生成代理类,这个代理类的那俩方法里面会有代理代码,代理代码会在运行的时候动态根据 url 中的 protocol 来获取那个 key,默认是 dubbo,你也可以自己指定,你如果指定了别的 key,那么就会获取别的实现类的实例了。

核心类、核心接口

ExtensionLoader

ExtensionLoader表示某个接口的扩展点加载器,可以用来加载某个扩展点的实例。在ExtensionLodader中 除开有有几个非常重要的属性

    1、Class<?> type:表示当前ExtensionLoader实例是哪个接口的扩展点加载器

    2、ExtensionFactory objectFactory:扩展点工厂(对象工厂),可以获得某个对象

ExtensionLoader和ExtensionFactory的区别在于:

  1. ExtensionLoader最终所得到的对象是Dubbo SPI机制产生的
  2. ExtensionFactory最终所得到的对象可能是Dubbo SPI机制所产生的,也可能是从Spring容器中所获得的对象

前面我们介绍了三种获得扩展点的方式,我们现在来介绍另一个非常重要的方法:

createExtension(String name)方法

在调用createExtension(String name)方法去创建一个扩展点实例时,要经过以下几个步骤:

  1. 根据name找到对应的扩展点实现
  2. 根据实现类生成一个实例,把实现类和对应生成的实例进行缓存
  3. 对生成出来的实例进行依赖注入(给实例的属性进行赋值)
  4. 对依赖注入后的实例进行AOP(Wrapper),把当前接口类的所有的Wrapper全部一层一层包裹在实例对象上,没包裹个Wrapper后,也会对Wrapper对象进行依赖注入
  5. 返回最终的Wrapper对象

                      

getExtensionClasses()方法

getExtensionClasses()是用来加载当前接口所有扩展点实现类的,返回一个Map。之后可以从这个map中按照指定的那么获取对应的扩展点实现类。

当把当前接口的所有实现类都记载出来以后也会进行缓存,下次需要的时候直接从缓存中拿。

Dubbo在加载一个接口的扩展点时,思路是这样的:

1、根据接口的全限定名去META-INF/dubbo/internal/目录下寻找对应的文件,调用loadResource方法进行加载

2、根据接口的全限定名去META-INF/dubbo/目录下寻找对应的文件,调用loadResource方法进行加载

3、根据接口的全限定名去META-INF/services/目录下寻找对应的文件,调用loadResource方法进行加载

 loadResource方法

loadResource方法就是完成对文件内容的解析,按行进行解析,回解析出“=”两边的内容,“=”左边的内容就是扩展点的name,右边的内容就是扩展点实现类,并且会利用ExtensionLoader类的类加载器来加载扩展点实现。然后调用loadClass方法对name和扩展点实例进行详细的解析,并且最终把他们放到map中。

loadClass()方法 

loadClass方法会做下面几件事情:

1、当前扩展点实现类上是否存在@Adaptive注解,如果存在则把类认为是当前接口的默认自适应类(接口代理类),并把该类存到cachedAdaptiveClass属性上。

2、当前扩展点实现是否一个当前接口的Wrapper类,如何判断是否是Wrapper类?就是看当前类中是否存在一个构造方法,该构造方法只有一个参数,参数类型就为接口类型,如果存在这一个这样的构造方法,那么这个类就是该接口类的Wrapper类,如果是,则把该类添加到cachedWrapperClasses中去,cachedWrapperClasses是一个set。

3、如果不是自适应类,或者也不是Wrapper类,则判断时是否存在name,如果不存在name,那么就会报错。

4、如果有多个name,则判断一下当前扩展点实现类上是否存在@Adaptive注解, 如果存在则把该类添加到cachedActivates中,cachedWrapperClasses是一个map。

5、最后,遍历多个name,把每个name和对应的实现类存到extensionClasses中去,extensionClasses就是上文所提到的map。

至此,加载类就走完了。

回到createExtension(String name)方法中的逻辑,当前这个接口的所有扩展点实现类都扫描完了之后,就可以根据用户所指定的名字,找到对应的实现类了,然后进行实例化,然后进行IOC(依赖注入)和AOP。

Dubbo中的IOC

1、根据当前 实例的类,找到这个类中的setter方法,进行依赖注入

2、先分析出setter()方法的参数类型pt

3、在截取出setter方法所对应的属性名property

4、调用objectFactory.getExtension(pt, property)得到一个对象,这里就会从Spring容器 或通过DubboSpi机制得到一个对象,比较特殊的是,如果是通过DubboSpi机制得到的对象,是pt这个类型的一个自适应对象(代理对象)。

5、再反射调用setter方法进行注入。

Dubbo中的AOP

dubbo中也实现了一套非常简单的AOP,就是利用Wrapper,如果一个接口的扩展点中包含了多个Wrapper类,那么在实例化完某个扩展点后,就会利用这些Wrapper类对这个实例进行包裹,比如:现在有一个DubboProtocol的实例,同时对于Protocol这个接口还有很多的Wrapper,比如ProtocolFilterWrapper、ProtocolListenerWrapper,那么,当对DubboProtocol的实例完成了IOC之后,就会先调用new ProtocolFilterWrapper(DubboProtocol实例)生成一个新的Protocol的实例,再对此实例进行IOC,完了之后,会再调用new ProtocolListenerWrapper(ProtocolFilterWrapper实例)生成一个新的Protocol的实例,然后进行IOC,从而完成DubboProtocol实例的AOP。

小结

至此,Dubbo中关于SPI机制实现的基本原理都介绍完了,其中流程图中涉及到的核心方法都进行了解析,当然源码中还有一些其他的实现方法,感兴趣的读者可以继续进行分析。

猜你喜欢

转载自blog.csdn.net/u013045437/article/details/113630289