avatar

42.Dubbo源码分析之核心篇(一)

从这篇起,开始进入Dubbo的源码篇,源码这部分,我准备主要分析Dubbo的服务注册和发布以及服务的调用。该篇是Dubbo源码的第一篇,我们来介绍下Dubbo里面的核心——SPI机制,为后续分析Dubbo其他代码做准备。那么这篇会学到如下的内容:

  1. SPI机制简介
  2. Java中的SPI机制
  3. Dubbo中的SPI机制

0x01 SPI机制简介

  1. SPI是什么?
    • SPI英文全称是Service Provider Interface,它是一种服务的发现机制,简单点说它就是一个扩展点,是给其他人去实现,去扩展的。它是一种动态替换发现机制。
    • 举个例子,我定义了一个接口,但是我本身不实现这个接口,这个接口的实现交给第三方厂商进行,我在运行时需要动态的得到这个接口的实现类(肯定会引入第三方的jar包),这时候该怎么做呢?那我可以定义个规则,这个实现类必须放到classpath下的某个文件夹下,并且要以我这个接口的全类名作为文件名新建一个文件,然后在这个文件中写上我们实现类的全类名。然后我在调用这个实现类的时候,就可以找到对应的目录下的对应文件,再用反射进行加载,不就在运行时动态的得到了改接口的实现类了么?这整个过程就是SPI.
    • 目前在市面上大多数框架都是用它来作为服务的扩展发现,拿JDBC来说,JDK中定义了Driver接口,用来获取数据库的驱动,但是这个Driver接口的实现是不由JDK来管的,因为市面上数据库太多了,交给JDK去实现太复杂了。怎么办呢?那就每个厂商自己实现吧,我JDK定义一个标准(接口),其他的我不管,在要使用的时候我怎么知道要取哪个实现类呢?所以就需要定义一个规则,JDK通过这个规则可以找到该实现类进行加载。
  2. 为什么要Dubbo源码要从SPI开始说起?
    • 因为在Dubbo中运用了大量的SPI机制,实现灵活的扩展,比如协议、负载均衡、序列化、代理的生成等等地方。所以接下来要去深入Dubbo的源码,SPI是必须要弄懂的,不然到时候你就不知道自己在哪里,然后为什么要这样。

