Dubbo源码分析系列-扩展机制的实现

Posted qq418517226

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Dubbo源码分析系列-扩展机制的实现相关的知识,希望对你有一定的参考价值。

Spring可扩展Schema

像标签dubbo:monitor、dubbo:service、dubbo:provider等怎么读的,读完之后怎么又是怎么解析的呢?

这里写图片描述

以上标签都是基于Spring可扩展Schema提供的自定义配置

下面举个例子

1)创建JavaBean

首先设计好配置项,通过JavaBean来建模,如类People

public class People {
    private String name; 
    private Integer age;
}

2)编写XSD文件

XSD文件是XML的结构化定义,Spring提供了可扩展的XSD,为上一步设计好的配置项编写XSD文件,people.xsd文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema
xmlns="http://blog.csdn.net/418517226/schema/people" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:beans="http://www.springframework.org/schema/beans"      targetNamespace="http://blog.csdn.net/418517226/schema/people">
<xsd:import namespace="http://www.springframework.org/schema/beans" /> 
  <xsd:element name="people">
    <xsd:complexType>
      <xsd:complexContent>
        <xsd:extension base="beans:identifiedType">                    
          <xsd:attribute name="name" type="xsd:string" /> 
          <xsd:attribute name="age" type="xsd:int" />
        </xsd:extension> 
      </xsd:complexContent>
    </xsd:complexType>
  </xsd:element>
</xsd:schema>

完成后需把xsd文件放在classpath下,一般都放在META-INF目录下

3)编写NamespaceHandler和BeanDefinitionParser完成解析工作

NamespaceHandler会根据XSD文件具体的element找到某个BeanDefinitionParser,然后由BeanDefinitionParser完成具体的解析工作。因此需要分别完成 NamespaceHandler 和 BeanDefinitionParser 的实现类,Spring提供可默认实现类 NamespaceHandlerSupport 和AbstractSingleBeanDefinitionParser,简单的方式就是去继承这两个类,如下:

public class MyNamespaceHandler extends
     NamespaceHandlerSupport {
     public void init() {
         registerBeanDefinitionParser("people", new  
            PeopleBeanDefinitionParser());//people是一个element
     }
}

其中registerBeanDefinitionParser(“people”, new PeopleBeanDefinitionParser()),就是用来把element和解析类联系起来,具体看下类PeopleBeanDefinitionParser:

public class PeopleBeanDefinitionParser extends
         AbstractSingleBeanDefinitionParser {
   protected Class getBeanClass(Element
                  element) { return People.class;}
   protected void doParse(Element element,   
               BeanDefinitionBuilder bean) {
       String name=element.getAttribute("name"); 
       String age = element.getAttribute("age");

       if (StringUtils.hasText(name)) {   
          bean.addPropertyValue("name", name);
       }
       if (StringUtils.hasText(age)) {
             bean.addPropertyValue("age",           
                                Integer.valueOf(age))
       }
   }
}

4)编写 spring.handlers 和 spring.schemas 串联起所有部件

上面几个步骤走下来会发现开发好的 NamespaceHandler 与 xsd文件还没有联系,spring提供了spring.handlers 和 spring.schemas 这 两个配置文件来完成这项工作,这两个文件需要我们自己编写并放入 META-INF 文件夹中, 这两个文件的地址必须是 META-INF/spring.handlers 和 META-INF/spring.schemas,spring会默认去载入它们

spring.handlers文件内容如下所示:
http://blog.csdn.net/418517226/schema/people=com.MyNamespaceHandler

spring.schemas文件内容如下所示:
http://blog.csdn.net/418517226/schema/people.xsd=META-INF/people.xsd

5)在Bean文件中应用

使用方法和配置一个普通的bean 类似,只不过需要基于我们自定义schema,如下:

<beans
xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:418517226="http://blog.csdn.net/418517226/schema/people" 
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.x sd
http://blog.csdn.net/418517226/schema/people http://blog.csdn.net/418517226/schema/people.xsd">

      <418517226:people id="p" name="168" age="18"/> 

</beans>

因此,dubbo的配置解析也类似以上过程,会有dubbo.xsd,DubboNamespaceHandler等

