avatar

Dubbo源码分析之核心篇(终)

上面我们聊到了ExtensionLoader.getAdaptiveExtension()的实现源码,这篇咱们来聊一聊Dubbo SPI最后一种——ExtensionLoader.getActiveExtension(),也就是激活扩展点的实现。那么在本篇安排的内容如下:

  • 还是回忆
  • 继续深入
  • 持续填坑
  • 最后总结

回忆

上篇我们说到了ExtensionLoader.getAdaptiveExtension(),那么我们这篇聊聊ExtensionLoader.getActivateExtension(),这种方式获取的扩展点我们称为激活扩展点,顾名思义,激活指的是需要特定的条件才激活才生效。那么我们回忆下它的规则吧。

  • 首先接口少不了,在接口上加上@SPI注解,在指定位置定义一个以接口全限定名为名的文件,在里面定义key和实现类的全限定类名。

  • 在实现类上加上@Activate注解,该注解有如下属性:

    • value:激活条件,只有在URL中有该条件作为参数,就激活该扩展。
    • group:通过group过滤,需要配合ExtensionLoader#getActivateExtension(URL, String, String)使用。只有当传递过来的group在配置的group中,才会激活。
    • before:配置相对位置。也就是可以通过这个来配置实现类的顺序
    • after:同上,也是和顺序相关
    • order:配置绝对位置,同上,也是和顺序相关
  • 万事具备,只差使用了。使用方式如下(从前面的文章中Copy出来的):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class App {
    public static void main( String[] args ){
    ExtensionLoader<IHelloService> extensionLoader = ExtensionLoader.getExtensionLoader(IHelloService.class);
    URL url = new URL("http","localhost",8080);
    //key为@Activate的value属性,而value就是配置文件中你需要获取的实现类对应的key.多个可以用“,”分割
    url = url.addParameter("impl","helloServiceImpl,helloServiceProvider");
    //这里的第二个参数传你需要进行激活的key。也就是说,在url中要有该key,才会创建类的实例
    List<IHelloService> impl = extensionLoader.getActivateExtension(url, "impl");
    for (IHelloService iHelloService : impl) {
    System.out.println(iHelloService.sayHello(url));
    }
    }
    }
  • 回忆完毕,下面进入正题——分析它的实现

深入

入口点:getActivateExtension(url,string)

  • 先来一起看看他的源码

    1
    2
    3
    4
    public List<T> getActivateExtension(URL url, String key) {
    //调用重载方法
    return getActivateExtension(url, key, null);
    }
  • 继续深入getActivateExtension(url, key, null),源码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    public List<T> getActivateExtension(URL url, String key, String group) {
    //从url中获取key对应的value
    String value = url.getParameter(key);
    //调用重载方法,传递从url中取出来的value.如果value配置有多个,则以","分割。
    //这也是我们回忆部分,parameter中的value用","分割多个的原因
    //COMMA_SPLIT_PATTERN是一个正则,\s*[,]+\s*
    return getActivateExtension(url, StringUtils.isEmpty(value) ? null : COMMA_SPLIT_PATTERN.split(value), group);
    }
  • getActivateExtension(URL url, String[] values, String group)源码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    public List<T> getActivateExtension(URL url, String[] values, String group) {
    List<T> exts = new ArrayList<>();
    List<String> names = values == null ? new ArrayList<>(0) : Arrays.asList(values);
    if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
    getExtensionClasses();
    for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
    String name = entry.getKey();
    Object activate = entry.getValue();

    String[] activateGroup, activateValue;

    if (activate instanceof Activate) {
    activateGroup = ((Activate) activate).group();
    activateValue = ((Activate) activate).value();
    } else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
    activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
    activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
    } else {
    continue;
    }
    if (isMatchGroup(group, activateGroup)) {
    T ext = getExtension(name);
    if (!names.contains(name)
    && !names.contains(REMOVE_VALUE_PREFIX + name)
    && isActive(activateValue, url)) {
    exts.add(ext);
    }
    }
    }
    exts.sort(ActivateComparator.COMPARATOR);
    }
    List<T> usrs = new ArrayList<>();
    for (int i = 0; i < names.size(); i++) {
    String name = names.get(i);
    if (!name.startsWith(REMOVE_VALUE_PREFIX)
    && !names.contains(REMOVE_VALUE_PREFIX + name)) {
    if (DEFAULT_KEY.equals(name)) {
    if (!usrs.isEmpty()) {
    exts.addAll(0, usrs);
    usrs.clear();
    }
    } else {
    T ext = getExtension(name);
    usrs.add(ext);
    }
    }
    }
    if (!usrs.isEmpty()) {
    exts.addAll(usrs);
    }
    return exts;
    }
    • 上面就是getActivateExtension的具体实现了。下面我们一段一段解答它。

