Dubbo学习笔记(二) SPI

1. Java SPI

1.1 概念

SPI全称是Service Provider Interface,是JDK内置的一种服务提供发现功能,一种动态替换发现的机制。

1.2 规范

使用SPI需要遵循下面的规范:

  • 设置META-INF目录,并在下面新建一个services目录

  • 在上面创建的目录下,放置配置文件

  • 使用ServiceLoad.load()方法进行加载

1.3 示例

首先新建一个maven项目,结构如下:
1572792821854.png

下面分步骤进行描述。

首先新建一个HelloService接口:

public interface HelloInterface {
    public void sayHello();
}

以及该接口的两个实例:

public class ImageHello implements HelloInterface {
    public void sayHello() {
        System.out.println("Image hello!");
    }
}
public class TextHello implements HelloInterface {
    public void sayHello() {
        System.out.println("Text hello");
    }
}

然后在resources目录下的META-INF.services目录下新建一个配置文件,文件名要求是接口全路径名,内容是要实现的接口实现类:

com.spiexample.impl.ImageHello
com.spiexample.impl.TextHello

并且文件是UTF-8编码的。

最后在SPIMain这个启动类中进行测试:

public class SPIMain {
    public static void main(String[] args){
        ServiceLoader serviceLoader = ServiceLoader.load(HelloInterface.class);
        if (serviceLoader != null){
            for(HelloInterface helloInterface : serviceLoader){
                helloInterface.sayHello();
            }
        }
    }
}

最终输出结果为:

Image hello!
Text hello

说明已经成功创建出接口的两个实现类的对象并执行方法了。

2. Dubbo SPI

2.1 介绍

Dubbo SPI相比于Java SPI而言,做了一定的优化和改进。JDK标准的SPI会一次性实例化扩展点所有实现,如果有扩展实现则初始化很耗时,如果没有用上也加载,则浪费资源。如第一小节的例子中,HelloInterface接口有很多实现类,ServiceLoader.load方法返回的serviceLoader对象中封装了所有实现类的实例。所以ServiceLoader实现了Iterable接口,以遍历出所有的实现类。

Dubbo SPI则不然,它只加载配置文件中的类,并分成不同的种类缓存在内存中,而不会全部初始化,在性能上有更好的表现。

下面是一个例子,在第一小节的例子上通过Dubbo SPI进行改进。

2.2 示例

首先修改接口类:

@SPI("impl")
public interface HelloInterface {
    public void sayHello();
}

添加了一个注解,并且设置属性为impl,表示接口默认实现为impl。

@SPI注解:

当该注解标记在接口上时,表示这是一个Dubbo SPI接口,即是一个扩展点。SPI注解有一个value属性,通过这个属性,我们可以传入不同的参数来设置这个接口的默认实现类。

然后再定义一个这个接口的实现类:

public class NewHello implements HelloInterface {
    public void sayHello() {
        System.out.println("new hello!");
    }
}

然后在META-INF目录下新建一个dubbo目录,再新建一个配置文件,文件名也是要求是接口HelloInterface的全路径名,内容是:

impl=com.spiexample.impl.NewHello

impl即对应了接口的一个实现类。

写完这些后,最后main方法中调用如下:

HelloInterface helloInterface = ExtensionLoader.
  getExtensionLoader(HelloInterface.class).
  getDefaultExtension();
helloInterface.sayHello();

最终就成功打印new hello,说明调用实例的就是指定的NewHello。

2.3 规范

Dubbo SPI兼容了Java SPI的配置路径和内容配置方式。在Dubbo启动的时候,会默认扫这三个目录下的配置文件:META-INF/services/,META-INF/dubbo/,META-INF/dubbo/internal/。

3. 扩展点的特性

扩展类一共包含四种特性:自动包装,自动加载,自适应和自动激活。

3.1 自适应

在第二小节中提到,Dubbo通过@SPI("impl")注解中的属性来明确默认使用接口的哪个实现类,那如果实际场景中某些条件使用实现类A,其他条件使用实现类B呢?这种灵活的场景再直接硬编码写死就不合适了。为此下面再看下Dubbo提供的另外一个注解Adaptive,它实现了Dubbo扩展点的一个特性:自适应。