public class DubboNamespaceHandler extends
NamespaceHandlerSupport {

    static {
        Version.checkDuplicate(DubboNamespaceHandler.class);
    }

    public void init() {
        registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
        registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
        registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
        registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
        registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
        registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
        registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
        registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
        registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
        registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
    }

}

像dubbo:service interface=”xxx”,ref=”xxx” 中的interface、ref等参数都是在XXXConfig中的

SPI扩展机制

站在一个框架作者的角度来说,定义一个接口,自己默认给出几个接口的实现类,同时允许框架的使用者也能自定义接口的实现。现在就有一个问题:如何优雅的根据一个接口来获取该接口的所有实现类呢?

  • 我自己想出的一个方案,在服务端使用工厂模式,通过客户端传过来的URL参数,根据参数动态实例化对象(好像比较复杂,有点像Spring,欢迎讨论)
  • Dubbo给出的方案,Java SPI机制

Java SPI介绍

首先看下Java SPI机制,网上一搜一大推,具体可以参考这篇博客Java SPI机制简介

Java SPI缺点分析

  • 虽然ServiceLoader也算是使用的延迟加载,但是接口的实现类会被全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。
  • 获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。

Dubbo扩展机制

dubbo的扩展机制和java的SPI机制非常相似,但是又增加了如下功能:

  • 可以方便的获取某一个想要的扩展实现,由注解@SPI提供默认实现

  • 对扩展实现IOC依赖注入功能:
    举例来说:接口A,实现者A1、A2;接口B,实现者B1、B2
    现在实现者A1含有setB()方法,会自动注入一个接口B的实现者,此时注入B1还是B2呢?都不是,而是注入一个动态生成的接口B的实现者B$Adpative,该实现者能够根据参数的不同,自动引用B1或者B2来完成相应的功能

  • 对扩展采用装饰器模式进行功能增强,类似AOP实现的功能
    还是上面的例子,接口A的另一个实现者AWrapper1,我们在获取某一个接口A的实现者A1的时候,已经被AWrapper1包装了

Dubbo的ExtensionLoader扩展解析过程

以下面的例子来分析:

ExtensionLoader<Protocol> protocolLoader=ExtensionLoader.getExtensionLoader(Protocol.class);

其中Protocol接口定义如下:

@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();

}

对应的实现者如下:
这里写图片描述

根据要加载的接口(Protocol)创建出一个ExtensionLoader实例

ExtensionLoader中含有一个静态属性:

private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();

用于缓存所有的扩展加载实例,这里加载的Protocol.class为key

我们先来看下,ExtensionLoader实例是如何来加载Protocol的实现类的:

  • 先解析Protocol上的注解SPI,存至String cachedDefaultName属性中,作为默认的实现
  • 到类路径下加载 META-INF/dubbo.internal/com.alibaba.dubbo.rpc.Protocol文件
    这里写图片描述
    这里写图片描述

该文件内容如下:
这里写图片描述
都是key/value形式,@SPI(“dubbo”)中的dubbo就是这的key

然后读取每一行内容,加载对应的class对象

  • 对于上述class分成三种情况来处理(约定)

    对于一个接口的实现者,ExtensionLoader分三种情况来分别存储对应的实现者,属性分别如下:

Class<?> cachedAdaptiveClass; 
Set<Class<?>> cachedWrapperClasses;
Reference<Map<String, Class<?>>> cachedClasses;

情况1:如果这个class含有Adaptive注解,则将这个class设置为 cachedAdaptiveClass。

情况2:尝试获取带对应接口参数的构造器,如果能够获取到,则说明这个class是一个装饰类,需要存到cachedWrapperClasses中

情况3:如果没有上述构造器。则将文件中的key作为当前key,存至cachedClasses结构中

如DubboProtocol,没有Adaptive注解,同时只有无参构造器,所以只能存放到cachedClasses中

如ProtocolFilterWrapper,含有Protocol参数的构造器,作为一个装饰类,存放至cachedWrapperClasses中

至此,解析文件过程结束

Dubbo的ExtensionLoader扩展获取过程

以获取DubboProtocol为例

ExtensionLoader<Protocol> protocolLoader=ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol dubboProtocol=protocolLoader.getExtension(DubboProtocol.NAME);

获取过程如下getExtension->createExtension:

private T createExtension(String name) {
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = injectExtension((T) clazz.newInstance());
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (wrapperClasses != null && wrapperClasses.size() > 0) {
            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);
    }
}