getActivateExtension(URL url,String[] values,String group)解读

起始

  • 我们先看如下代码

    1
    2
    3
    4
    5
    6
    7
    8
    //存放最终的结果
    List<T> exts = new ArrayList<>();
    List<String> names = values == null ? new ArrayList<>(0) : Arrays.asList(values);
    if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
    getExtensionClasses();
    // .....以下代码省略
    }
    // .....以下代码省略
    • if前面和判断我们先忽略。映入眼帘的就是getExtensionClasses();,这段代码已经是老相识了,里面的内容我也就不说了,总之,这里是会加载以对应接口全限定名为名的文件中配置的实现类。那么这里面还有一个坑未填。我们下面把他堵上。

填坑

  • 还记得下面的代码吗?位于ExtesionLoader.loadClass()方法中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //判断配置文件中的类
    if (clazz.isAnnotationPresent(Adaptive.class)) {
    //缓存类级别自适应扩展点
    //.....
    } else if (isWrapperClass(clazz)) {
    //如果是Warpper类,则缓存起来
    //.....
    } else {
    //....
    String[] names = NAME_SEPARATOR.split(name);
    if (ArrayUtils.isNotEmpty(names)) {
    //缓存激活扩展点
    cacheActivateClass(clazz, names[0]);
    for (String n : names) {
    //...
    //保存实现类到map
    saveInExtensionClass(extensionClasses, clazz, name);
    }
    }
    }
  • 想起来了么?上面if...else if...的坑我们都已经填完了。接下来就开始填最后一个else中缓存激活扩展点的坑。他的代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //clazz为实现类,name为配置的key
    private void cacheActivateClass(Class<?> clazz, String name) {
    //获取类上面的@Activate注解。如果没有就是返回null
    Activate activate = clazz.getAnnotation(Activate.class);
    if (activate != null) {
    //不为空,就往map中put。注意这里不是put实现类,而是注解
    cachedActivates.put(name, activate);
    } else {
    //如果为空,则尝试获取一下之前alibaba包下面的Activate注解。
    //因为没有捐给apache之前,dubbo的包名是alibaba。捐给apache之后,包名都是apache之类的。
    //这里这么做是为了兼容Dubbo没有捐给apache之前的版本,不然人家升上来还得改包引用
    com.alibaba.dubbo.common.extension.Activate oldActivate = clazz.getAnnotation(com.alibaba.dubbo.common.extension.Activate.class);
    if (oldActivate != null) {
    //同样,不为空就put到map。为空就什么都不做
    cachedActivates.put(name, oldActivate);
    }
    }
    }
    • 从上面代码看,先判断实现类上是否有加注解,有加注解就把注解信息put到Map。那么我们的实现类是保存到哪了呢?它会在外层调用saveInExtensionClass()方法,保存到extensionClasses中。那么到这里,最后一个坑也算是填完了。