3.1.1 示例

这个注解一般声明在接口的方法上。下面在第二节的示例基础上进行修改。

首先还是定义接口:

@SPI
public interface HelloInterface {
  @Adaptive({"key"})  
  void printInfo(URL url);
}

其中:

  • SPI注解上没有设置默认值了

  • 方法定义上加了一个Adaptive注解,该注解可以传入多个字符串,表示的参数的名字

  • 方法传入了参数,类型为URL。这个URL不是我们常用的那个URL,而是Dubbo自定义的一个类。那这个类有什么用呢?实际上,某一个接口有多个实现类的时候,就是通过往URL里面传参数,来指定我们想调用哪个实现类的该方法的。口说无凭,下面继续看。

之前我们定义了HelloInterface接口的一个实现类,名为NewHello,下面再定义一个:

public class NewHello2 implements HelloInterface {
    public void sayHello() {
        System.out.println("new hello2!");
    }
}

同样的,在dubbo配置文件中加上这个实现类的配置:

impl=com.spiexample.impl.NewHello
impl2=com.spiexample.impl.NewHello2

最后,写一个main方法进行测试:

URL url = URL.valueOf("test://localhost/test").
  addParameter("key","impl2");

HelloInterface helloInterface = ExtensionLoader.
  getExtensionLoader(HelloInterface.class).
  getAdaptiveExtension();
helloInterface.sayHello();

可以看到:

  • 在URL中传入参数key,它的值设置为impl2,因此最后执行helloInterface.sayHello()的时候,实际执行的就是NewHello2.sayHello()
  • 要注意上述代码中获取接口实例是通过调用ExtensionLoader类的getAdaptiveExtension()方法,而不是第二小节的getDefaultExtension()方法。这里涉及到一个概念,叫Dubbo扩展点的自适应特性,这个特性在上述代码中也很好的体现出来了,就是通过Adaptive注解,可以动态地通过URL中的参数来确定要使用的是哪个具体的实现类。

3.2 自动包装

自动包装是Dubbo扩展点的另外一个特性,这个特性实现了装饰器设计模式(关于装饰器模式的思想,可以看另一篇博客的介绍)。

3.2.1 装饰器模式

装饰器模式的编程范式如下:

public class XxxWrapper implements Xxx {
    private Xxx xxx;

  public XxxWrapper(Xxx xxx){
    this.xxx = xxx;
  }
    
  @Override
  ... // 重写的方法
}

装饰器模式最重要的特点就是:构造函数的入参和要实现的接口类型一致。

3.2.2 示例

下面我们在前面的示例基础上修改,体验下这个自动包装是如何实现装饰器模式的。

首先添加一个装饰器类:

public class HelloInterfaceWrapper implements HelloInterface {

    private HelloInterface helloInterface;

    public HelloInterfaceWrapper(HelloInterface helloInterface){
        this.helloInterface = helloInterface;
    }

    @Override
    public void sayHello(URL url) {
        System.out.println("start print");
        helloInterface.sayHello(url);
        System.out.println("end print");
    }
}

一般如果没有使用dubbo的时候,如果想使用上面这个wrapper类来实现一个装饰器模式的时候,首先要做的就是注入一个接口的实现类,然后调用具体的方法的时候,就可以通过这个wrapper类在方法前后做一些额外的操作,实现类似于切面的效果。引入了Dubbo之后,就不需要自己手动注入这个接口实现类了,而是自动注入的。

为了实现自动包装,还是得把这个Wrapper类定义到dubbo的配置文件中:

impl=com.spiexample.impl.NewHello
impl2=com.spiexample.impl.NewHello
common=com.spiexample.impl.XxxWrapper

这样这个接口就有三个实现类了。

最后再把第三小节示例的main方法原样拷贝过来:

URL url = URL.valueOf("test://localhost/test").
  addParameter("key","impl2");

HelloInterface helloInterface = ExtensionLoader.
  getExtensionLoader(HelloInterface.class).
  getAdaptiveExtension();
helloInterface.sayHello();

最终输入如下:

start print
hello world2
end print

