dubbo源码分析2(jdk原生spi机制)

  jdk中有一个spi的机制,可能很多人听都没听过,我以前也没有听说过,我擦(╯—﹏—)╯(┷━━━┷

  因为一个接口可以有很多个不同的实现类嘛,而spi机制的作用就是使用配置文件可以动态的加载实现类;

  而dubbo中对java原生的spi机制进行了扩充,后面我们会看到dubbo源码中spi机制无处不在;

  现在我们先学习一下java原生的spi机制

1.java原生的spi

  首先我们需要创建一个maven项目,什么依赖都不需要,能打印出hello world就行了

 

  然后我们新建一些文件,如下图所示

 

     一个接口,两个实现类:

package com.protagonist;

public interface ISayName {
    void say();
}

 

package com.protagonist;

public class SayEnglishName implements ISayName{
    @Override
    public void say() {
        System.out.println("English:hello cool java boy");
    }
}

  

package com.protagonist;

public class SayChineseName implements ISayName {
    @Override
    public void say() {
        System.out.println("中文:哈喽,你好帅呀๑乛◡乛๑");
    }
}

 

  执行结果下图所示,可以看到正确的加载到了配置文件里面的所有实现类,然后分别调用它们的say方法;

 

2.java中spi机制的分析

  spi全称是Service Provider Interface,这个是针对厂商或者插件的一种机制,用于一些服务提供给第三方实现或者扩展,更多的内容我就不去复制粘贴了,可以看看这篇博客 ,说的比较通俗易懂,嘿嘿๑乛◡乛๑

  举个例子,jdk提供数据库驱动的接口, 然后不同的公司根据这些接口实现自己的产品,例如mysql,oracle驱动就是最经典的了;

  下面我们可以简单的看一下mysql驱动加载的时候,首先导入依赖

<dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.11</version>
</dependency>
              

  

  一般我们jdbc的原始代码是这样的:

public static void test() {
        private String URL = "jdbc:mysql://localhost:3306/T_USER?useUnicode=true&characterEncoding=UTF-8";
        private String USER = "root";
        private String PASSWORD = "123456";
       
        Class.forName("com.mysql.jdbc.Driver");              //1 加载数据库驱动(现在的jdbc已经不需要显示的加载驱动了,这一行可以不要)
        Connection connection = DriverManager.getConnection(URL,USER,PASSWORD); //    2 获取链接connection
        PreparedStatement preparedStatement = connection.prepareStatement("insert into test (name, sex) values (?,?)"); // 3 通过statement对象执行sql
        preparedStatement.setString(1, "xx");
        preparedStatement.setString(2, "yy");
        Boolean result = preparedStatement.execute();      // 4 获取返回结果
    }

  

  然后我们可以看看java.sql.DriverManager这个类,这个类就是我们使用jdbc时候,获取连接的:

 

 

 

 

 

  由此看来只要是自己手动的使用jdbc或者持久层框架中封装了这句代码:DriverManager.getConnection(URL,USER,PASSWORD)  ,去获取数据库连接,启动服务的时候,就会去遍历所有jar包下的META-INF/services目录,找到文件名称为java.sql.Driver的文件,取出其中所有实现类的全路径,然后去实例化就可以使用了;

  那可能有的人就会又问了,为啥非要是META-INF/services目录呀,你猜的吧?(-_-メ)

  其实我们可以跟着ServiceLoader.load方法一直往里面看看

 

 

 

 

 

 

 

 

 

 

 

 

   

  到这里应该就知道为什么是加载META-INF/services/目录下了吧!

  有兴趣的还可以看看ServiceLoader类的parse和parseLine方法,这里是详细的解析META-INF/serivces/下文件内容的,下图所示:

 

 

  在解析了文件中所有的实现类的全路径的时候,返回的是一个List<String>, 里面存放的就是一个个实现类的全路径,然后我们在调用ServiceLoader迭代器方法做循环的时候,其实就是使用反射Class.forName(cn, false, loader)的方式去动态的加载实现类

 

 

 

 

 

3. 打破双亲委派机制

  不知道大家有没有看到上图中Class.forName(cn, false, loader),这一行代码有个loader,这是一个类加载器(是在最开始load方法的时候就实例化的),大家知道这里为什么要有一个类加载器么?或者说这个类加载器有啥用?

 

 

  首先这里默认你已经熟悉了双亲委派机制了,双亲委派机制就是为了保证系统安全,jdk已经定义过的类,我们就不能再写一个相同类名的类了;

  但是这里有个问题,如果有这么几个类,String类,Teacher类,Student类,, 其中String类肯定是要启动类加载器加载的吧,然后另外两个类是应用类加载器加载的,我们可以在Teacher类中使用String name = new Strign("小王老师"), 那么我们可以在String类中使用引用Teacher类和Student吗?

  我们刚刚说的spi就有这个问题,厂商实现的类为什么可以在jdk使用啊?jdk的类都是启动类加载器和扩展类加载器加载的,而厂商实现的类都是应用类加载器加载的。

 

 

  所以jdk给spi打破了这个双亲委托机制,可以把一个ClassLoader置于一个线程的实例之中,使该ClassLoader成为一个相对共享的实例.这样即使是启动类加载器中的代码也可以通过这种方式访问应用类加载器中的类了;

  这里是真的很重要!

 

 

  如果上面说的你可能没有看懂,我也查了很多资料,在一个老哥的博客中有段话说的挺好的,只需要看我贴出来的这部分就可以了(类加载器是组合的哦,不是继承!)

以JDBC加载驱动为例:
在JDBC4.0之后支持SPI方式加载java.sql.Driver的实现类。SPI实现方式为,通过ServiceLoader.load(Driver.class)方法,
去各自实现Driver接口的lib的META-INF/services/java.sql.Driver文件里找到实现类的名字,通过Thread.currentThread().getContextClassLoader()类加载器
加载实现类并返回实例。 驱动加载的过程大致如上,那么是在什么地方打破了双亲委派模型呢? 先看下如果不用Thread.currentThread().getContextClassLoader()加载器加载,整个流程会怎么样。 1.从META-INF/services/java.sql.Driver文件得到实现类名字DriverA 2.Class.forName("xx.xx.DriverA")来加载实现类 3.Class.forName()方法默认使用当前类的ClassLoader,JDBC是在DriverManager类里调用Driver的,当前类也就是DriverManager,
它的加载器是BootstrapClassLoader。 4.用BootstrapClassLoader去加载非rt.jar包里的类xx.xx.DriverA,就会找不到 5.要加载xx.xx.DriverA需要用到AppClassLoader或其他自定义ClassLoader 6.最终矛盾出现在,要在BootstrapClassLoader加载的类里,调用AppClassLoader去加载实现类 这样就出现了一个问题:如何在父加载器加载的类中,去调用子加载器去加载类? 1.jdk提供了两种方式,Thread.currentThread().getContextClassLoader()和ClassLoader.getSystemClassLoader()一般都指向AppClassLoader,
他们能加载classpath中的类 2.SPI则用Thread.currentThread().getContextClassLoader()来加载实现类,实现在核心包里的基础类调用用户代码

 

4.总结

  没想到一个java原生的spi不知不觉的写了这么多(´⊙ω⊙`),就很离谱!

  其实总结一下,spi其实就是讲接口和实现类的定义放在了配置文件中,项目启动的时候,根据接口全名A,就会去找所有jar包中META-INF/services/目录下找到对应的A文件,在A文件中写着有所有对于A的实现类,通过io流的方式读取并解析成List<String>,然后我们调用ServiceLoader的迭代器方法的时候,就会遍历这个集合,取出所有的实现类全路径,通过反射实例化对象,然后调用方法;

  本篇博客还通过mysql驱动实际的看了一下spi的原理,然后而且还说了spi打破了类加载的双亲委派机制,以及jdk中是怎么打破的(在java历史上有好几次打破了双亲委派机制,有兴趣的可以自己了解一下)

  现在说个问题,jdk原生的spi是会将文件中所有实现类都给加载实例化,但是有的时候我们只会使用到其中一个呀?全部加载了多浪费资源啊!这个问题会在dubbo中会解决,dubbo中有一套自己的spi机制,可以说是在jdk基础上优化了性能,而且还给扩展了一些新的功能,后续的再说( ̄▽ ̄)ノ

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章