继续

  • 我们回到getActivateExtension()方法,上面仅仅是在getExtensionClasses()方法中做的操作。下面我们继续看这段代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
    //这里我们刚刚看过了。
    getExtensionClasses();
    //从这个for开始看。看看在循环中做了什么
    for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
    //entry.getKey() ==> 获取的是我们在文件中配置的key
    String name = entry.getKey();
    //entry.getValue() ==> 注解类
    Object activate = entry.getValue();

    String[] activateGroup, activateValue;
    //判断注解是apache包下面的还是dubbo包下面的。从而进行不同的操作
    if (activate instanceof Activate) {
    //获取配置的group信息和激活条件
    activateGroup = ((Activate) activate).group();
    activateValue = ((Activate) activate).value();
    } else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
    //这里也是一样,获取配置的group信息和激活条件。只是包不同而已。
    activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
    activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
    } else {
    continue;
    }
    //匹配group
    if (isMatchGroup(group, activateGroup)) {
    //如果作为参数传入的group和我们加在@Activate注解上面的group匹配
    //就通过name获取到我们对应的实现类
    T ext = getExtension(name);
    //names是在url中通过key得到的value
    //name是在配置文件中配置的key
    //处理仅用参数名做匹配,而参数值无意义的情况。
    if (!names.contains(name)
    && !names.contains(REMOVE_VALUE_PREFIX + name)
    && isActive(activateValue, url)) {
    //把扩展类示例放到结果集中。
    exts.add(ext);
    }
    }
    }
    //上面一系列操作做完后,在这里排序,这个排序和在注解上配置的位置有关。
    exts.sort(ActivateComparator.COMPARATOR);
    }
  • 我们看看他group是怎么匹配的。代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //group是在我们main方法中传递过来的。groups是我们在注解上面配置的
    private boolean isMatchGroup(String group, String[] groups) {
    //如果我们作为参数传过来的group是空串,那么直接返回true
    if (StringUtils.isEmpty(group)) {
    return true;
    }
    //如果注解中获取的groups不为空,然后main方法中传递的group也不为空和空串。
    //循环比对,匹配上就是true。如果全部都没匹配上,就返回false
    if (groups != null && groups.length > 0) {
    for (String g : groups) {
    if (group.equals(g)) {
    return true;
    }
    }
    }
    return false;
    }
    • 匹配规则也很简单,如下:
      • 如果我们从main方法穿过来的group为null或者空串(该参数在调用时是可选参数,所以可能是null或空串),直接返回true,匹配成功。
      • 如果group不为null或空串,并且groups也不为空,就一个一个比对,只要有一个匹配上了就返回true,如果都不匹配就返回false。
  • 再然后看一下isActive()方法,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    //keys是在实现类上的注解的value属性,也就是激活条件
    //url就是我们在main方法的传参。
    private boolean isActive(String[] keys, URL url) {
    //如果条件为空,就直接返回true。
    //那就说明我这个实现类没有任何的激活条件,可以直接使用。
    if (keys.length == 0) {
    return true;
    }
    //遍历条件
    for (String key : keys)
    //获取url中的所有参数遍历
    for (Map.Entry<String, String> entry : url.getParameters().entrySet()) {
    // k-->参数的key
    String k = entry.getKey();
    // v-->参数对应的值
    String v = entry.getValue();
    //如果url中有在注解中配置的value值作为参数,并且参数值不为空,就返回true
    if ((k.equals(key) || k.endsWith("." + key))
    && ConfigUtils.isNotEmpty(v)) {
    return true;
    }
    }
    }
    return false;
    }
    • 判断是否激活的依据总结如下:
      • 如果在注解上没有配置条件,那自然而然就返回true。也就是说,这个实现类可以不需要条件进行使用。
      • 上面说了注解上没有设置条件的情况处理。那设置了条件会怎样?
        • 遍历注解上配置的条件,并且遍历我们url中的参数
        • 如果url中有条件作为参数名,并且参数值不为空,则说明匹配成功,就返回true,代表该实现类已经激活。
        • 还有一种情况就是如果参数中是.条件作为参数结尾的,并且参数不为空,这时候也匹配成功,返回true。
      • 其他情况都是false。
  • 在继续往下,看最后一段代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    List<T> usrs = new ArrayList<>();
    //names为我们通过传入的参数key在url中得到得到的值
    for (int i = 0; i < names.size(); i++) {
    String name = names.get(i);
    //REMOVE_VALUE_PREFIX ==》 ‘-’
    if (!name.startsWith(REMOVE_VALUE_PREFIX)
    && !names.contains(REMOVE_VALUE_PREFIX + name)) {
    if (DEFAULT_KEY.equals(name)) {
    if (!usrs.isEmpty()) {
    exts.addAll(0, usrs);
    usrs.clear();
    }
    } else {
    //通过参数值获取扩展点
    T ext = getExtension(name);
    usrs.add(ext);
    }
    }
    }
    //如果不为空,则加入到结果集
    if (!usrs.isEmpty()) {
    exts.addAll(usrs);
    }
    return exts;
    • 这里我的猜测是处理参数值为指定的扩展实现类的情况。也就是回忆部分demo的那种情况。
  • 那我们总结下这段代码。

    • 初始化扩展实现类到缓存(getExtensionClasses)

    • 遍历该接口在实现类上配置的@Activate注解集合。

      • 读取在注解上配置的group和value属性

      • 使用main方法中传过来group和注解上配置的group进行比对。

        • 如果传过来的group为空,则默认认为是匹配的。
        • 如果匹配不成功,这返回false
      • 匹配成功后,调用getExtension获取对应的实现类

      • 如果是仅仅用参数名作为匹配,而参数值无意义的情况,就会通过url中的参数名和注解上配置的条件做比对。

        • 如果注解上没有条件,那默认返回true
        • 如果注解上有条件,就循环和url中的参数名做比对,如果相同并且参数值不为空,就返回true
        • 其他情况为false。
      • 加入到结果集。

    • 遍历URL中根据参数名获取的参数值。处理参数值为我们的SPI实现类的情况。