0x02 java中的SPI

  1. 上面我们稍稍的讲了下SPI,可能大家伙还是很懵逼。所以接下来我们搞个demo,配合代码来看就好很多了。
  2. 在写demo之前,我们先说说要实现Java的SPI需要注意的点(规则)
    1. 新建一个接口(注意,是接口)
    2. classpath:/META-INF/service路径下新建一个以接口全类名为名的文件
    3. 在文件内部写上改接口的实现类,可以有多个,用换行分割,也就是一行一个实现类
    4. 文件的编码格式是UTF-8
    5. 通过ServiceLoader.load(接口.class)来进行发现和加载。
  3. 下面我们开始准备结合Demo来看看SPI到底是个什么鬼。
    • 先定义一个接口。这里我们就简单来,就IHelloService了。代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      /**
      * @author zyzling
      */
      public interface ISayHelloService {
      /**
      * say hello
      * @return
      */
      public String sayHello();
      }
    • 接下来我们定义两个实现类,为了证明可以有多个。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      /**
      * @author :zyzling
      */
      public class SayHelloServiceImpl implements ISayHelloService {
      @Override
      public String sayHello() {
      return "hello welcome";
      }
      }

      /**
      * @author :zyzling
      */
      public class SayHelloWorldServiceImpl implements ISayHelloService {

      @Override
      public String sayHello() {
      return "hello world";
      }
      }
    • 最后我们按照约定,在classpath:META-INF/service目录下创建一个名为接口全限定名的文件,里面写好我们两个实现类的全限定名。路径如下:

      • 内容如下:
        1
        2
        top.zyzling.provider.SayHelloServiceImpl
        top.zyzling.provider.SayHelloWorldServiceImpl
    • 最后的最后,我们在main方法中进行调用。代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public class App{
      public static void main( String[] args ){
      //通过ServiceLoader来加载我们的接口,
      ServiceLoader<ISayHelloService> serviceLoaders = ServiceLoader.load(ISayHelloService.class);
      //遍历得到我们具体的实现类,然后调用方法
      for (ISayHelloService service : serviceLoaders) {
      System.out.println(service.sayHello());
      }
      }
      }
    • 运行结果如下:

      • 可见,是依次按照我们在文件中书写的顺序来的。那么我们下面开始往深里分析下它的实现吧。
  4. ServiceLoader源码分析
    • 我们先从ServiceLoader.load()方法入手。看看其做了什么。代码如下所示
      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
      public static <S> ServiceLoader<S> load(Class<S> service) {
      //通过线程获取当前的classloader
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      //调用load的重载方法。方法在下面
      return ServiceLoader.load(service, cl);
      }

      //load方法的重载
      public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
      //看上去就new了个ServiceLoader
      return new ServiceLoader<>(service, loader);
      }

      //ServiceLoader的构造方法
      private ServiceLoader(Class<S> svc, ClassLoader cl) {
      //断言
      service = Objects.requireNonNull(svc, "Service interface cannot be null");
      //classLoader处理
      loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
      //jdk安全处理
      acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
      //调用reload方法。代码如下
      reload();
      }

      //重新加载
      public void reload() {
      //清空所有的provider。这个providers是一个LinkedHashMap。定义为:
      //private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
      providers.clear();
      //lookupIterator 实现了Iterator接口,所以直接可以看做是一个迭代器。我们看下他的构造方法
      //private class LazyIterator implements Iterator<S>
      lookupIterator = new LazyIterator(service, loader);
      }

      private LazyIterator(Class<S> service, ClassLoader loader) {
      this.service = service;
      this.loader = loader;
      }
    • 从上面的代码看,好像也没有做什么,只是给对象赋初值而已。最终是返回一个LazyIterator对象,那么我们的文件是什么时候解析的呢?类又是什么时候加载的呢?没错啦,就是遍历的时候进行加载的。因为我们使用时用的增强for,本质上也是在用迭代器,既然是用迭代器,也就是会调用ServiceLoader.iterator()方法,那么就让我们看下吧。分别如下:
      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
      53
      public Iterator<S> iterator() {
      return new Iterator<S>() {
      //获取缓存的迭代器,因为我们new的时候会清空,所以这里肯定是空的。
      Iterator<Map.Entry<String,S>> knownProviders
      = providers.entrySet().iterator();

      public boolean hasNext() {
      //先判断缓存中有没有下一个,没有的话就到lookupIterator中找
      if (knownProviders.hasNext())
      return true;
      //代码在下方
      return lookupIterator.hasNext();
      }

      public S next() {
      //如果缓存中,就直接返回了,不需要在去实例化了。
      if (knownProviders.hasNext())
      return knownProviders.next().getValue();
      //如果缓存中没有,就需要实例化,代码如下
      return lookupIterator.next();
      }

      public void remove() {
      throw new UnsupportedOperationException();
      }

      };
      }

      //------------------------ 以下代码是在lookupIterator中 -------------
      public boolean hasNext() {
      if (acc == null) {
      //调用hasNextService()方法。
      return hasNextService();
      } else {
      PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
      public Boolean run() { return hasNextService(); }
      };
      return AccessController.doPrivileged(action, acc);
      }
      }

      public S next() {
      if (acc == null) {
      //调用nextService()方法
      return nextService();
      } else {
      PrivilegedAction<S> action = new PrivilegedAction<S>() {
      public S run() { return nextService(); }
      };
      return AccessController.doPrivileged(action, acc);
      }
      }
    • 那么依照上面的代码,主要逻辑是在hasNextServie()nextService()方法中。我们先看hasNextService()
      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
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      private boolean hasNextService() {
      //nextName是一个属性,存放我们当前找到的实现类。首次进来肯定是=null的。所以继续往下走
      //nextName的定义为:String nextName = null;
      if (nextName != null) {
      return true;
      }
      //configs是一个Enumeration<URL>。存放我们读到的资源路径。首次进来也是null。所以执行if体内的代码
      if (configs == null) {
      try {
      //拼接路径。PREFIX = META-INF/services/。所以为什么我们的文件要放在这个路径下。
      //service.getName()则是取接口的全限定名,所以为什么我们要以接口的全限定名为文件名。
      String fullName = PREFIX + service.getName();
      //loader就是Classloader。这个在上面new LazyIterator()的时候就已经赋值了
      if (loader == null)
      configs = ClassLoader.getSystemResources(fullName);
      else
      //加载资源
      configs = loader.getResources(fullName);
      } catch (IOException x) {
      fail(service, "Error locating configuration files", x);
      }
      }

      //pending是一个迭代器,定义是这样的:Iterator<String> pending = null;
      //首次进来时肯定也是null。所以进while循环。
      while ((pending == null) || !pending.hasNext()) {
      if (!configs.hasMoreElements()) {
      return false;
      }
      //调用parse解析文件。
      pending = parse(service, configs.nextElement());
      }
      //这里每执行一次,就会得到一个实现类的全限定名
      nextName = pending.next();
      return true;
      }

      //解析方法
      private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError{
      InputStream in = null;
      BufferedReader r = null;
      //names存放我们每个实现类
      ArrayList<String> names = new ArrayList<>();
      try {
      in = u.openStream();
      r = new BufferedReader(new InputStreamReader(in, "utf-8"));
      int lc = 1;
      //读一行,调用parseLine()解析一行到names里面
      while ((lc = parseLine(service, u, r, lc, names)) >= 0);
      } catch (IOException x) {
      fail(service, "Error reading configuration file", x);
      } finally {
      try {
      if (r != null) r.close();
      if (in != null) in.close();
      } catch (IOException y) {
      fail(service, "Error closing configuration file", y);
      }
      }
      //返回迭代器
      return names.iterator();
      }

      //解析
      private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
      List<String> names)throws IOException, ServiceConfigurationError{
      String ln = r.readLine();
      if (ln == null) {
      return -1;
      }
      int ci = ln.indexOf('#');
      if (ci >= 0) ln = ln.substring(0, ci);
      ln = ln.trim();
      int n = ln.length();
      if (n != 0) {
      if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
      fail(service, u, lc, "Illegal configuration-file syntax");
      int cp = ln.codePointAt(0);
      if (!Character.isJavaIdentifierStart(cp))
      fail(service, u, lc, "Illegal provider-class name: " + ln);
      for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
      cp = ln.codePointAt(i);
      if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
      fail(service, u, lc, "Illegal provider-class name: " + ln);
      }
      if (!providers.containsKey(ln) && !names.contains(ln))
      names.add(ln);
      }
      return lc + 1;
      }
      • 上面解析的代码我们就不一行一行分析了。总之就是读取配置文件,然后把我们实现类的全限定名加入到names这个list里面,最后返回一个迭代器。
      • 总结下这个hasNextService()方法吧,如果之前解析过配置,则直接判断是否还有下一个实现类,如果有,就把实现类读到nextName中。如果是第一次进来,没有解析过配置,就会从META-INF/service/目录下找以接口全限定名的文件去解析。然后把使用迭代器的next()读取实现类赋值给nextName
    • 既然配置文件的解析是在hasNext()中,那么实例化一定是在next()方法中,也就是nextService()里面
      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
      private S nextService() {
      if (!hasNextService())
      throw new NoSuchElementException();
      //把nextName,也就是hasNextService中获取到的实现类的全限定名给cn
      String cn = nextName;
      nextName = null;
      Class<?> c = null;
      try {
      //使用Class.forName来加载Class
      c = Class.forName(cn, false, loader);
      } catch (ClassNotFoundException x) {
      fail(service,
      "Provider " + cn + " not found");
      }
      if (!service.isAssignableFrom(c)) {
      fail(service,
      "Provider " + cn + " not a subtype");
      }
      try {
      //创建实例并且强转
      S p = service.cast(c.newInstance());
      //放到providers中,作为缓存。
      providers.put(cn, p);
      //返回实例
      return p;
      } catch (Throwable x) {
      fail(service,"Provider " + cn + " could not be instantiated",x);
      }
      throw new Error(); // This cannot happen
      }
  5. 上面我们已经把ServiceLoader从创建->解析文件->实例化Class已经分析完了。通过上面的使用+分析,我们可以发现java SPI有如下特点:
    • 通过Class获取的是一个迭代器,使用的时候需要迭代。(缺点,使用不方便)
    • 类的实例化是在使用的时候进行的,也就是说会延迟new一个对象。(优点,延迟实例化对象)
    • 每次使用时,都会把配置文件中所有的类进行实例化,不管你后续是否要用到它。(缺点,不使用也创建对象,浪费内存)
  6. 下面我们来说说Dubbo中的SPI机制。

