9. SPI mechanism analysis of Dubbo 5-Activate

Keywords: Java Dubbo

1. Dubbo's @Activate example

@SPI
public interface ActivateExt {
    String echo(String msg);
}
@Activate(group = {"default"})
public class ActivateExtImpl1 implements ActivateExt {
    @Override
    public String echo(String msg) {
        return msg;
    }
}

@Activate(group = {"group1", "group2"})
public class GroupActivateExtImpl implements ActivateExt {

    @Override
    public String echo(String msg) {
        return msg;
    }
}
@Activate(order = 2, group = {"order"})
public class OrderActivateExtImpl1 implements ActivateExt {

    @Override
    public String echo(String msg) {
        return msg;
    }
}

@Activate(order = 1, group = {"order"})
public class OrderActivateExtImpl2 implements ActivateExt {

    @Override
    public String echo(String msg) {
        return msg;
    }
}
@Activate(value = {"value"}, group = {"group"})
public class ValueActivateExtImpl implements ActivateExt {

    @Override
    public String echo(String msg) {
        return msg;
    }
}

New META-INF/dubbo/internal folder and com.alibaba.dubbo.demo.provider.activate.ActivateExt under resources, i.e. fully qualified name of interface, with file contents as follows:

group=com.alibaba.dubbo.demo.provider.activate.impl.GroupActivateExtImpl
key=com.alibaba.dubbo.demo.provider.activate.impl.ValueActivateExtImpl
order1=com.alibaba.dubbo.demo.provider.activate.impl.OrderActivateExtImpl1
order2=com.alibaba.dubbo.demo.provider.activate.impl.OrderActivateExtImpl2
com.alibaba.dubbo.demo.provider.activate.impl.ActivateExtImpl1

Following are several unit tests and their test results. Observing the test results, we can draw the following conclusions:
1) If the group parameter in loader.getActivateExtension(URL url, String[] values, String group) can be matched, it is our choice. ActivateExtImpl1 is matched according to group=default in test 1, which also needs to be activated under this condition.
2) Value in @Activate is the second level of validation parameter, and group is the first level. If the group validation passes, the key parameter in the URL is the same as the value parameter in the class @Activate annotation and the value corresponding to the key parameter is not empty, then it will be selected.
3) The smaller the order, the higher the priority.

/**
 * 1
 * class com.alibaba.dubbo.demo.provider.activate.impl.ActivateExtImpl1
 */
@Test
public void test1(){
    ExtensionLoader<ActivateExt> loader = ExtensionLoader.getExtensionLoader(ActivateExt.class);
    URL url = URL.valueOf("test://localhost/test");
    List<ActivateExt> list = loader.getActivateExtension(url, new String[]{}, "default");
    System.out.println(list.size());
    list.forEach(item -> System.out.println(item.getClass()));
}
/**
 * 1
 * class com.alibaba.dubbo.demo.provider.activate.impl.GroupActivateExtImpl
 */
@Test
public void test2(){
    ExtensionLoader<ActivateExt> loader = ExtensionLoader.getExtensionLoader(ActivateExt.class);
    URL url = URL.valueOf("test://localhost/test");
    List<ActivateExt> list = loader.getActivateExtension(url, new String[]{}, "group1");
    System.out.println(list.size());
    list.forEach(item -> System.out.println(item.getClass()));
}
/**
 * 2
 * class com.alibaba.dubbo.demo.provider.activate.impl.OrderActivateExtImpl1
 * class com.alibaba.dubbo.demo.provider.activate.impl.ValueActivateExtImpl
 */
@Test
public void test3(){
    ExtensionLoader<ActivateExt> loader = ExtensionLoader.getExtensionLoader(ActivateExt.class);
    URL url = URL.valueOf("test://localhost/test");
    // Note that URL reception is used here, not directly url.addParameter()
    url = url.addParameter("value", "test");
    List<ActivateExt> list = 
                   loader.getActivateExtension(url, new String[]{"order1", "default"}, "group");
    System.out.println(list.size());
    list.forEach(item -> System.out.println(item.getClass()));
}
/**
 * 2
 * class com.alibaba.dubbo.demo.provider.activate.impl.OrderActivateExtImpl2
 * class com.alibaba.dubbo.demo.provider.activate.impl.OrderActivateExtImpl1
 */