大致分成4步:

  1. 根据name获取对应的class,首先获取ExtensionLoader(Protocol)对象的cachedClasses属性中的DubboProtocol.class
  2. 根据获取到的class创建一个实例
  3. 对获取的实例,进行依赖注入
  4. 对于上述经过依赖注入的实例,再次进行包装。即遍历cachedWrapperClasses中每一个包装类,分别调用带Protocol参数的构造函数创建出实例,然后同样进行依赖注入
    以Protocol为例,cachedWrapperClasses中存着上述提到过的ProtocolFilterWrapper、ProtocolListenerWrapper。分别会对DubboProtocol实例进行包装,这个比较好理解的

下面对于依赖注入的过程就要详细说明下,来看下这个过程:

private T injectExtension(T instance) {
    try {
        for (Method method : instance.getClass().getMethods()) {
            if (method.getName().startsWith("set")
                    && method.getParameterTypes().length == 1
                    && Modifier.isPublic(method.getModifiers())) {
                Class<?> pt = method.getParameterTypes()[0];
                if (pt.isInterface() && getExtensionLoader(pt).getSupportedExtensions().size() > 0) {
                    try {
                        Object adaptive = getExtensionLoader(pt).getAdaptiveExtension();
                        method.invoke(instance, adaptive);
                    } catch (Exception e) {
                        logger.error("fail to inject via method " + method.getName()
                                + " of interface " + type.getName() + ": " + e.getMessage(), e);
                    }
                }
            }
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;
}

从上面可以看到,进行注入的条件如下(很多都是约定,针对该实例的方法来判断的):

  • set开头的方法
  • 方法的参数只有一个
  • 方法必须是public
  • 方法的参数必须是接口,并且是ExtensionLoader能够获取其扩展类

我们知道一个接口的实现者可能有多个,此时到底注入哪一个呢?

此时采取的策略是,并不去注入一个具体的实现者,而是注入一个动态生成的实现者,这个动态生成的实现者的逻辑是确定的,能够根据不同的参数来使用不同的实现者实现相应的方法。这个动态生成的实现者就是根据ExtensionLoader的cachedAdaptiveClass对象由getAdaptiveExtension()方法生成的instance

以Protocol为例,动态生成的Protocol实现者instance大概如下:

class Protocol$Adpative implements Protocol{
    public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.RpcException{
        if (arg0 == null)  { 
            throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null"); 
        }
        if (arg0.getUrl() == null) { 
            throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null"); 
        }
        com.alibaba.dubbo.common.URL url = arg0.getUrl();
        String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
        if(extName == null) {
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])"); 
        }
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)com.alibaba.dubbo.common.ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }

    public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0,com.alibaba.dubbo.common.URL arg1) throws com.alibaba.dubbo.rpc.RpcException{
        if (arg1 == null)  { 
            throw new IllegalArgumentException("url == null"); 
        }
        com.alibaba.dubbo.common.URL url = arg1;
        String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
        if(extName == null) {
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])"); 
        }
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)com.alibaba.dubbo.common.ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.refer(arg0, arg1);
    }

    public void destroy(){
        throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }
}

从上面的代码中可以看到,Protocol$Adpative是根据URL参数中protocol属性的值来选择具体的实现类的。

如值为dubbo,则从ExtensionLoader(Protocol)中获取dubbo对应的实例,即DubboProtocol实例

对于上述获取动态实现者即Protocol$Adpative的过程还需要补充一些细节内容:

  • 要求被依赖注入的接口中的某些方法必须含有Adaptive注解,没有Adaptive注解,则表示不需要生成动态类
  • 对于接口的方法中不含Adaptive注解的,全部是不可调用的,如上述的destroy()方法
  • 含有Adaptive注解的方法必须含有URL类型的参数,或者能够获取到URL,分别如上述的refer方法和export方法

至此,dubbo的ExtensionLoader的内容大概就说完了。

以上是关于Dubbo源码分析系列-扩展机制的实现的主要内容,如果未能解决你的问题,请参考以下文章

dubbo 源码 v2.7 分析:核心机制

Dubbo底层源码分析之SPI扩展点

Dubbo SPI源码分析

Dubbo Filter机制概述

源码分析---SOFARPC可扩展的机制SPI

Dubbo之SPI源码分析