第3章 Dubbo SPI 使用姿势

SPI 机制是实现可扩展性的一种方式。上一篇介绍了 JDK SPI 的使用姿势和基本原理,本节来分析 Dubbo SPI 的基本使用、适配类使用、AOP 使用、IOC 使用以及激活点的使用(基于 dubbo 2.6.6)。

示例代码地址:https://github.com/zhaojigang/dubbo-demo

可参考的 SPI 实现:

一、Dubbo SPI 基本使用

image.png
  • Dubbo 配置文件名依然是:SPI 接口的全接口名
  • Dubbo SPI 会从以下三个目录读取配置文件:
  • META-INF/dubbo/internal/:该目录用于存储 Dubbo 框架本身提供的 SPI 扩展实现,eg.
    image.png
  • META-INF/dubbo/:第三方提供的扩展(包括我们自己写的)建议 写在这个目录下(实际上写到三个目录的任一目录下都可以,但是不方便管理)
  • META-INF/services/:JDK SPI 的配置文件目录

SPI 接口

@SPI("logback")
public interface Log {
    void execute();
}

SPI 接口实现

public class Logback implements Log {
    @Override
    public void execute() {
        System.out.println("this is logback!");
    }
}

public class Log4j implements Log {
    @Override
    public void execute() {
        System.out.println("this is log4j!");
    }
}

配置文件

logback=io.study.dubbo.spi.basic.Logback
log4j=io.study.dubbo.spi.basic.Log4j

测试主类

public class TestBasic {
    public static void main(String[] args) {
        ExtensionLoader<Log> loader = ExtensionLoader.getExtensionLoader(Log.class);

        // 1. 指定名称获取具体 SPI 实现类
        Log logback = loader.getExtension("logback");
        logback.execute(); // this is logback!
        Log log4j = loader.getExtension("log4j");
        log4j.execute(); // this is log4j!

        // 2. 获取默认实现类 @SPI("logback") 中的 logback 就指定了默认的 SPI 实现类的 key
        Log defaultExtension = loader.getDefaultExtension();
        defaultExtension.execute(); // this is logback!
        System.out.println(loader.getDefaultExtensionName()); // logback

        // 3. 获取支持哪些 SPI 实现类
        Set<String> supportedExtensions = loader.getSupportedExtensions();
        supportedExtensions.forEach(System.out::println); // log4j \n logback

        // 4. 获取已经加载了哪些 SPI 实现类
        Set<String> loadedExtensions = loader.getLoadedExtensions();
        loadedExtensions.forEach(System.out::println); // log4j \n logback

        // 5. 根据 SPI 实现类实例或者实现类的 Class 信息获取其 key
        System.out.println(loader.getExtensionName(logback)); // logback
        System.out.println(loader.getExtensionName(Logback.class)); // logback

        // 6. 判断是否具有指定 key 的 SPI 实现类
        System.out.println(loader.hasExtension("logback")); // true
        System.out.println(loader.hasExtension("log4j2"));  // false
    }
}