可以看到,在hello的前后都打印了start和end,说明先执行了Wrapper类,里面包含了我们指定的imp2对应的那个类。这个就是扩展点的自动包装功能:自动扫描配置文件中定义的类是否是Wrapper类,是的话就用其对实际要调用的类进行自动包装

3.3 自动加载

我们知道Spring依赖注入有构造注入和属性注入等形式。上一小节提到了,Dubbo通过自动包装来实现构造注入,将一个扩展类注入到另一个扩展类中。那除了构造注入外,同样也有属性注入的形式,这种特性在Dubbo中可称为扩展点的自动加载。

下面对前一小节的包装类进行修改如下:

public class HelloInterfaceWrapper implements HelloInterface {

    private HelloInterface helloInterface;
  
    public void setHelloInterface(HelloInterface helloInterface) {
        this.helloInterface = helloInterface;
    }

    @Override
    public void sayHello(URL url) {
        System.out.println("start print");
        helloInterface.sayHello(url);
        System.out.println("end print");
    }
}

修改后,删除了构造函数,只写了一个setter方法,其他代码都不用调整,当main方法调用seyHello方法时发现,最终效果和前面自动包装实现的效果是一样的,这就说明自动加载(我更喜欢称为属性注入)成功了。

3.4 自动激活

@Activate注解可以标记在类,接口,枚举类和方法上。主要使用在有多个扩展点实现,需要根据不同条件被激活的场景中。

如我们使用过滤器都是多个同时使用,即Filter需要多个同时激活,因为每个Filter实现的是不同的功能:

// 无条件自动激活
@Activate
public class XxxFilter implements Filter {
    // ...
}

// 当配置了xxx参数,并且参数为有效值时激活,比如配了cache="lru",自动激活CacheFilter
@Activate(cache="lru")
public class CacheFilter implements Filter {
    // ...
}

// 只对提供方激活,group可选"provider"或"consumer",用这个说明应用已经注册到Dubbo注册中心了
@Activate(group = "provider", value = "xxx")
public class XxxFilter implements Filter {
    // ...
}

定义好扩展类后,就可以通过下面的形式进行获取:

URL url = URL.valueOf("test://localhost/test");
url = url.addParameter("cache", "lru");

// 指明了使用参数cache=lru,group=provider的扩展类
List filters = ExtensionLoader.
  getExtensionLoader(Filter.class).
  getActivateExtension(url,new String[]{},"provider");

可以看到此时返回的扩展类实例是List形式的,即多个的。

4. ExtensionLoader原理

ExtensionLoader是扩展机制的主要逻辑类,前面说到获取扩展类有下面三种方式:

ExtensionLoader.getExtensionLoader(HelloInterface.class).getDefaultExtension();

ExtensionLoader.getExtensionLoader(HelloInterface.class).getAdaptiveExtension();

ExtensionLoader.getExtensionLoader(HelloInterface.class).getActivateExtension();

下面就详细讲解getDefaultExtension,getAdaptiveExtension,getActivateExtension这个三个方法的实现。

4.1 getDefaultExtension

该方法源码如下:

public T getDefaultExtension() {
  this.getExtensionClasses();
  return !StringUtils.isBlank(this.cachedDefaultName) && !"true".equals(this.cachedDefaultName) ? this.getExtension(this.cachedDefaultName) : null;
}

可以看到该方法判断cachedDefaultName变量,进而判断是否调用getExtension方法。在第二小节使用getDefaultExtension方法的示例中,我们给方法添加了SPI注解:

@SPI("impl")
public interface HelloInterface {
    public void sayHello();
}

并且在配置文件中定义:

impl=com.spiexample.impl.NewHello

Dubbo在初始化的时候,会自动扫描带有SPI注解的接口,并且根据注解中指定的参数在配置文件中找到默认要加载的类,并将该类名赋值给cachedDefaultName上。

所以getDefaultExtension方法的关键还是在getExtension方法上。下面就看下该方法的实现:

public T getExtension(String name) {
  if (StringUtils.isEmpty(name)) {
    throw new IllegalArgumentException("Extension name == null");
  } else if ("true".equals(name)) {
    return this.getDefaultExtension();
  } else {
    Holder holder = this.getOrCreateHolder(name);
    Object instance = holder.get();
    if (instance == null) {
      synchronized(holder) {
        instance = holder.get();
        if (instance == null) {
          instance = this.createExtension(name);
          holder.set(instance);
        }
      }
    }

    return instance;
  }
}