0x03 Dubbo中的SPI

  1. Dubbo实现了自己的一套SPI扩展,他的规则如下:
    • 定义一个接口,在接口上加上@SPI注解
    • classpath:META-INF/dubbo或者classpath:META-INF/dubbo/internal或者classpath:META-INF/services目录下,创建一个文件名为接口的全限定名的文件
    • 在文件中定义key=value形式的字符串。其中key用来在程序中取对应的实现类。value为实现类的全限定名。类似如下:
      1
      2
      helloService=top.zyzling.HelloServiceImpl
      helloWorldService=top.zyzling.HelloWorldServiceImpl
  2. 上面说了规则,那我们现在就来实战一波吧。
    • 首先定义一个接口IHelloService,在接口上加上@SPI注解
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      package top.zyzling.service;
      import org.apache.dubbo.common.extension.SPI;
      @SPI
      public interface IHelloService {
      /**
      * hello
      * @return
      */
      String hello();
      }
    • classpath:META-INF/dubbo目录下创建一个名为top.zyzling.service.IHelloService的文件。如图:
    • 创建两个实现类,HelloServiceImplHelloServiceProvider。分别如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      package top.zyzling.impl;
      public class HelloServiceImpl implements IHelloService {
      @Override
      public String hello() {
      return "[HelloServiceImpl] hello dubbo SPI";
      }
      }

      package top.zyzling.impl;
      public class HelloServiceProvider implements IHelloService {
      @Override
      public String hello() {
      return "[HelloServiceProvider] hello dubbo SPI";
      }
      }
    • 在文件中以key=value的形式来声明我们的实现类。如下:
      1
      2
      helloServiceImpl = top.zyzling.impl.HelloServiceImpl
      helloServiceProvider = top.zyzling.impl.HelloServiceProvider
    • ok,规则就是上面那样,那么我们来在main方法中使用吧。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      public class App {
      public static void main( String[] args ){
      //通过接口获取我们的ExtensionLoader
      //可以类比于我们的ClassLoader,所有的扩展点(接口的实现类)都是由它进行加载
      ExtensionLoader<IHelloService> extensionLoader = ExtensionLoader.getExtensionLoader(IHelloService.class);
      //获取扩展点,参数是我们在文件中定义的key
      IHelloService helloServiceImpl = extensionLoader.getExtension("helloServiceImpl");
      IHelloService helloServiceProvider = extensionLoader.getExtension("helloServiceProvider");
      //调用方法,输出
      System.out.println(helloServiceImpl.hello());
      System.out.println(helloServiceProvider.hello());
      }
      }
    • 结果如下:
  3. 上面我们介绍了dubbo扩展点的一种获取方式,也就是直接使用extensionLoader.getExtension(key)的方式直接获取key所对应的实现类。那么我有一些扩展点我不想他在框架初始化的时候进行实例化,我想在调用接口中的方法时,通过参数来自动选择需要实例化的类,也就是我要先调用该接口的方法,然后在方法中根据参数实例化扩展类,这种情况该怎么办?没错,就是用代理方式解决,我先搞个代理类,在执行方法的时候,在代理类中通过参数,动态的获取真正的扩展类,然后再进行调用。而在dubbo中,这一切都交给了一个@Adaptive注解搞定。那么我们下面就来说说这个。
    • 首先说下它的规则。
      • 还是和上面一样,需要建一个接口,然后在接口上加上@SPI注解
      • @Adaptive可以加在实现类上,也可以加在接口的方法声明。那么这里有什么不同呢?
        • 加在实现类上(Dubbo中不常见):
          • 代表这个类就是我们的代理类,也就是代理类是需要我们手动定义,在这个类里会进行动态的获取真正的扩展类.
        • 加在接口方法声明上(Dubbo中很常见):
          • 代表这个代理类是dubbo使用字节码技术帮我们在运行时自动生成.
          • 代理类的名称是接口名$Adaptive,比如Protocal$Adaptive
      • 获取我们的扩展类时分两步:
        • 先通过ExtensionLoader.getExtensionLoader(接口.class).getAdaptiveExtension()获取我们的代理类,不管这个代理类是自动生成的还是我们定义好的。
        • 再调用代理类的方法,在里面动态获取我们真正的扩展类。在代理类中,也是通过ExtensionLoader#getExtesion(String)来获取真正的扩展类
    • 规则清楚了,按照惯例,先写个demo出来(@Adaptive加在实现类上)。
      • 按照@SPI的规则,我们需要先定义一个接口,简单起见,和上面保持一致。
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        package top.zyzling.service;
        import org.apache.dubbo.common.URL;
        import org.apache.dubbo.common.extension.SPI;
        /**
        * @author :zyzling
        */
        @SPI
        public interface IHelloService {
        /**
        * hello
        * @param url
        * @return
        */
        String sayHello(URL url);
        }
      • 然后定义我们的两个实现类
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        package top.zyzling.imp;
        public class HelloSeriviceImpl implements IHelloService {
        @Override
        public String sayHello(URL url) {
        return "[HelloServiceImpl] url:"+url;
        }
        }
        //----------------------------------
        public class HelloServiceProvider implements IHelloService {
        @Override
        public String sayHello(URL url) {
        return "[HelloServiceProvider] url:"+url;
        }
        }
      • 定义我们的代理类,并在代理类上加上@Adaptive注解
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        package top.zyzling.imp;
        import org.apache.dubbo.common.URL;
        import org.apache.dubbo.common.extension.Adaptive;
        import org.apache.dubbo.common.extension.ExtensionLoader;
        import top.zyzling.service.IHelloService;

        @Adaptive //@Adaptive方法,标识该类为代理类
        public class HelloServiceAdaptive implements IHelloService {

        @Override
        public String sayHello(URL url) {
        System.out.println("[HelloServiceAdaptive] url:"+url);
        String type = url.getParameter("type");
        //根据Type的不同获取不同的实现类
        ExtensionLoader<IHelloService> loader = ExtensionLoader.getExtensionLoader(IHelloService.class);
        IHelloService extension = loader.getExtension(type);
        return extension.sayHello(url);
        }
        }
      • 最后,在classpath:META-INF/dubbo目录下创建一个名为top.zyzling.service.IHelloService的文件,并在里面定义好key和value
        1
        2
        3
        helloServieAdaptive = top.zyzling.imp.HelloServiceAdaptive
        helloServiceImpl = top.zyzling.imp.HelloSeriviceImpl
        helloServiceProvider = top.zyzling.imp.HelloServiceProvider
      • Main方法
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        public class App {
        public static void main( String[] args ){
        ExtensionLoader<IHelloService> loader = ExtensionLoader.getExtensionLoader(IHelloService.class);
        //这时候获取的是一个代理类。可能是我们自己定义的,也有可能是动态生成的,
        //这个需要看@Adaptive注解加在哪。
        IHelloService adaptiveExtension = loader.getAdaptiveExtension();
        //构造一个URL对象,注意是dubbo包下面的。信息都是随便填
        URL implUrl = new URL("http","localhost",8080);
        implUrl = implUrl.addParameter("type","helloServiceImpl");
        System.out.println(adaptiveExtension.sayHello(implUrl));

        System.out.println("-------------------------------");

        URL providerUrl = new URL("http","localhost",8089);
        providerUrl = providerUrl.addParameter("type","helloServiceProvider");
        System.out.println(adaptiveExtension.sayHello(providerUrl));
        }
        }
      • 运行结果:
  4. 除了上面这种自适应扩展,dubbo中还提供了一种激活扩展,也就是指定条件,只有满足了该条件,类实例才会被加载。dubbo中使用@Activate注解来做这事。
    • 规则:
      • 和前面一样,需要在接口上加上@SPI注解,并且也需要在对应目录创建以接口全限定名为文件名的文件,里面定义好key和value。
      • 在实现类上加上@Activate注解,并且通过value属性来指定条件,注意,该条件是一个key,当URL的参数中有该Key时,激活当前扩展@Activate还有其他的属性。分别如下:
        • group:可以通过group过滤,需要配合ExtensionLoader#getActivateExtension(URL, String, String)使用。只有当传递过来的group在配置的group中,才会激活。
        • value:当指定的键出现在URL的参数中时,激活当前扩展名。
        • before:配置相对位置。也就是可以通过这个来配置实现类的顺序
        • after:同上,也是和顺序相关
        • order:配置绝对位置,同上,也是和顺序相关
      • 那么怎么获取激活扩展点呢?可以通过Extension#getActivateExtension(URL,String)来获取。其中String类型的参数代表着key。也就是说,如果我有一个扩展类的@Activate的value为xxx,那么我调用getActivateExtension的时候,传递xxx,就会获取相应的类。
    • demo:
      • 定义个一个接口,加上@SPI注解
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        /**
        * @author :zyzling
        */
        @SPI
        public interface IHelloService {
        /**
        * hello
        * @param url
        * @return
        */
        String sayHello(URL url);
        }
      • 定义连个实现类,在上面加上@Activate注解,一个value属性值为impl,一个为provider.
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        /**
        * @author :zyzling
        */
        @Activate(value="provodier")
        public class HelloServiceProvider implements IHelloService {
        @Override
        public String sayHello(URL url) {
        return "[HelloServiceProvider] activateExtension url:"+url;
        }
        }

        /**
        * @author :zyzling
        */
        @Activate(value="impl")
        public class HelloServiceImpl implements IHelloService {
        @Override
        public String sayHello(URL url) {
        return "[HelloServiceImpl] activateExtension url:"+url;
        }
        }
      • 在对应的目录下创建一个以接口全限定名为文件名的文件,里面的内容如下:
        1
        2
        helloServiceProvider = top.zyzling.impl.HelloServiceProvider
        helloServiceImpl = top.zyzling.impl.HelloServiceImpl
      • main方法
        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));
        }
        }
        }
      • 运行结果

0x04 总结

这篇我们从Jdk的SPI一路说到Dubbo的SPI,也通过一些例子来演示了一下,想必到这里,你应该对SPI有了一定的了解,那么SPI用一句话来说就是:内部对外提供接口,由外部去做具体实现。内部通过定义好的一些规则来对外部实现进行加载。整一系列就是Service Provider Interface

对于dubbo的SPI来说,其实他就分为3种,一种是普遍的通过Key来找对应的实现类(静态扩展点)。一种是提供一个代理类,由代理类利用第一种通过Key来获取对应的实现类,在调用对应的方法(自适应扩展点)。最后一种是通过条件来判断是否需要加载(激活扩展点)。相信大家通过上面的例子,对这三种都有了一定的了解,那么下篇我们就带来Dubbo SPI的源码分析,去了解它们内部的秘密。

知识浅薄,如有表达不当之处,还望指出。谢谢观看,enjoy~


评论