愿寻一山水间,看水听风随流水,再无凡尘乱我心。

Java SPI机制

概述:SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,JDK内置的一种服务提供发现机制。

描述:Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。Java SPI就是提供这样的一个机制:是一种将服务接口与服务实现分离以达到解耦、大大提升了程序可扩展性的机制。引入服务提供者就是引入了spi接口的实现者,通过本地的注册发现获取到具体的实现类,轻松可插拔。所以SPI的核心思想就是解耦。

JavaSPI

理解并分析

常见实现:目前市面上有很多框架都是用它来做服务的扩展发现,JDBC、JCE、JNDI、JAXP和JBI等。

理解: 底层系统的动态服务实现。

需求:系统需要调用实现类。我们知道作为底层系统,我们只提供了约定的API服务。具体实现由使用者提供。我们希望在系统启动时动态获取到服务的实现、而不通过硬编码方式。

目的:实现运行时动态发现并加载服务。

分析:不管怎么实现,肯定得知道服务的实现类。那问题就在怎么知道服务实现类,又不耦合。我们知道在分布式系统中我们经常是将服务的实现注册到注册中心。使用时从注册中心取到具体实现的服务。从而达到服务和服务实现的解耦。但是我们现在说的是底层系统,没有注册中心。我们平时大部分做的项目都是上层服务系统,都是利用各种rpc,分布式协调服务来实现这种动态服务。而jdk作为高度抽象的底层系统,没有那些乱七八糟的各种框架又是怎么实现动态服务的呢?

实现:我们约定一个服务注册的地方,获取时从这个地方获取。这就是注册中心的概念。spi也可以这么理解。我们约定统一将服务放在META-INF/services,添加一个文件,文件名为服务全类名,内容为具体实现全类名。这样系统启动时就能从这个地方去查寻指定的服务实现。

  • SPI约定:要使用Java SPI,需要遵循如下约定

    • 当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
    • 接口实现类所在的jar包放在主程序的classpath中;
    • 主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
    • SPI的实现类必须携带一个不带参数的构造方法;

总结:SPI是jdk核心类实现动态服务发现机制。程序启动时ServiceLoader通过load来动态加载指定的服务实现。实现服务接口和服务实现的解耦。

问题:SPI是ServiceLoader来加载服务实现类。那么问题来了。它是JDK核心类库中的类,根据类加载机制可知,它是被Bootstrap Classloader加载,Bootstrap Classloader主要负责加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类。而服务的实现类是第三方jar提供的,在classpath中Bootstrap Classloader肯定找不到。再由双亲委托机制可知,如果它找不到指定类那就委托父加载器加载。我们知道Bootstrap Classloader已经是最顶层的类加载器。不可能加载到实现类,那SPI是如何实现的呢?

破坏双亲委派类加载机制

结论:从上面的问题我们得出,SPI服务实现通过Java默认推荐的双亲委托机制加载是不可行的。SPI服务API在jdk核心库中由Bootstrap Classloader加载,而服务实现是第三方提 供的在AppClassLoader类加载的路径中。那是不是可以让AppClassLoader来加载呢?这是必然的,它肯定能加载。但是违反了Java默认推荐的双亲委派加载机制。没办法,得到好的东西总是要有些付出的。那么SPI最终的实现就是,Bootstrap Classloader委托AppClassLoader去加载服务实现。


   转载规则


《Java SPI机制》 山卡卡里的码农 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录