总结

以上这些(包括前几篇文章)就是Dubbo中SPI中的源码分析了。我们来一次大的总结吧,为后面分析dubbo的服务注册源码做铺垫。

  • 最基本的一种扩展点获取方式ExtensionLoader.getExtension(string),它的实现如下:
    • 先从缓存中根据name获取值。如果获取不到,就走加载的逻辑。
    • 读取指定目录下面的以接口全限定类名为名的文件,解析里面配置的key和对应的实现类
    • 根据实现类的特点,以及接口的特点进行分类。
      • 缓存在实现类上加了@Adaptive的实现类。这就是我们自己写的代理。
      • 如果是Wrapper增强的类,也缓存起来
      • 缓存激活扩展点,这点在上面已经说了。
      • 剩下的就是普通的实现类了,也缓存起来
    • 依赖注入(set方法)
  • 然后就是我们的自适应扩展点了,也就是ExstionLoder.getAdaptiveExtension()。调用这个方法得到的是一个代理类。具体的实现类是需要通过这个代理类去返回的。方法实现如下:
    • 先从缓存中获取,如果取不到就往下面走。取到了就返回
    • 如果是有类级别的自适应扩展实现类,就直接返回。也就是说,如果@Adaptive加在类实现类上,就把这个实现类返回。即我们手动写的代理类。(这种方式在dubbo中少见)
    • 如果没有,就会根据接口,自动生成我们的代理类。
    • 创建实例,然后使用set方法进行依赖注入。
  • 再然后就是今天说的激活扩展点的实现了。
    • 首先会初始化对应的扩展类。
    • 如果有配置group,就会先匹配group,匹配到了在通过条件(注解的value值)去匹配。
    • 最终返回匹配的实现类。

ok。上面就是SPI的全部内容了。根据个人理解,肯定会有点偏差。希望大佬们能够不吝指教。谢谢~~


评论