Java SPI 思想

系统里抽象的各个模块,往往有不同实现方案(日志、xml、过滤器等),模块间应基于接口编程

避免在代码中写死服务提供者(或动态指定具体类名),通过 SPI 服务加载机制进行服务的注册和发现,基于接口编程,实现多个模块的解耦

如果基于具体实现类,违反可插拔原则,替换一种实现,就要修改代码

Java SPI 规范约定

提供者在jar META-INF/services/建接口命名的文件,内容为所有实现类列表

mysql-connector.jar\META-INF\services\java.sql.Driver

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

外部程序装配这模块时,通过 META-INF/services/配置找到实现类名,并装载实例化,完成模块注入

基于约定找到服务接口实现类,而不需代码里制定

jdk 服务实现查找工具类:java.util.ServiceLoader

使用场景

  • jdbc 4.0

4.0 前要 Class.forName(“com.mysql.jdbc.Driver”) 加载驱动,然后获取连接等

4.0 利用 SPI 直接

String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url,username,password);
  • common-logging

META-INF/services/org.apache.commons.logging.LogFactory(是个抽象类)

  • Dubbo(重新实现,扩展了java SPI)

demo

package com.jason.spi;

public interface Search {
    String search(String keyword);
}
package com.jason.spi;

public class FileSearch implements Search {

    @Override
    public String search(String keyword) {
        System.out.println("now use file system search. keyword:" + keyword);
        return null;
    }
}
package com.jason.spi;

public class DatabaseSearch implements Search {
  
    @Override  
    public String search(String keyword) {
        System.out.println("now use database search. keyword:" + keyword);  
        return null;  
    }
}
package com.jason.spi;

import java.util.Iterator;
import java.util.ServiceLoader;

public class SearchTest {
  
    public static void main(String[] args) {  
        ServiceLoader<Search> s = ServiceLoader.load(Search.class);
        Iterator<Search> searchs = s.iterator();
        while (searchs.hasNext()) {
            Search search = searchs.next();
            search.search("test");
        }  
    }  
}

src/main/resources/META-INF/services/com.jason.spi.Search

内容

com.jason.spi.FileSearch com.jason.spi.DatabaseSearch

JDK API缺点,DUBBO增强的部分

  1. JDK SPI 一次性实例化扩展点所有实现,没用上的耗时/耗资源也实例化

  2. 扩展点加载失败,连扩展点的名称都拿不到。JDK ScriptEngine,getName(),如果RubyScriptEngine 依赖的 jruby.jar不存在,RubyScriptEngine实现类加载失败,失败原因被吃掉,用户执行ruby脚本时,会报不支持ruby,而不是真正失败的原因

  3. 扩展点IoC和AOP支持,一个扩展点可setter注入其它扩展点

DUBBO 约定

META-INF/dubbo/接口

内容为:配置名=扩展实现类,多个实现类换行符分隔

配置文件是放在自己的jar包内,不是dubbo本身的jar包内,Dubbo会全ClassPath扫描

DriverManager

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

   AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

DataSource对开发者是API,而对Oracle、MySQL的开发者来说,是SPI

为什么Oracle提供的JDBC驱动不开源,MySQL的jdbc驱动开源

oracle TCP端口默认1521,是公开的。但是具体客户端和server通讯的协议格式是什么,这个是保密的。用户也不用关心

Oracle已经将连接的方法封装好了,提供给用户,比如针对java平台,就提供了ojdbc.jar(同时,也实现了jdbc的规范)

如果ojdbc.jar是开源的,读一下源码,就知道oracle开放接口的协议细节了。这是不必要的,而且也是不安全的

但MySQL自身都是开源的,所以驱动也没有道理不开源

对于C/S架构的应用,其实server-api也就是这么回事:把怎么连接server、提供什么服务封装好,提供给客户端开发者

一般server-api都有2个部分:

  1. 公开的部分,规范一点的还配有详细的文档,让客户端开发者来调用
  2. 私有的部分,是用来处理底层细节的,如建立连接,处理协议等等,这部分不是提供给客户端开发者调用的,但是又必须包含在server-api里

SPI使用步骤总结

  • 组织或公司定义标准
  • 具体厂商或者框架开发者实现
  • 程序猿使用

SPI 本地化扩展

与ServiceLoader 的 SPI 稍有不同,要打包成 jar 包后,放 jre/lib/ext

CalendarData/CalendarName/CurrencyName/LocaleName/LocaleService/ResourceBundleControl/TimeZoneName Provider

参考

Dubbo基于Spring可扩展 Schema提供自定义配置支持分析

https://my.oschina.net/u/3729778/blog/1575550

基于Spring可扩展Schema提供自定义配置支持(spring配置文件中 配置标签支持)

http://www.cnblogs.com/jifeng/archive/2011/09/14/2176599.html