二、Dubbo SPI 适配类使用

  • Dubbo 适配类:适配类其实就是一个工厂类,根据传递的参数动态的使用相应的 SPI 实现类;
  • Dubbo 适配类有两种姿势:(一个 SPI 接口最多只有一个适配类,如果有手动编写的适配类,那么则首先使用手动编写的适配类)
  • 手动编写一个适配类(Dubbo 默认只提供了两个手动编写的适配类 AdaptiveExtensionFactoryAdaptiveCompiler
  • 根据 SPI 接口动态生成一个适配类

2.1 手动编写一个适配类

SPI 接口

@SPI("logback")
public interface Log {
    void execute(String name);
}

SPI 实现

public class Log4j implements Log {
    @Override
    public void execute(String name) {
        System.out.println("this is log4j! " + name);
    }
}

public class Logback implements Log {
    @Override
    public void execute(String name) {
        System.out.println("this is logback! " + name);
    }
}

SPI适配类

/**
 * 手动编写 SPI 适配类
 * 注意:适配类也需要在配置文件中进行配置
 */
@Adaptive
public class AdaptiveLog implements Log {
    private static final ExtensionLoader<Log> loader = ExtensionLoader.getExtensionLoader(Log.class);

    @Override
    public void execute(String name) {
        Log log = null;
        if (name == null || name.length() == 0) {
            log = loader.getDefaultExtension();
        } else {
            log = loader.getExtension(name);
        }
        if (log != null) {
            log.execute(name);
        }
    }
}

适配类要实现 SPI 接口。(见 ExtensionLoader<T>.public T getAdaptiveExtension() 方法定义,类泛型与方法返回泛型相同,都是 SPI 接口)

配置文件

log4j=io.study.dubbo.spi.adaptive.manual.Log4j
logback=io.study.dubbo.spi.adaptive.manual.Logback
adaptive=io.study.dubbo.spi.adaptive.manual.AdaptiveLog

注意:手动编写的适配类需要在配置文件中进行配置

测试主类

public class TestAdaptiveManual {
    public static void main(String[] args) {
        ExtensionLoader<Log> loader = ExtensionLoader.getExtensionLoader(Log.class);

        System.out.println("======================= 获取 SPI 适配类(自己手写适配类) =======================");
        Log adaptiveExtension = loader.getAdaptiveExtension(); // AdaptiveLog 实例
        adaptiveExtension.execute("log4j"); // this is log4j! log4j
    }
}

2.2 根据 SPI 接口动态生成一个适配类

SPI 接口

/**
 * SPI 接口
 */
@SPI("logback")
public interface Log {

    /**
     * 含有 @Adaptive 注解的方法,生成的动态类会实现该方法,该方法必须直接包含 URL 参数或者方法的参数中要包含 URL 参数
     * @Adaptive 注解中的 String[] value() 代表 url 中用于获取 SPI 实现类的 key 的参数名:
     *
     * eg. 本例的配置生成的代码如下
     * String extName = url.getParameter("xxx", url.getParameter("ooo", "logback")); // 其中 logback 是默认值, 即先获取 Url 中key为xxx的值,如果该值存在,则使用该值去 SPI 配置文件中获取对应的实现
     * Log extension = ExtensionLoader.getExtensionLoader(Log.class).getExtension(extName);
     */
    @Adaptive({"xxx","ooo"})
    void execute(URL url);

    /**
     * 不带有 @Adaptive 注解的方法,生成的动态类中该方法的方法体直接抛异常
     */
    void test();
}
注意点
  • 含有 @Adaptive 注解的方法,生成的动态类会实现该方法,该方法必须直接包含 URL 参数或者方法的参数中要包含 URL 参数(因为要根据 URL 参数来判断具体使用哪一个 SPI 实现,具体见如下“动态生成的适配类”,由此可见,适配类其实就是一个工厂类,根据传递的参数动态的使用相应的 SPI 实现类)
  • 不带有 @Adaptive 注解的方法,生成的动态类中该方法的方法体直接抛异常
  • @Adaptive 注解中的 String[] value() 代表 url 中用于获取 SPI 实现类的 key 的参数名(示例解释见如下代码注释);假设 @Adaptive 没有配置 String[] value(),那么会默认按照类名(大写变小写,且加“.”作为分隔符)作为 key 去查找(eg. interface io.study.TestLog,则 key=“test.log”),即 String extName = url.getParameter("test.log", "logback");
动态生成的适配类
public class Log$Adaptive implements io.study.dubbo.spi.adaptive.auto.Log {
    @Override
    public void execute(com.alibaba.dubbo.common.URL arg0) {
        if (arg0 == null) {
            throw new IllegalArgumentException("url == null");
        }
        com.alibaba.dubbo.common.URL url = arg0;
        // 首先获取url中的xxx=ppp这个参数的值ppp,假设有,使用该值去获取key为ppp的 SPI 实现类;假设没有,再获取ooo=ppp,假设也没有,使用默认的logback去获取key为logback的SPI实现类
        String extName = url.getParameter("xxx", url.getParameter("ooo", "logback"));
        if (extName == null) {
            throw new IllegalStateException("Fail to get extension(io.study.dubbo.spi.adaptive.auto.Log) name from url(" + url.toString() + ") use keys([xxx, ooo])");
        }
        io.study.dubbo.spi.adaptive.auto.Log extension = (io.study.dubbo.spi.adaptive.auto.Log) ExtensionLoader.getExtensionLoader(io.study.dubbo.spi.adaptive.auto.Log.class).getExtension(extName);
        extension.execute(arg0);
    }

    @Override
    public void test() {
        throw new UnsupportedOperationException("method public abstract void io.study.dubbo.spi.adaptive.auto.Log.test() of interface io.study.dubbo.spi.adaptive.auto.Log is not adaptive method!");
    }
}

SPI 实现

public class Log4j implements Log {
    @Override
    public void execute(URL url) {
        System.out.println("this is log4j! " + url.getIp());
    }

    @Override
    public void test() {}
}

public class Logback implements Log {
    @Override
    public void execute(URL url) {
        System.out.println("this is logback! " + url.getIp());
    }

    @Override
    public void test() {}
}

配置文件

logback=io.study.dubbo.spi.adaptive.auto.Logback
log4j=io.study.dubbo.spi.adaptive.auto.Log4j

测试主类

public class TestAdaptiveAuto {
    public static void main(String[] args) {
        ExtensionLoader<Log> loader = ExtensionLoader.getExtensionLoader(Log.class);

        Log adaptiveExtension = loader.getAdaptiveExtension();
        URL url = new URL("dubbo", "10.211.55.6", 8080);
        adaptiveExtension.execute(url.addParameter("xxx", "log4j")); // this is log4j! 10.211.55.6
    }
}

三、Dubbo SPI AOP 使用

相较于 JDK SPI 的一个增强点。如果设置了 Wrapper 类,该类会对所有的 SPI 实现类进行包裹。

SPI 接口

@SPI("logback")
public interface Log {
    void execute();
}

SPI 实现

public class Log4j implements Log {
    @Override
    public void execute() {
        System.out.println("this is log4j!");
    }
}

public class Logback implements Log {
    @Override
    public void execute() {
        System.out.println("this is logback!");
    }
}

wrapper 类

/**
 * wrapper 类也必须实现 SPI 接口,否则 loadClass() 处报错
 */
public class LogWrapper1 implements Log {
    private Log log;

    /**
     * wrapper 类必须有一个含有单个 Log 参数的构造器
     */
    public LogWrapper1(Log log) {
        this.log = log;
    }

    @Override
    public void execute() {
        System.out.println("LogWrapper1 before");
        log.execute();
        System.out.println("LogWrapper1 after");
    }
}

public class LogWrapper2 implements Log {
    private Log log;
    public LogWrapper2(Log log) {
        this.log = log;
    }

    @Override
    public void execute() {
        System.out.println("LogWrapper2 before");
        log.execute();
        System.out.println("LogWrapper2 after");
    }
}
  • wrapper 类也必须实现 SPI 接口
  • wrapper 类必须有一个含有单个 Log 参数的构造器

配置文件

log4j=io.study.dubbo.spi.aop.Log4j
logback=io.study.dubbo.spi.aop.Logback
io.study.dubbo.spi.aop.LogWrapper1
io.study.dubbo.spi.aop.LogWrapper2
  • wrapper 类必须在配置文件中进行配置,不需要配置 key

测试主类

public class TestAOP {
    public static void main(String[] args) {
        ExtensionLoader<Log> loader = ExtensionLoader.getExtensionLoader(Log.class);

        System.out.println("======================= 根据指定名称获取具体的 SPI 实现类(测试 wrapper) =======================");
        Log logback = loader.getExtension("logback"); // 最外层的 Wrapper 类实例
        /**
         * 输出
         * LogWrapper2 before
         * LogWrapper1 before
         * this is logback!
         * LogWrapper1 after
         * LogWrapper2 after
         */
        logback.execute();
    }
}

关于 wrapper 类的加载顺序,见 https://github.com/apache/dubbo/issues/4578

四、Dubbo SPI IOC 使用

也是对 JDK SPI 功能的一个增强,

  • Dubbo SPI IOC 有两种姿势:
  • 注入 Dubbo 适配类:值得注意的是,Dubbo 只能注入适配类,不能直接注入 SPI 具体实现;
  • 注入 Spring Bean:需要将包含该 Spring Bean 的 Spring 上下文添加到 Dubbo 的 Spring 上下文管理器(SpringExtensionFactory)中,这样后续做 IOC 时,才能获取到该 Bean
  • Dubbo 的 SPI 使用的是 set 注入,所以需要提供一个 public void setXxx(单个 SPI 接口参数) 方法,对于注入适配类方式来讲,由于注入的只是适配类,只与 SPI 接口有关,与 setXxx 方法的 Xxx无关;对于注入 Spring Bean 方式来讲,由于注入的是具体的 Bean,Xxx 是 Spring Bean 的名称。

2.1 注入 Dubbo 适配类

SPI 接口

@SPI("logback")
public interface Log {
    void execute();
}

SPI 实现

public class Logback implements Log {

    /**
     * SPI IOC 注入:
     * Book 是 SPI 接口,
     * 必须存在一个 public ooo setXxx(单个SPI接口) 的方法才可以进行 IOC 注入,
     * 且被注入的 SPI 接口必须有适配类(无论是手动还是自动)
     */
    private Book book;

    /**
     * 对于 SPI 注入方式来讲,setXxx 中的 Xxx 没有任何作用,因为注入的都是 SPI 接口的适配类而不是具体的实现类
     */
    public void setBookx(Book book) {
        this.book = book;
    }

    @Override
    public void execute() {
        URL url = new URL("dubbo", "10.211.55.5", 8080);
        System.out.println("this is logback! " + book.bookName(url.addParameter("language", "go")));
    }
}

public class Log4j implements Log {

    /**
     * SPI IOC 注入:
     * Book 是 SPI 接口,
     * 必须存在一个 public ooo setXxx(单个SPI接口) 的方法才可以进行 IOC 注入,
     * 且被注入的 SPI 接口必须有适配类(无论是手动还是自动)
     */
    private Book book;

    // @DisableInject 禁用 IOC 注入
    @DisableInject
    public void setBook(Book book) {
        this.book = book;
    }

    @Override
    public void execute() {
        System.out.println("this is log4j!");
    }
}

被注入的 SPI 接口及其实现类

/**
 * SPI IOC 注入方式:必须有适配类(无论是手动还是自动)
 * note:手动编写的 Adaptive 类内也可以实现 IOC 注入
 */
@SPI("java")
public interface Book {
    @Adaptive({"language"})
    String bookName(URL url);
}

public class JavaBook implements Book {
    @Override
    public String bookName(URL url) {
        return "this is java book!" + url.getIp();
    }
}

public class GoBook implements Book {
    @Override
    public String bookName(URL url) {
        return "this is go book!" + url.getIp();
    }
}

// 动态生成的 SPI 适配类
public class Book$Adaptive implements io.study.dubbo.spi.ioc.spi.Book {
    @Override
    public java.lang.String bookName(com.alibaba.dubbo.common.URL arg0) {
        if (arg0 == null) {
            throw new IllegalArgumentException("url == null");
        }
        com.alibaba.dubbo.common.URL url = arg0;
        String extName = url.getParameter("language", "java");
        if (extName == null) {
            throw new IllegalStateException("Fail to get extension(io.study.dubbo.spi.ioc.spi.Book) name from url(" + url.toString() + ") use keys([language])");
        }
        io.study.dubbo.spi.ioc.spi.Book extension = (io.study.dubbo.spi.ioc.spi.Book) ExtensionLoader.getExtensionLoader(io.study.dubbo.spi.ioc.spi.Book.class).getExtension(extName);
        return extension.bookName(arg0);
    }
}

配置文件

Log SPI 配置文件
log4j=io.study.dubbo.spi.ioc.spi.Log4j
logback=io.study.dubbo.spi.ioc.spi.Logback
Book SPI 配置文件
java=io.study.dubbo.spi.ioc.spi.JavaBook
go=io.study.dubbo.spi.ioc.spi.GoBook

测试主类

public class TestSPIIOC {
    public static void main(String[] args) {
        ExtensionLoader<Log> loader = ExtensionLoader.getExtensionLoader(Log.class);

        // 1. 测试 SPI IOC
        Log logback = loader.getExtension("logback");
        logback.execute(); // this is logback! this is go book!10.211.55.5

        // 2. 测试禁用 SPI IOC
        Log log4j = loader.getExtension("log4j");
        log4j.execute(); // this is log4j!
    }
}

2.2 注入 Spring Bean

SPI 接口

@SPI("logback")
public interface Log {
    void execute();
}

SPI 实现

public class Logback implements Log {
    /**
     * Spring IOC 注入:
     * 1. 必须存在一个 public ooo setXxx(其中 Xxx 是 Spring bean 的名称) 的方法才可以进行 IOC 注入,
     */
    private Book book;

    /**
     * 对于 Spring 注入来讲,setXxx 中的 Xxx 代表了注入 Bean 的名称,这里注入的就是 javaBook 这个 Bean
     */
    public void setJavaBook(Book book) {
        this.book = book;
    }

    @Override
    public void execute() {
        System.out.println("this is logback! " + book.bookName());
    }
}

public class Log4j implements Log {
    /**
     * Book 是 SPI 接口,
     * 必须存在一个 public ooo setXxx(单个SPI接口) 的方法才可以进行 IOC 注入
     */
    private Book book;

    public void setGoBook(Book book) {
        this.book = book;
    }

    @Override
    public void execute() {
        System.out.println("this is log4j!" + book.bookName());
    }
}

被注入的 SPI 接口及其实现类

/**
 * Spring IOC 注入方式
 */
public interface Book {
    String bookName();
}

public class GoBook implements Book {
    @Override
    public String bookName() {
        return "this is go book!";
    }
}

public class JavaBook implements Book {
    @Override
    public String bookName() {
        return "this is java book!";
    }
}

spring 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
    <!-- spring bean -->
    <bean id="javaBook" class="io.study.dubbo.spi.ioc.spring.JavaBook"/>
    <bean id="goBook" class="io.study.dubbo.spi.ioc.spring.GoBook"/>
</beans>

SPI 配置文件

log4j=io.study.dubbo.spi.ioc.spring.Log4j
logback=io.study.dubbo.spi.ioc.spring.Logback

测试主类

public class TestSpringIOC {
    public static void main(String[] args) {
        // 1. 创建 spring 容器 + 加载 Bean 到该容器中
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"spring.xml"});
        // 2. 将 spring 容器添加到 dubbo 的 SpringExtensionFactory 工厂中
        SpringExtensionFactory.addApplicationContext(context);

        ExtensionLoader<Log> loader = ExtensionLoader.getExtensionLoader(Log.class);
        // 3. 测试 dubbo spring ioc
        Log logback = loader.getExtension("logback");
        logback.execute();  // this is logback! this is java book!
    }
}