该方法中需要关注getOrCreateHolder和createExtension这两个方法的实现。

4.1.1 获取Holder


前面示例中我们传入的参数为:com.spiexample.impl.NewHello,则会调用getOrCreateHolder方法来获取Holder。什么是Holder呢?

public class Holder {
    private volatile T value;

    public Holder() {
    }

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return this.value;
    }
}

看来就只是将要加载的对象包裹起来而已。

接下来再看下getOrCreateHolder方法是如何获取Holder的:

private Holder getOrCreateHolder(String name) {
  Holder holder = (Holder)this.cachedInstances.get(name);
  if (holder == null) {
    this.cachedInstances.putIfAbsent(name, new Holder());
    holder = (Holder)this.cachedInstances.get(name);
  }

  return holder;
}

private final ConcurrentMap> cachedInstances = new ConcurrentHashMap();

代码中用到了一个cachedInstances变量来缓存Holder,如果没有就new一个。

4.1.2 创建扩展对象

createExtension方法源码如下:

private T createExtension(String name) {
  
  // 根据类名获取对应的Class对象,该方法中也定义了cachedClasses变量进行缓存
  Class clazz = (Class)this.getExtensionClasses().get(name);
  if (clazz == null) {
    throw this.findException(name); // 获取不到,则抛异常
  } else {
    try {
      
      // 根据Class对象从缓存对象EXTENSION_INSTANCES中获取实例
      T instance = EXTENSION_INSTANCES.get(clazz);
      if (instance == null) { // 如果对象不存在,则new一个
        EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
        instance = EXTENSION_INSTANCES.get(clazz);
      }

      // 实现扩展点的自动加载特性,如果需要其他扩展类,进行注入
      this.injectExtension(instance);
      
      // 实现扩展点的自动包装特性,创建包装类,并按需将其他扩展类注入到包装类中
      Set> wrapperClasses = this.cachedWrapperClasses;
      Class wrapperClass;
      if (CollectionUtils.isNotEmpty(wrapperClasses)) {
        for(Iterator var5 = wrapperClasses.iterator(); var5.hasNext(); instance = this.injectExtension(wrapperClass.getConstructor(this.type).newInstance(instance))) {
          wrapperClass = (Class)var5.next();
        }
      }

      // 调用每个实例本身的初始化方法
      this.initExtension(instance);
      return instance;
    } catch (Throwable var7) {
      throw new IllegalStateException("...");
    }
  }
}

这个方法还算简单,下面贴出injectExtension方法的源码,分析下它是如何实现自动注入的:

private T injectExtension(T instance) {
  // objectFactory中保存了所有实例化后的扩展对象
  if (this.objectFactory == null) {
    return instance;
  } else {
    try {
      
      // 获取实例定义的所有方法
      Method[] var2 = instance.getClass().getMethods();
      int var3 = var2.length;

      // 遍历所有方法
      for(int var4 = 0; var4 < var3; ++var4) {
        Method method = var2[var4];
        
        //如果是setter方法,并且没有声明禁止注入
        if (this.isSetter(method) && method.getAnnotation(DisableInject.class) == null) {
          
          // 获取setter方法入参类型,即要注入的类名
          Class pt = method.getParameterTypes()[0];
          if (!ReflectUtils.isPrimitives(pt)) {
            try {
              String property = this.getSetterProperty(method);
              // 从objectFactory中获取要注入的类的对象
              Object object = this.objectFactory.getExtension(pt, property);
              if (object != null) {
                // 如果获取了这个要注入的扩展类实例,则调用这个set方法将这个实例注入进去
                method.invoke(instance, object);
              }
            } catch (Exception var9) {
              logger.error("...");
            }
          }
        }
      }
    } catch (Exception var10) {
      logger.error(var10.getMessage(), var10);
    }

    return instance;
  }
}

看了injectExtension这个方法的源码,思路也很清楚,就是根据反射将其他扩展类注入到当前扩展类中。也正是这个方法,从而实现了Dubbo扩展点的两个特性:自动包装和自动加载。

