Dubbo中的四大扩展机制

173 阅读15分钟

所谓 Dubbo 的内核是指,Dubbo 中所有功能都是基于它之上完成的,都是由它作为基础的。Dubbo 内核的工作原理由四部分构成:

  • SPI:服务发现机制
  • Adaptive:自适应机制
  • Wrapper:包装机制
  • Activate:激活机制

动态编译的实现

​ Dubbo SPI的自适应特性让整个框架非常灵活, 而动态编译又是自适应特性的基础, 因为动态生成的自适应类只是字符串, 需要通过编译才能得到真正的Class0虽然我们可以使用反射来动态代理一个类, 但是在性能上和直接编译好的Class会有一定的差距。

​ Dubbo SPI通过代码的动态生成, 并配合动态编译器, 灵活地在原始类基础上创建新的自适应类。

Dubbo中的三种动态编译器

截屏2020-12-21 下午10.29.01

​ Compiler接口上含有一个SPI注解, 注解的默认值是@SPI(”javassist”),很明显, Javassist编译器将作为默认编译器。 如果用户想改变默认编译器, 则可以通过

<dubbo:application compiler="jdk"/>

AdaptiveCompiler动态编译

​ AdaptiveCompiler上面有@Adaptive注解, 说明AdaptiveCompiler会固定为默认实现这个Compiler的主要作用和AdaptiveExtensionFactory相似, 就是为了管理其他Compile。

AdaptiveCompiler部分源码

//设置默认的编译器名称
public static void setDefaultCompiler(String compiler) (
		DEFAULTJZOMPILER = compiler;
}

  public Class<?> compile(String code, ClassLoader classLoader) {
    ......
     //通过ExtensionLoader获取对应的编译器扩展实现,调用真正的compile做编译
    return compiler.comile(code, classLoader);
  }

​ AdaptiveCompiler#setDefaultCompiler 方法会在 ApplicationConfig 中被调用, 也就是 Dubbo在启动时会扫描解析配置中的<dubbo:application compiler="jdk"/> 标签。如果没有配置此值,则会使用 @SPI("javassist") 注解中的默认动态编译方式。

​ AbstpactCompiler,它是一个抽象类, 无法实例化, 但在里面封装了通用的模板逻辑。 还定义了一个抽象方法decompile,留给子类来实现具体的编译逻辑。OavassistCompiler和DdkCompiler都实现了这个抽象方法。

​ Abstractcompiler的主要抽象逻辑如下:

  • 通过正则匹配出包路径、 类名, 再根据包路径、 类名拼接出全路径类名。
  • 尝试通过Class.forName加载该类并返回, 防止重复编译。 如果类加载器中没有这个类, 则进入第3步。
  • 调用doCompile方法进行编译。 这个抽象方法由子类实现。

JDK动态编译

​ JDK原生编译器位于javax.tools包下。主要使用JavaFileObject接口,ForwardingJavaFileManager接口,JavaCompiler.CompilationTask方法。

​ 编译过程为:首先初始化一个JavaFileObject对象,然后把代码字符串作为参数传入构造方法,然后调用JavaCompiler.CompilerationTash方法编译出具体的类。

  • JavaFileObject:字符串代码会被包装成一个文件对象,并且提供获取二进制流的接口。在Dubbo框架中有一个类叫做JavaFileObjectImpl 这个类可以看做是JavaFileObject这个接口的实现扩展,构造方法需要传入生成好的字符串代码,此文件对象输入、输出流都是ByteArray 流。
  • JavaFileManager:主要管理文件的读取和输出位置。JDK中没有可以直接使用的实现类,有个实现类叫ForwardingJavaFileManager构造器又是protect类型的。所以在Dubbo框架中扩张了一个JavaFileManagerImpl的实现类,然后通过一个自定义类加载器ClassLoaderImpl来完成资源的加载。
  • JavaCompiler.CompilationTask把JavaFileObject来编译成具体的类。

Javassist动态编译

Javassist代码示例