五、Dubbo SPI 激活点使用

Dubbo 的激活点机制基于 @Activate 注解完成,可以用于实现根据条件加载多个 SPI 激活点实现类。

  • @Activate 注解:
  • String[] group() default {}
  • 如果 getActivateExtension 接口传入的 group 参数为 null 或者 length==0,表示不限制 group,则允许加载当前 SPI 实现;
  • 查看当前的 SPI 实现的 @Activate 注解中的参数 groups 是否包含传入的限制参数 group,如果包含,则允许加载当前的 SPI 实现。
  • String[] value() default {}
  • 如果当前的 SPI 实现的 @Activate 注解没有 value() 属性,则认为默认是允许当前的 SPI 实现加载的;
  • 如果 value() 中的任一值出现在当前传入的 URL#getParameters() 中的一个参数名,则认为默认是允许当前的 SPI 实现加载的;
  • 激活点加载流程(仅列出最常用的主线,其他支线见后续的源码分析及源码注释,整个流程配合下边的例子 TestActivate#testValue() 来看):
  1. 首先获取除了传入的 spiKey 集合(values)指定的 spi 激活点实现类(称为 default 激活点),之后对 default 激活点进行排序

加载 default 激活点的规则:

  • 如果 getActivateExtension 接口传入的 group 参数为 null 或者 length==0,表示不限制 group,则允许加载当前 SPI 实现;
  • 如果 group 有效,则查看当前的 SPI 实现的 @Activate 注解中的参数 groups 是否包含传入的限制参数 group,如果包含,则允许加载当前的 SPI 实现;
  • 传入的spiKey 集合(values)不包含(-name,name 表示当前处理到的 SPI 激活点的 spiKey):也就是说配置 -name 可以排除掉某个实现类;
  • 如果当前的 SPI 实现的 @Activate 注解没有 value() 属性,则认为默认是加载的,直接返回 true;
  • 如果当前的 SPI 实现的 @Activate 注解有 value() 属性,遍历每一个元素,如果 url.getParameters() 中的参数名包含了其中任意一个元素(也就是说String[] value() 中的任一值出现在当前传入的 URL#parameters() 中的一个参数名)
  1. 之后获取传入的 spiKey 集合(values)指定的 SPI 激活点实现类(称为 usr 激活点)
  • 传入的spiKey 集合(values)不包含(-name,name 表示当前处理到的 SPI 激活点的 spiKey):也就是说配置 -name 可以排除掉某个实现类;
  1. 将 default 激活点集合和 usr 激活点集合放到一个集合中,default 在前,usr 在后

SPI 接口

@SPI
public interface Log {
    void execute();
}

SPI 实现

@Activate
public class NoCondition implements Log {
    @Override
    public void execute() {
        System.out.println("this is noCondition!");
    }
}

// order 值越小,则排在集合前边,order 默认为 0
@Activate(group = {"provider"}, order = 1)
public class SingleGroup implements Log {
    @Override
    public void execute() {
        System.out.println("this is single group!");
    }
}

@Activate(group = {"provider", "consumer"}, order = 2)
public class MultiGroup implements Log {
    @Override
    public void execute() {
        System.out.println("this is multi group!");
    }
}

@Activate(value = {"singleValue"})
public class SingleValue implements Log {
    @Override
    public void execute() {
        System.out.println("this is single value!");
    }
}

@Activate(value = {"multi"})
public class MultiValue implements Log {
    @Override
    public void execute() {
        System.out.println("this is multi value!");
    }
}

@Activate(group = {"provider", "consumer"}, value = {"groupAndValue"})
public class GroupAndValue implements Log {
    @Override
    public void execute() {
        System.out.println("this is GroupAndValue!");
    }
}

配置文件

nc=io.study.dubbo.spi.activate.NoCondition
sg=io.study.dubbo.spi.activate.SingleGroup
mg=io.study.dubbo.spi.activate.MultiGroup
sv=io.study.dubbo.spi.activate.SingleValue
mv=io.study.dubbo.spi.activate.MultiValue
gv=io.study.dubbo.spi.activate.GroupAndValue

测试主类

public class TestActivate {
    public static void main(String[] args) {
        ExtensionLoader<Log> loader = ExtensionLoader.getExtensionLoader(Log.class);
        testGroup(loader);
        testValue(loader);
    }

    /**
     * 1. 测试 group
     * 仅仅过滤出@Activate.groups包含url传入的group=xxx参数
     */
    private static void testGroup(ExtensionLoader<Log> loader) {
        System.out.println("======================= 测试 group =======================");
        URL url = new URL("dubbo", "10.211.55.6", 8080);
        String group = "provider";
        String[] values = new String[]{};
        List<Log> activateExtension = loader.getActivateExtension(url, values, group);
        /**
         * 输出:
         * this is single group!
         * this is multi group!
         */
        activateExtension.forEach(Log::execute);
    }

    /**
     * 2. 测试 value
     */
    private static void testValue(ExtensionLoader<Log> loader) {
        System.out.println("======================= 测试 value =======================");
        URL url = new URL("dubbo", "10.211.55.6", 8080);
//        url = url.addParameter("groupAndValue", "gv");
        String[] values = new String[]{"sv", "-mg"};
        /**
         * NoCondition @Activate                                    no
         * SingleGroup @Activate(group = {"provider"}, order = 1)   sg
         * MultiGroup  @Activate(group = {"provider", "consumer"}, order = 2)  mg
         * SingleValue @Activate(value = {"singleValue"})           sv
         * MultiValue  @Activate(value = {"multi"})                 mv
         * GroupAndValue @Activate(group = {"provider", "consumer"}, value = {"groupAndValue"})  gv
         *
         * 1.首先加载 default 激活点(除了 "sv", "mg"之外的其他激活点),加载条件:
         *   我们加了 group 参数,首先会获取具有相关 group 的组,这里获取到 SingleGroup、GroupAndValue,
         *   由于 SingleGroup 没有配置 values 属性,所以认为激活,而 GroupAndValue 的 value 值的任一元素(groupAndValue)没有出现在 url.getParameter中,
         *   所以 GroupAndValue 不能加载(如果加上该句 url = url.addParameter("groupAndValue", "gv"); 代码,则可以加载)
         *   最后对所有的 default 激活点按照 order、before、after 属性进行排序
         * 2.之后加载 usr 激活点("sv", "mg" 激活点),sv 正常加载,而 mg 我们配置的 values 是 -mg,也就是说不加载 mg
         */
        List<Log> activateExtension = loader.getActivateExtension(url, values, "provider");
        /**
         * 输出:
         * this is single group!
         * this is single value!
         */
        activateExtension.forEach(Log::execute);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容