4.2 getAdaptiveExtension

当我们使用扩展点的自适应特性时,获取扩展对象实例就需要使用这个方法。该方法定义如下:

public T getAdaptiveExtension() {
  // 从缓存中获取实例
  Object instance = this.cachedAdaptiveInstance.get();
  if (instance == null) {
    // 该缓存变量也是以Holder形式来包装扩展类对象的
    Holder var2 = this.cachedAdaptiveInstance;
    synchronized(this.cachedAdaptiveInstance) {
      instance = this.cachedAdaptiveInstance.get();
      // 创建实例并放入缓存中
      try {
        instance = this.createAdaptiveExtension();
        this.cachedAdaptiveInstance.set(instance);
      } catch (Throwable var5) {
        throw new IllegalStateException("...");
      }
    }
  }

  return instance;
}

private final Holder cachedAdaptiveInstance = new Holder();

创建实例主要在方法createAdaptiveExtension中实现,下面进行分析。

4.2.1 创建扩展对象

private T createAdaptiveExtension() {
  try {
    return this.injectExtension(this.getAdaptiveExtensionClass().newInstance());
  } catch (Exception var2) {
    throw new IllegalStateException("...");
  }
}

这里调用的injectExtension方法前面也已经分析过了,下面主要看下getAdaptiveExtensionClass方法是如何获取Class对象的:

private Class getAdaptiveExtensionClass() {
  this.getExtensionClasses();
  return this.cachedAdaptiveClass != null ? this.cachedAdaptiveClass : (this.cachedAdaptiveClass = this.createAdaptiveExtensionClass());
}

下面看下createAdaptiveExtensionClass方法的具体实现:

private Class createAdaptiveExtensionClass() {
  String code = (new AdaptiveClassCodeGenerator(this.type, this.cachedDefaultName)).generate();
  ClassLoader classLoader = findClassLoader();
  Compiler compiler = (Compiler)getExtensionLoader(Compiler.class).getAdaptiveExtension();
  return compiler.compile(code, classLoader);
}

看到这里会感到奇怪,为什么会涉及到编译器呢?当我们给方法添加@Adaptive("...")注解并指明参数的时候,Dubbo会以此为依据动态加载扩展点,加载的形式就是在原扩展类的基础上进行编码生成新的扩展类,并使用不同的编译器对其进行编译。

所以基于自适应特性使用的扩展类HelloInterface,最终系统会自动编译生成一个HelloInterface$Adaptive类对象。

关于编译器的实现原理,这里先不展开了,后面涉及到再进行深入分析。

4.3 getActivateExtension

getActivateExtension方法有多个重载,源码如下:

public List getActivateExtension(URL url, String key, String group) {
  String value = url.getParameter(key);
  return this.getActivateExtension(url, StringUtils.isEmpty(value) ? null : CommonConstants.COMMA_SPLIT_PATTERN.split(value), group);
}

其中又调用了getActivateExtension方法,该方法最终会调用getExtension方法,具体代码这里先不分析了,后面涉及到再具体分析。

5. ExtensionFactory

前面提到,ExtensionLoader用法如下:

ExtensionLoader.getExtensionLoader(HelloInterface.class).getDefaultExtension();

前面已经分析了getDefaultExtension方法的实现代码,但是并没有分析getExtensionLoader(...)方法,其源码如下:

public static  ExtensionLoader getExtensionLoader(Class type) {
  if (type == null) {
    throw new IllegalArgumentException("Extension type == null");
  } else if (!type.isInterface()) {
    throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
  } else if (!withExtensionAnnotation(type)) {
    throw new IllegalArgumentException("Extension type (" + type + ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
  } else {
    ExtensionLoader loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
    if (loader == null) {
      EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type));
      loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
    }

    return loader;
  }
}

当我们传入的Class对象不为空时,最终会new一个ExtensionLoader对象出来,构造函数如下:

private ExtensionLoader(Class type) {
  this.type = type;
  this.objectFactory = type == ExtensionFactory.class ? null : (ExtensionFactory)getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension();
}