@Test
public void test4(){
    ExtensionLoader<ActivateExt> loader = ExtensionLoader.getExtensionLoader(ActivateExt.class);
    URL url = URL.valueOf("test://localhost/test");
    List<ActivateExt> list = loader.getActivateExtension(url, new String[]{}, "order");
    System.out.println(list.size());
    list.forEach(item -> System.out.println(item.getClass()));
}

2. Source code analysis

public void test3(){
    ExtensionLoader<ActivateExt> loader = ExtensionLoader.getExtensionLoader(ActivateExt.class);
    URL url = URL.valueOf("test://localhost/test");
    // Note that URL reception is used here, not directly url.addParameter()
    url = url.addParameter("value", "test");
    List<ActivateExt> list = 
                   loader.getActivateExtension(url, new String[]{"order1", "default"}, "group");
    System.out.println(list.size());
    list.forEach(item -> System.out.println(item.getClass()));
}
public List<T> getActivateExtension(URL url, String[] values, String group) {
    List<T> exts = new ArrayList<T>();
    // Wrap the passed values into List-type names
    List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);
    // Packed data does not contain "-default"
    if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
        // Get all extension information for this type of data
        getExtensionClasses();
        for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
            // Get the extended name, and configure the key=value in the configuration file
            String name = entry.getKey();
            // Get annotations
            Activate activate = entry.getValue();
            // To determine whether the group matches, the group is the value passed in by the program, and if it is not set, it returns true to indicate the match.
            // If you set up a need for comparison matching
            if (isMatchGroup(group, activate.group())) {
                // Get the key value, that is, the instance corresponding to the name
                T ext = getExtension(name);
                if (!names.contains(name)
                        && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
                        && isActive(activate, url)) {
                    exts.add(ext);
                }
            }
        }
        // sort
        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 name is default, add usrs to exts header and empty usrs
                if (!usrs.isEmpty()) {
                    exts.addAll(0, usrs);
                    usrs.clear();
                }
            } else {
                // Get the extension of name and add usrs
                T ext = getExtension(name);
                usrs.add(ext);
            }
        }
    }
    if (!usrs.isEmpty()) {
        exts.addAll(usrs);
    }
    return exts;
}

The cachedActivates used in the above code are assigned in the getExtension Classes () method, for specific reference: SPI Mechanism Analysis of Dubbo 1-SPI Loading class

Activate activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
    cachedActivates.put(names[0], activate);
}
// It's easy to see if the group matches.
private boolean isMatchGroup(String group, String[] groups) {
    if (group == null || group.length() == 0) {
        return true;
    }
    if (groups != null && groups.length > 0) {
        for (String g : groups) {
            if (group.equals(g)) {
                return true;
            }
        }
    }
    return false;
}
// Match the key in url with the value value in the Activate annotation, and the value corresponding to the key cannot be empty
private boolean isActive(Activate activate, URL url) {
    String[] keys = activate.value();
    if (keys.length == 0) {
        return true;
    }
    for (String key : keys) {
        for (Map.Entry<String, String> entry : url.getParameters().entrySet()) {
            String k = entry.getKey();
            String v = entry.getValue();
            if ((k.equals(key) || k.endsWith("." + key))
                    && ConfigUtils.isNotEmpty(v)) {
                return true;
            }
        }
    }
    return false;
}

Note that if there is - default in the incoming values in the following form, @Activate through which all matches can be passed will not be activated, only those specified in values can be activated. If "-" is passed in to match the initial extension class name, the extension point will not be activated.

// Output class com.alibaba.dubbo.demo.provider.activate.impl.OrderActivateExtImpl1
ExtensionLoader<ActivateExt> loader = ExtensionLoader.getExtensionLoader(ActivateExt.class);
URL url = URL.valueOf("test://localhost/test");
// Note that URL reception is used here, not directly url.addParameter()
url = url.addParameter("value", "test");
List<ActivateExt> list = loader.getActivateExtension(url, new String[]{"order1", "-default"}, "group");
System.out.println(list.size());
list.forEach(item -> System.out.println(item.getClass()));

Posted by Charles Wong on Sat, 05 Oct 2019 19:26:38 -0700