zoukankan      html  css  js  c++  java
  • 搞懂Dubbo SPI可拓展机制

    前言

      阅读本文需要具备java spi的基础,本文不讲java spi,please google it.

    一.Dubbo SPI 简介

      SPI(Service Provider Interface)是服务发现机制,Dubbo没有使用jdk SPI而对其增强和扩展:

    1. jdk SPI仅通过接口类名获取所有实现,但是Duboo SPI可以根据接口类名和key值获取具体一个实现
    2. 可以对扩展类实例的属性进行依赖注入,即IOC
    3. 可以采用装饰器模式实现AOP功能

      你可以发现Dubbo的源码中有很多地方都用到了@SPI注解,例如:Protocol(通信协议),LoadBalance(负载均衡)等。基于Dubbo SPI,我们可以非常容易的进行拓展。ExtensionLoader是扩展点核心类,用于载入Dubbo中各种可配置的组件,比如刚刚说的Protocol和LoadBalance等。那么接下来我们看一下Dubbo SPI的示例

    二.Dubbo SPI 示例

      比如现在我们要拓展Protocol这个组件,新建一个DefineProtocol类并修改默认端口为8888:

     1 /**
     2  * @author GrimMjx
     3  */
     4 public class DefineProtocol implements Protocol {
     5     @Override
     6     public int getDefaultPort() {
     7         return 8888;
     8     }
     9 
    10     @Override
    11     public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
    12         return null;
    13     }
    14 
    15     @Override
    16     public <T> Invoker<T> refer(Class<T> aClass, URL url) throws RpcException {
    17         return null;
    18     }
    19 
    20     @Override
    21     public void destroy() {
    22 
    23     }
    24 }

      配置文件的文件名字是接口的全限定名,那么在这个例子中就是:com.alibaba.dubbo.rpc.Protocol

      Dubbo SPI所需的配置文件要放在以下3个目录任意一个中:

      META-INF/services/

      META-INF/dubbo/

      META-INF/dubbo/internal/

      同时需要将服务提供者配置文件设计成KV键值对的形式,Key是拓展类的name,Value是扩展的全限定名实现类。比如:

    myProtocol=com.grimmjx.edu.DefineProtocol

      然后测试一下:

    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("dubbo-client.xml");
    Protocol myProtocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("myProtocol");
    System.out.println(myProtocol.getDefaultPort());

      结果如下:

    三.源码解析

      那我们就从上面的方法看起,重要方法红色标注:

    ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("myProtocol");

    1.getExtensionLoader方法,入参是一个可拓展的借口,返回ExtensionLoader实体类,然后可以通过name(key)来获取具体的扩展:

     1 public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
     2     if (type == null) {
     3         throw new IllegalArgumentException("Extension type == null");
     4     }
     5     // 扩展点必须是接口
     6     if (!type.isInterface()) {
     7         throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
     8     }
     9     // 必须有@SPI注解
    10     if (!withExtensionAnnotation(type)) {
    11         throw new IllegalArgumentException("Extension type (" + type +
    12                 ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
    13     }
    14     // 每个扩展只会被加载一次
    15     ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    16     if (loader == null) {
    17         // 初始化扩展
    18         EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
    19         loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    20     }
    21     return loader;
    22 }

    2.getExtension方法,首先检查缓存,如果没有则用双检锁方式创建实例:

     1 public T getExtension(String name) {
     2     if (StringUtils.isEmpty(name)) {
     3         throw new IllegalArgumentException("Extension name == null");
     4     }
     5     if ("true".equals(name)) {
     6         // 默认拓展实现类
     7         return getDefaultExtension();
     8     }
     9     // 获取持有目标对象
    10     Holder<Object> holder = getOrCreateHolder(name);
    11     Object instance = holder.get();
    12     // 双检锁
    13     if (instance == null) {
    14         synchronized (holder) {
    15             instance = holder.get();
    16             if (instance == null) {
    17                 // 创建实例
    18                 instance = createExtension(name);
    19                 holder.set(instance);
    20             }
    21         }
    22     }
    23     return (T) instance;
    24 }

    3.createExtension方法,这个方法比较核心。做了有4件事情,第3件和第4件分别为上面介绍Dubbo SPI中对jdk SPI扩展的第二和第三点(红字已标注)。请看代码注释:

     1 private T createExtension(String name) {
     2     // 1.加载配置文件所有拓展类,得到配置名-拓展类的map,从map中获取到拓展类
     3     Class<?> clazz = getExtensionClasses().get(name);
     4     if (clazz == null) {
     5         throw findException(name);
     6     }
     7     try {
     8         T instance = (T) EXTENSION_INSTANCES.get(clazz);
     9         if (instance == null) {
    10             // 2.通过反射创建实例
    11             // EXTENSION_INSTANCES这个map是配置名-拓展类实例的map
    12             EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
    13             instance = (T) EXTENSION_INSTANCES.get(clazz);
    14         }
    15         // 3.注入依赖,即IOC
    16         injectExtension(instance);
    17         Set<Class<?>> wrapperClasses = cachedWrapperClasses;
    18         if (CollectionUtils.isNotEmpty(wrapperClasses)) {
    19             // 4.循环创建Wrapper实例
    20             for (Class<?> wrapperClass : wrapperClasses) {
    21                 // 通过反射创建Wrapper实例
    22                 // 向Wrapper实例注入依赖,最后赋值给instance
    23                 // 自动包装实现类似aop功能
    24                 instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
    25             }
    26         }
    27         return instance;
    28     } catch (Throwable t) {
    29         throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
    30                 type + ") couldn't be instantiated: " + t.getMessage(), t);
    31     }
    32 }

    4.getExtensionClasses方法,这里就是找出所有拓展类,返回一个配置名-拓展类的map。

     1 private Map<String, Class<?>> getExtensionClasses() {
     2     Map<String, Class<?>> classes = cachedClasses.get();
     3     // 双检锁
     4     if (classes == null) {
     5         synchronized (cachedClasses) {
     6             classes = cachedClasses.get();
     7             if (classes == null) {
     8                 // 缓存无则加载
     9                 classes = loadExtensionClasses();
    10                 cachedClasses.set(classes);
    11             }
    12         }
    13     }
    14     return classes;
    15 }

    5.loadExtensionClasses方法,主要就是解析SPI注解,然后加载指定目录的配置文件,也不是很难

     1 private Map<String, Class<?>> loadExtensionClasses() {
     2     // 获取SPI注解,检查合法等
     3     final SPI defaultAnnotation = type.getAnnotation(SPI.class);
     4     if(defaultAnnotation != null) {
     5         String value = defaultAnnotation.value();
     6         if(value != null && (value = value.trim()).length() > 0) {
     7             String[] names = NAME_SEPARATOR.split(value);
     8             if(names.length > 1) {
     9                 throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
    10                         + ": " + Arrays.toString(names));
    11             }
    12             if(names.length == 1) cachedDefaultName = names[0];
    13         }
    14     }
    15     
    16     Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    17     // META-INF/dubbo/internal/目录
    18     loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    19     // META-INF/dubbo/目录
    20     loadFile(extensionClasses, DUBBO_DIRECTORY);
    21     // META-INF/services/目录
    22     loadFile(extensionClasses, SERVICES_DIRECTORY);
    23     return extensionClasses;
    24 }

      这里返回的extensionClasses的map就肯定包含了"myProtocol"->"com.grimmjx.edu.DefineProtocol"。同时也可以看到,dubbo支持有很多协议:

      接下来不用多说了吧,再从map里get出"myProtocol",得到的就是我们自定义的协议类。

      上面3.createExtension方法,这个方法里注释的3和4很关键,里面实现了依赖注入和AOP的功能,那么接下来我们主要看看Dubbo的IOC和AOP

    Dubbo IOC

       Dubbo IOC是通过setter方法注入依赖的,首先遍历方法是否有setter方法特征,如果有则通过objectFactory获取依赖对象进行注入。Dubbo注入的可能是Dubbo的扩展,也有可能是一个Spring bean!

      上面的3方法中有这一行代码,实现了Dubbo SPI的IOC

    injectExtension(instance);

      1.injectExtension方法。我们主要看这个方法,自动装配的功能都在这个方法中:

     1 private T injectExtension(T instance) {
     2     try {
     3         if (objectFactory != null) {
     4             for (Method method : instance.getClass().getMethods()) {
     5                 // 如果方法以set开头 && 只有一个参数 && 方法是public级别的
     6                 if (method.getName().startsWith("set")
     7                         && method.getParameterTypes().length == 1
     8                         && Modifier.isPublic(method.getModifiers())) {
     9                     // 获取setter方法参数类型
    10                     Class<?> pt = method.getParameterTypes()[0];
    11                     try {
    12                         String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
    13 
    14                         // 关键,从objectFactory里拿出依赖对象
    15                         Object object = objectFactory.getExtension(pt, property);
    16                         if (object != null) {
    17                             // 利用反射进行注入
    18                             method.invoke(instance, object);
    19                         }
    20                     } catch (Exception e) {
    21                         logger.error("fail to inject via method " + method.getName()
    22                                 + " of interface " + type.getName() + ": " + e.getMessage(), e);
    23                     }
    24                 }
    25             }
    26         }
    27     } catch (Exception e) {
    28         logger.error(e.getMessage(), e);
    29     }
    30     return instance;
    31 }

      2.objectFactory,究竟这里的objectFactory是什么呢?它是ExtensionFactory类型的,自身也是一个扩展点。我先告诉你这里的objectFactory是AdaptiveExtensionFactory。后面会有解释。

    1 private ExtensionLoader(Class<?> type) {
    2     this.type = type;
    3     objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    4 }

      3.getExtension方法,这里比较简单,为了直观,用debug模式看一下,factories有两个,分别是SpiExtensionFactory和SpringExtensionFactory

      对于SpringExtensionFactory就是从工厂里根据beanName拿到Spring bean来注入,对于SpiExtensionFactory就是根据传入Class获取自适应拓展类,那么我们写一段代码,来试试获取一个Spring Bean玩玩,先定义一个bean:

     1 /**
     2  * @author GrimMjx
     3  */
     4 public class Worker {
     5 
     6     private int age = 24;
     7 
     8     public int getAge() {
     9         return age;
    10     }
    11 
    12     public void setAge(int age) {
    13         this.age = age;
    14     }
    15 }

      然后再配置文件里配置这个Spring Bean:

      最后简单写个Main方法,可以看出SpringExtensionFactory可以加载Spring Bean:

     

    Dubbo AOP

      Dubbo中也支持Spring AOP类似功能,通过装饰者模式,使用包装类包装原始的扩展点实例。在扩展点实现前后插入其他逻辑,实现AOP功能。说这很绕口啊,那什么是包装类呢?举个例子你就知道了:

     1 class A{
     2     private A a;
     3     public A(A a){
     4         this.a = a;
     5     }
     6 
     7     public void do(){
     8         // 插入扩展逻辑
     9         a.do();
    10     }
    11 }

      这里的插入扩展逻辑,是不是就是实现了AOP功能呢?比如说Protocol类,有2个Wrapper,分别是ProtocolFilterWrapper和ProtocolListenerWrapper,我们可以在dubbo-rpc/dubbo-rpc-api/src/main/resources/META-INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol看到:

    filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
    listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper

      源码的话createExtension方法里的注释已经写的很清楚了,这里可以自行研究。所以我们在最开始的Dubbo SPI的例子中,我们打个断点就很明显了,得到的myProtocol对象其实是这样的

      如果你调用export方法的话,会先经历ProtocolFilterWrapper的export方法,再经历ProtocolListenerWrapper的export方法,这样是不是就实现了Spring AOP的功能呢?

    Dubbo SPI 自适应

      这里getAdaptiveExtension到底获取的是什么呢,这里涉及到SPI 自适应扩展,十分重要,涉及到@Adaptive注解。

      如果注解加在类上,比如说com.alibaba.dubbo.common.extension.factory.AdaptiveExtensionFactory(自行验证):直接加载当前的适配器

      如果注解加载方法上,比如说com.alibaba.dubbo.rpc.Protocol:动态创建一个自适应的适配器,就像是执行如下代码,返回的是一个动态生成的代理类

    Protocol adaptiveExtension = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
     1 public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
     2     public void destroy() {
     3         throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
     4     }
     5 
     6     public int getDefaultPort() {
     7         throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
     8     }
     9 
    10     public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) {
    11         if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
    12         if (arg0.getUrl() == null)
    13             throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
    14         com.alibaba.dubbo.common.URL url = arg0.getUrl();
    15         String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
    16         if (extName == null)
    17             throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
    18         com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
    19         return extension.export(arg0);
    20     }
    21 
    22     public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) {
    23         if (arg1 == null) throw new IllegalArgumentException("url == null");
    24         com.alibaba.dubbo.common.URL url = arg1;
    25         String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
    26         if (extName == null)
    27             throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
    28         com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
    29         return extension.refer(arg0, arg1);
    30     }
    31 }

      为什么getDefaultPort和destroy方法都是直接抛出异常呢?因为Protocol接口只有export和refer方法使用了@Adaptive注解,Dubbo会自动生成自适应实例,其他方法都是抛异常。

      为什么还要要动态生成呢?有时候拓展不像在框架启动的时候被加载,而是希望在扩展方法被调用的时候,根据运行时参数进行加载。

      最后看看上面2个标红的代码,是不是就是本文开始的源码分析,是不是很简单了?

     参考

    http://dubbo.apache.org/zh-cn/docs/source_code_guide/dubbo-spi.html(官方文档,稳稳的)

    https://blog.51cto.com/13679539/2125211

  • 相关阅读:
    AC日记——[HNOI2008]GT考试 bzoj 1009
    AC日记——[SCOI2009]游戏 bzoj 1025
    AC日记——[HNOI2010]BOUNCE 弹飞绵羊 洛谷 P3203
    AC日记——旅游 bzoj 2157
    NOIP模拟2017.6.11解题报告
    AC日记——【模板】Link Cut Tree 洛谷 P3690
    AC日记——[SDOI2010]大陆争霸 洛谷 P3690
    [NOI2010]超级钢琴 倍增
    [HNOI2004]L语言 字典树 记忆化搜索
    对拍
  • 原文地址:https://www.cnblogs.com/GrimMjx/p/10970643.html
Copyright © 2011-2022 走看看