public static void main(String[] args){
    //初始化一个Javassist类池
    ClassPool classPool = ClassPool.getDefault();
    //创建一个Hello World 类
    CtClass ctClass = classPool.makeClass("Hello World");
    //添加一个test方法,会打印Hello World,直接传入方法的字符串
    CtMethod ctMethod = CtNewMethod.make("
      public static void test(){
          System.out・printin(\"Hello World\");
      }", ctClass);
    ctClass.addMethod(ctMethod);
    //生成类
    Class aClass = ctClass.toClass();
    //使用反射调用实例
    Object object = aClass.newlnstance();
    Method m = aClass.getDeclaredMethod("test"null);
    m.invoke(objectnull);
}

​ 在JavassistCompiler中, 就是不断通过正则表达式匹,配不同部位的代码, 然后调用Javassist库中的API生成不同部位的代码, 最后得到一个完整的Class对象。

​ 在Dubbo框架中JavassistCompiler具体的步骤为:

  • 初始化Javassist,设置默认参数, 如设置当前的classpath。
  • 通过正则匹配出所有import的包, 并使用Javassist添加import。
  • 通过正则匹配出所有extends的包, 创建Class对象, 并使用Javassist添加extends。
  • 通过正则匹配出所有implements包, 并使用Javassist添加implements。
  • 通过正则匹配出类里面所有内容, 即得到{} 中的内容, 再通过正则匹配出所有方法,并使用Javassist添加类方法。
  • 生成Class对象。

JDK服务发现机制SPI

​ SPI,全称Service Provider Interface,服务提供者接口,是一种服务发现机制。

JDK中SPI规范

  • 接口名:可随意定义。
  • 实现类名:可随意定义。
  • 提供者配置文件路径:查找的目录为 META-INF/services。
  • 提供者配置文件名称:接口的全限定性类名,没有扩展名。
  • 提供者配置文件内容:该接口的所有实现类的全限类性类名写入到该文件中,一个类名占一行。

代码例子

定义具体接口

package com.gc.service;

/**
 * @description: 定义接口
 **/
public interface GcService {

    void habit();
}

定义具体实现类1

package com.gc.service.impl;

import com.gc.service.GcService;

/**
 * @description: GC的生活习惯
 **/
public class GcLivingHabitsServiceImpl implements GcService {
    @Override
    public void habit() {
        System.out.println("这里是GC的 [生活] 习惯");
    }
}

定义具体实现类2

package com.gc.service.impl;

import com.gc.service.GcService;

/**
 * @description: GC的工作习惯
 **/
public class GcWorkHabitsServiceImpl implements GcService {

    @Override
    public void habit() {
        System.out.println("这里是GC的 [工作] 习惯");
    }
}

在查找路径下定义配置信息文件:com.gc.service.GcService

com.gc.service.impl.GcLivingHabitsServiceImpl
com.gc.service.impl.GcWorkHabitsServiceImpl

编写测试类

package com.gc.test;

import com.gc.service.GcService;

import java.util.ServiceLoader;

/**
 * @description: 测试类
 **/
public class SPI {
    public static void main(String[] args) {
        ServiceLoader<GcService> load = ServiceLoader.load(GcService.class);
        load.forEach(gc->gc.habit());
    }
}

项目整体结构

image-20201214152935370

运行结果

image-20201214152956772

Dubbo服务发现机制SPI

​ Dubbo 并没有直接使用 JDK 的 SPI,而是在其基础之上对其进行了兼容改进。

​ @SPI注解可以使用在类、接口和枚举类上,Dubbo框架中都是使用在接口上。它的主要作用就是标记这个接口是一个Dubbo SPI接口,即是一个扩展点,可以有多个不同的内置或用户定义的实现。运行时需要通过配置找到具体的实现类。

@SPI注解的源码

package org.apache.dubbo.common.extension;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {
    //默认实现的key名称
    String value() default "";
}

我们可以看到SPI注解有一个value属性,通过这个属性,我们可以传入不同的参数来设置这个接口的默认实现类。

Dubbo中SPI规范

  • 接口名:可以随意定义。
  • 实现类名:在接口名前添加一个用于表示自身功能的“标识前辍”字符串。
  • 提供者配置文件路径:可放置的目录及依次查找的顺序为:
    • META-INF/dubbo/internal
    • META-INF/dubbo
    • META-INF/services
  • 提供者配置文件名称:接口的全限定性类名,无需扩展名。
  • 提供者配置文件内容:文件的内容为 key=value 形式,value 为该接口的实现类的全限类性类名,key 可以随意,但一般为该实现类的“标识前辍”(首字母小写)。一个类名占一行。
  • 提供者加载:ExtensionLoader 类相当于 JDK SPI 中的 ServiceLoader 类,用于加载提供者配置文件中所有的实现类,并创建相应的实例。

代码例子

pom.xml

<dependency>
  <groupId>org.apache.dubbo</groupId>
  <artifactId>dubbo</artifactId>
  <version>2.7.3</version>
</dependency>

定义具体接口

package com.gc.service;

import org.apache.dubbo.common.extension.SPI;

/**
 * @description: 定义接口
 **/

//如果没有传入指定的调用类,则默认调用living
@SPI("living")
public interface Habits {

    void habit();
}

具体实现1

package com.gc.service.impl;
import com.gc.service.Habits;

/**
 * @description: GC的生活习惯
 **/
public class LivingHabits implements Habits {

    @Override
    public void habit() {
        System.out.println("这里是GC的 [生活] 习惯");
    }
}

具体实现2

package com.gc.service.impl;
import com.gc.service.Habits;

/**
 * @description: GC的工作习惯
 **/
public class WorkHabits implements Habits {

    @Override
    public void habit() {
        System.out.println("这里是GC的 [工作] 习惯");
    }
}

在查找路径下定义配置信息文件:com.gc.service.Habits

living=com.gc.service.impl.LivingHabits
work=com.gc.service.impl.WorkHabits

编写测试类

package com.gc.test;

import com.gc.service.Habits;
import org.apache.dubbo.common.extension.ExtensionLoader;

/**
 * @description: 测试类
 **/
public class SPI {
    public static void main(String[] args) {
        ExtensionLoader<Habits> loader = ExtensionLoader.getExtensionLoader(Habits.class);
        //调用默认实现
        loader.getExtension("true").habit();
        //调用指定实现
        loader.getExtension("work").habit();
    }
}

项目结构

image-20201215114708806

运行结果

image-20201215114803582

ExtensionLoader 的工作流程

​ ExtensionLoader是整个扩展机制的主要逻辑类在这个类里面卖现了配置的加载,扩展类缓存,自适应对象生成等所有工作。

​ ExtensionLoader 的逻辑入口可以分为 getExtensio,getAdaptiveExtensio,getActivateExtension三个分别是获取普通扩展类、获取自适应扩展类、获取自动激活的扩展类。总体逻辑都是从调用这三个方法开始的,每个方法可能会有不同的重载的方法,根据不同的传入参数进行调整。

​ 关于ExtensionLoader 的工作流程这里先简单提一嘴,后面我会分析它的源代码。

image-20201215143408487

​ ExtensionLoader的工作流程图

自适应机制 Adaptive

​ Adaptive 机制,即扩展类的自适应机制即其可以指定想要加载的扩展名,也可以不指定。若不指定,则直接加载默认的扩展类。即其会自动匹配,做到自适应。其是通过@Adaptive注解实现的。一个 SPI 接口最多只会有一个 Adaptive 类(自定义的或自动生成的)

@Adaptive 注解

@Adaptive 注解可以修饰类与方法,其作用相差很大

//@Adaptive注解源码
public @interface Adaptive (
    //数组,可以设置多个key,会按顺序依次匹配
	String[] value() default {};
}
  • @Adaptive 修饰方法

被@Adapative 修饰的 SPI 接口中的方法称为 Adaptive 方法。在 SPI 扩展类中若没有找到Adaptive 类,但系统却发现了 Adapative 方法,就会根据 Adaptive 方法自动为该 SPI 接口动态生成一个 Adaptive 扩展类,并自动将其编译。例如 Protocol 接口中就包含两个 Adaptive方法。

  • @Adaptive 修饰类

    有些 SPI 接口中的方法不需要 URL 相关的参数,此时就可以直接让@Adaptivate 来修饰某个 SPI 接口的实现类,由该类实现对 SPI 扩展类的自适应。

    其是装饰者设计模式的应用。

Adaptive 方法规范

动态生成 Adaptive 类格式

package <SPI 接口所在包>;
public class SPI 接口名$Adpative implements SPI 接口 {
     public adaptiveMethod (arg0, arg1, ...) {
             // 注意,下面的判断仅对 URL 类型,或可以获取到 URL 类型值的参数进行判断
             // 例如,dubbo 的 Invoker 类型中就包含有 URL 属性
            if(arg1==null) 
                throw new IllegalArgumentException(异常信息);
            if(arg1.getUrl()==null) 
                throw new IllegalArgumentException(异常信息);

             URL url = arg1.getUrl();

            // 其会根据@Adaptive 注解上声明的 Key 的顺序,从 URL 获取 Value,
            // 作为实际扩展类。若有默认扩展类,则获取默认扩展类名;否则获取
            // 指定扩展名名。
             String extName = url.get 接口名() == null?默认扩展前辍名:url.get 接口名();
             if(extName==null) throw new IllegalStateException(异常信息);
             SPI 接口 extension = ExtensionLoader.getExtensionLoader(SPI 接口.class)
            .getExtension(extName);
         	return extension. adaptiveMethod(arg0, arg1, ...);
         }
    
         public unAdaptiveMethod( arg0, arg1, ...) {
         	throw new UnsupportedOperationException(异常信息);
     	} 
}		

方法规范

​ 从前面的动态生成的 Adaptive 类中的 adaptiveMethod()方法体可知,其对于要加载的扩展名的指定方式是通过 URL 类型的方法参数指定的。所以对于 Adaptive 方法的定义规范仅一条:其参数包含 URL 类型的参数,或参数可以获取到 URL 类型的值。方法调用者是通过URL 传递要加载的扩展名的。

代码例子

定义具体接口

package com.gc.service;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;

/**
 * @description: 定义接口
 **/

//如果没有传入指定的调用类,则默认调用living
@SPI("work")
public interface Habits {

    //好习惯
    void goodHabit();

    //坏习惯
    @Adaptive
    void badHabit(URL url);
}

具体实现1

package com.gc.service.impl;


import com.gc.service.Habits;
import org.apache.dubbo.common.URL;

/**
 * @description: GC的生活习惯
 **/
public class LivingHabits implements Habits {


    @Override
    public void goodHabit() {
        System.out.println("这是GC的 (生活) [好习惯]");
    }

    @Override
    public void badHabit(URL url) {
        System.out.println("这是GC的 (生活) [坏习惯]");
    }
}

具体实现2

package com.gc.service.impl;


import com.gc.service.Habits;
import org.apache.dubbo.common.URL;

/**
 * @description: GC的工作习惯
 **/
public class WorkHabits implements Habits {

    @Override
    public void goodHabit() {
        System.out.println("这是GC的 (工作) [好习惯]");
    }

    @Override
    public void badHabit(URL url) {
        System.out.println("这是GC的 (工作) [坏习惯]");
    }
}

编写测试类

package com.gc.test;

import com.gc.service.Habits;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;


/**
 * @description: 测试类
 **/
public class SPI {
    public static void main(String[] args) {
        ExtensionLoader<Habits> loader = ExtensionLoader.getExtensionLoader(Habits.class);
        //调用指定实现
        Habits habits = loader.getAdaptiveExtension();
        //模拟一个URL
        //如果不不指定调用实现类,则调用@SPI注解中指定的默认实现扩展类
        //URL url = URL.valueOf("xxx://127.0.0.1/xxx");

        //如果在url后面指定了要调用的扩展,则直接调用该扩展。
        URL url = URL.valueOf("xxx://127.0.0.1/xxx?habits=work");
        habits.badHabit(url);
    }
}

包装机制Wrapper

​ Wrapper 机制,即扩展类的包装机制。就是对扩展类中的 SPI 接口方法进行增强,进行包装,是AOP思想的提现是 Wrapper 设计模式的应用。一个 SPI 可以包含多个 Wrapper。

Wrapper规范

​ Wrapper并不是通过注解实现的,而是通过一套Wrapper规范。

  • 类需要实现SPI接口
  • 类中需要有SPI接口的引用
  • 类中的SPI实例需要是通过一个包含一个SIP接口参数的带参构造器来传的
  • 在接口实现方法中要调用SPI接口引用对象的相应方法
  • 类名的后缀需要时Wrapper

代码例子

定义具体接口

package com.gc.service;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;

/**
 * @description: 定义接口
 **/

//如果没有传入指定的调用类,则默认调用living
@SPI("work")
public interface Habits {

    //好习惯
    void goodHabit();

    //坏习惯
    @Adaptive
    void badHabit(URL url);
}

具体实现1

package com.gc.service.impl;


import com.gc.service.Habits;
import org.apache.dubbo.common.URL;

/**
 * @description: GC的生活习惯
 **/
public class LivingHabits implements Habits {


    @Override
    public void goodHabit() {
        System.out.println("这是GC的 (生活) [好习惯]");
    }

    @Override
    public void badHabit(URL url) {
        System.out.println("这是GC的 (生活) [坏习惯]");
    }
}

具体实现2

package com.gc.service.impl;


import com.gc.service.Habits;
import org.apache.dubbo.common.URL;

/**
 * @description: GC的工作习惯
 **/
public class WorkHabits implements Habits {

    @Override
    public void goodHabit() {
        System.out.println("这是GC的 (工作) [好习惯]");
    }

    @Override
    public void badHabit(URL url) {
        System.out.println("这是GC的 (工作) [坏习惯]");
    }
}

对具体实现进行包装

package com.gc.service.impl;

import com.gc.service.Habits;
import org.apache.dubbo.common.URL;

/**
 * TODO 包装增强类
 * 1.实现了Habits(SPI)接口
 * 2.在此包装类中定义包装接口的变量引用
 * 3.使用有参构造器传递具体包装类注入
 * 4.调用SPI接口中的方法进行包装
 */
public class HabitsWrapper implements Habits {

    private Habits habits;

    public HabitsWrapper(Habits habits){
        this.habits = habits;
    }

    @Override
    public void goodHabit() {
        System.out.println("在此之前,对GC的【好习惯】好习惯在此增强----开始");
        habits.goodHabit();
        System.out.println("增强----结束");
    }

    @Override
    public void badHabit(URL url) {
        System.out.println("在此之前,对GC的【坏习惯】坏习惯在此增强----开始");
        habits.badHabit(url);
        System.out.println("增强----结束");
    }
}

编写测试类

package com.gc.service.test;


import com.gc.service.Habits;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;


/**
 * TODO 测试类
 */
public class Test {

    public static void main(String[] args) {
        ExtensionLoader<Habits> loader = ExtensionLoader.getExtensionLoader(Habits.class);
        Habits habits =  loader.getAdaptiveExtension();
        URL url = URL.valueOf("xxx://localhost:8080/ooo/jjj");
        habits.badHabit(url);
    }
}

在查找路径下定义配置信息文件:com.gc.service.Habits

babits=com.gc.service.impl.LivingHabits
work=com.gc.service.impl.WorkHabits
wrapper=com.gc.service.impl.HabitsWrapper

项目结构

image-20201221105728460

激活机制Activae

​ 用于一次激活多个扩展类的。

​ Activate 机制,即扩展类的激活机制。通过指定的条件来激活当前的扩展类。其是通过@Active 注解实现的。

@Activae注解

​ 在@Activate 注解中共有五个属性,其中 before、after 两个属性已经过时,剩余有效属性还有三个。它们分别是:

  • group: 为扩展类指定所属的组别,是当前扩展类的一个标识。String[]类型,表示一个扩展类可以属于多个组。
  • value: 为当前扩展类指定的 key,是当前扩展类的一个标识。String[]类型,表示一个扩展类可以有多个指定的 key。
  • order: 指定筛选条件相同的扩展类的加载顺序。序号越小,优先级越高。默认值为 0。

代码例子

​ 我们已线上,线下支付举例。把线上和线下不同的支付方法分为不同的组。其中比如银行卡支付,它既可以是线上支付也可以是线下支付,所以它的处境可以有多个分组。

具体接口

package com.gc.service;

import org.apache.dubbo.common.extension.SPI;

/**
 * TODO 支付方式
 */
@SPI("alipay")
public interface Pay {

    void pay();
}

具体实现1

package com.gc.service.impl;

import com.gc.service.Pay;
import org.apache.dubbo.common.extension.Activate;

/**
 * TODO 支付宝支付
 */
@Activate(group = "online", value = "alipay", order = 2)
public class AlipayPay implements Pay {

    @Override
    public void pay() {
        System.out.println("支付宝支付");
    }
}

具体实现2

package com.gc.service.impl;

import com.gc.service.Pay;
import org.apache.dubbo.common.extension.Activate;

/**
 * TODO 银行卡支付
 *
 * @author GC
 * @date 2020/12/21 11:23 上午
 */
@Activate(group = {"online","offline"}, order = 3)
public class CardPay implements Pay {

    @Override
    public void pay() {
        System.out.println("银行卡支付");
    }
}

具体实现3

package com.gc.service.impl;

import com.gc.service.Pay;
import org.apache.dubbo.common.extension.Activate;

/**
 * TODO 现金支付
 *
 * @author GC
 * @date 2020/12/21 11:26 上午
 */
@Activate(group = "offline")
public class CashPay implements Pay {

    @Override
    public void pay() {
        System.out.println("现金支付");
    }
}

具体实现4

package com.gc.service.impl;

import com.gc.service.Pay;
import org.apache.dubbo.common.extension.Activate;

/**
 * TODO 购物券支付
 */
@Activate(group = {"online", "offline"}, order = 1)
public class CouponPay implements Pay {

    @Override
    public void pay() {
        System.out.println("购物券支付");
    }
}

具体实现5

package com.gc.service.impl;

import com.gc.service.Pay;
import org.apache.dubbo.common.extension.Activate;

/**
 * TODO 微信支付
 */

@Activate(group = "online")
public class WechatPay implements Pay {

    @Override
    public void pay() {
        System.out.println("微信支付");
    }
}

编写测试类

package com.gc.service.test;

import com.gc.service.Pay;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;

import java.util.List;
import java.util.Set;

/**
 * TODO 测试类
 */
public class Test {

    //激活所有group为online的扩展类
    /*public static void main(String[] args) {
        ExtensionLoader<Pay> loader = ExtensionLoader.getExtensionLoader(Pay.class);
        URL url = URL.valueOf("xxx://localhost:8080/ooo/jjj");
        List<Pay> online = loader.getActivateExtension(url, "", "online");
        for (Pay pay : online) {
           pay.pay();
        }
    }*/


    //激活所有group为offline的扩展类
    /*public static void main(String[] args) {
        ExtensionLoader<Pay> loader = ExtensionLoader.getExtensionLoader(Pay.class);
        URL url = URL.valueOf("xxx://localhost:8080/ooo/jjj");
        List<Pay> offline = loader.getActivateExtension(url, "", "offline");
        for (Pay pay : offline) {
            pay.pay();
        }
    }*/


    //按照指定的key激活的扩展类
    /*public static void main(String[] args) {
        ExtensionLoader<Pay> loader = ExtensionLoader.getExtensionLoader(Pay.class);
        URL url = URL.valueOf("xxx://localhost:8080/ooo/jjj?xxx=alipay");
        //key 和 group是 或的关系
        List<Pay> offline = loader.getActivateExtension(url, "xxx", "online");
        for (Pay pay : offline) {
            pay.pay();
        }
    }*/


    //获取所有的激活类
    public static void main(String[] args) {
        ExtensionLoader<Pay> loader = ExtensionLoader.getExtensionLoader(Pay.class);
        Set<String> set = loader.getSupportedExtensions();
        System.out.println(set);
    }
}

在查找路径下定义配置信息文件:com.gc.service.Pay

alipay=com.gc.service.impl.AlipayPay
wechat=com.gc.service.impl.WechatPay
card=com.gc.service.impl.CardPay
cash=com.gc.service.impl.CashPay
coupon=com.gc.service.impl.CouponPay

项目结构图

image-20201221121621294

@Activate 的 group 与 value 属性表示该扩展类的两种不同的标识。它们的用法是:

  • 当仅指定了 group 这个大范围标识,当前扩展类可以通过 group 来激活,也可以通过扩展名来激活。
  • 一旦指定了 value 这个小范围标识,其就会将 group 这个大范围标识给屏蔽,即其只能通过扩展名来激活,不能通过 group 来激活。

总结

配置文件中可能会存在四种类:普通扩展类,Adaptive 类,Wrapper 类,及 Activate 类。它们的共同点是:都实现了 SPI 接口。它们的不同点有:

  • 定义方式:Adaptive 类与 Activate 类都是通过注解定义的。
  • 数量:一个 SPI 接口的 Adaptive 类最多只能有一个(无论是自定义的,还是自动生成的),而 Wrapper 类与 Activate 类可以有多个。
  • 直接扩展类:Adaptive 类与 Wrapper 类不是直接扩展类。