可以看到里面又调用了getExtensionLoader方法,只不过此时指定了Class类型为ExtensionFactory类。所以我们创建的ExtensionLoader对象中的objectFactory变量是一个ExtensionFactory类型的对象,从这个变量的定义也可以看出这点:

private final ExtensionFactory objectFactory;

ExtensionFactory这个接口定义如下:

@SPI
public interface ExtensionFactory {
     T getExtension(Class var1, String var2);
}

它也加了SPI注解,它的子类有:SpiExtensionFactory,AdaptiveExtensionFactory,SpringExtensionFactory。实际使用时会指定其中一个子类。

我们前面在分析injectExtension方法的时候曾看到:

Object object = this.objectFactory.getExtension(pt, property);

该句代码从objectFactory中获取扩展对象,从而实现了将扩展对象B自动注入到扩展对象A的效果。下面就以SpringExtensionFactory为例,看下getExtension方法是如何获取扩展对象的。

5.1 SpringExtensionFactory

getExtension方法定义如下:

public  T getExtension(Class type, String name) {
    ...
      
    Object bean;
    do {
      if (!var3.hasNext()) {
        return null;
      }

      // 获取Spring上下文
      ApplicationContext context = (ApplicationContext)var3.next();
      // 获取Spring容器中指定的bean对象
      bean = BeanFactoryUtils.getOptionalBean(context, name, type);
    } while(bean == null);

    return bean;
}

从上述代码可知Dubbo可以和Spring进行集成,从Spring容器中获取Bean作为扩展类对象使用。

5.2 SpiExtensionFactory

该类的getExtension方法定义如下:

public  T getExtension(Class type, String name) {
  if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
    ExtensionLoader loader = ExtensionLoader.getExtensionLoader(type);
    if (!loader.getSupportedExtensions().isEmpty()) {
      return loader.getAdaptiveExtension();
    }
  }

  return null;
}

那这个SpiExtensionFactory用在哪里呢?例如,某个扩展点实现类ClassA上有@Adaptive注解,则调用SpiExtensionFactory#getExtension方法会直接返回ClassA对象。上述方法最终调用了ExtensionLoader.getAdaptiveExtension方法。

5.3 AdaptiveExtensionFactory

6. 扩展点动态编译

扩展点的自适应特性使得Dubbo框架非常灵活,而动态编译是自适应特性的基础。因为动态生成的自适应类只是字符串,需要通过编译才能得到真正的Class。虽然Jdk的动态代理也也可以实现类似的功能,但是在性能上和直接编译好的Class会有一定的差距。

为了动态编译成新的扩展类,Dubbo提供了三种代码编译器:JDK编译器,Javassist编译器和AdaptiveCompiler编译器。它们都实现了Compiler接口。

@SPI("javassist")
public interface Compiler {
    Class compile(String var1, ClassLoader var2);
}

可以看到默认加载的是javassist编译器,继承关系如下:
image

6.1 Javassist编译器

Java动态编译生成Class的方式有很多,常见的有:CGLIB,ASM,Javassist等。下面通过一个Javassist的使用示例来帮助理解Dubbo是如何使用它的:

ClassPool classPool = ClassPool.getDefault(); // 初始化Javassist的类池
CtClass ctClass = classPool.makeClass("Hello"); // 创建一个Hello类
CtMethod ctMethod = CtMethod.make("
   public static void test(){
     System.out.println(\"Hello World!\");
   }",ctClass);
ctClass.addMethod(ctMethod);
Class aClass = ctClass.toClass(); // 生成类
Object object = aClass.newInstance(); // 通过反射创建实例
Method m = aClass.getDeclaredMethod("test",null);
m.invoke(object,null); // 调用方法

执行后,打印出“Hello World”。

Dubbo的JavassistCompiler编译器的doCompiler方法大致实现与上类似,这里就不分析了。

6.2 JDK编译器

原生JDK编译器包位于javax.tools下。主要使用JavaFileObject接口,ForwordingJavaFileManager接口,JavaCompiler.CompilationTask方法。Dubbo实现的JdkCompiler类封装了这些原生的编译器实现类,最终将代码字符串编译成具体的类。

6.3 AdaptiveCompiler编译器

你可能感兴趣的:(Dubbo学习笔记(二) SPI)