Java的SPI机制

来源:
三产
最后修订:
2017年11月20日 09:05:51
 1464
Java SPI

本文转载自:Java的SPI机制分析 Java的SPI机制分析

1、问题引出:

     JDK的标准SPI(Service Provider Interface) 被在较多的地方使用到,例如我们常用的JDBC中:

DriverManage.getConnection()这个方法:

@CallerSensitive
public static Connection getConnection(String url,
    java.util.Properties info) throws SQLException {
    return (getConnection(url, info, Reflection.getCallerClass()));
}

继续点进去,会找到:

for(DriverInfo aDriver : registeredDrivers) {
    // If the caller does not have permission to load the driver then
    // skip it.
    if(isDriverAllowed(aDriver.driver, callerCL)) {
        try {
            println("    trying " + aDriver.driver.getClass().getName());
            Connection con = aDriver.driver.connect(url, info);
            if (con != null) {
                // Success!
                println("getConnection returning " + aDriver.driver.getClass().getName());
                return (con);
            }
        } catch (SQLException ex) {
            if (reason == null) {
                reason = ex;
            }
        }

    } else {
        println("    skipping: " + aDriver.getClass().getName());
    }

}

其中核心代码中有:Connection con = aDriver.driver.connect(url, info);

其中aDriver.driver是个接口,所以点进去的connect方法只是个Driver接口里面定义的方法,再看实现类,发现在:

(1)mysql/mysql-connector-java/5.1.36/mysql-connector-java-5.1.36.jar!/com/mysql/fabric/jdbc/FabricMySQLDriver.java中

(2)也在org/mybatis/mybatis/3.3.0/mybatis-3.3.0.jar!/org/apache/ibatis/datasource/unpooled/UnpooledDataSource.java中

这两个并不是jdbc的jar包!查资料可以知道,对于JavaEE定义了JDBC的标准。具体实现是不同提供商提供的。所以java.sql.Driver接口是Java对外公开的一个加载驱动接口,Java并未实现,至于实现这个接口由各个Jdbc厂商去实现就行了,可以解耦,使得更具有灵活性。

正是基于SPI,可以让系统找到具体的实现服务,通过查看具体实现jar包的META-INF/services与接口同名的文件中的内容:

例如mysql 的实现包下:mysql/mysql-connector-java/5.1.36/mysql-connector-java-5.1.36.jar!/META-INF/services/java.sql.Driver的文件内容为:

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

2、Java SPI 思想分析:

(1)当我们的系统里面抽象的各个模块,往往有很多不同的实现方案,比如日志处理模块、xml解析模块、过滤器的模块等,一般我们模块之间是基于接口编程的,模块之间不会对具体实现类进行硬编码。一旦代码里涉及到具体实现类时,就违反了可插拔的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。Java SPI 就是提供了这样一种机制:为某个接口寻找服务实现的机制。

(2)spi规范约定:

当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文 件里就是实现该服务接口的具体实现类。而当外部程序装配这个

模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的 实现类名,并装载实例化,完成模块的注入。 基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

(3)使用场景:

a、jdbc

b、common-logging

c、Dubbo等

(4)简单的例子:

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();
        if (searchs.hasNext()) {
            System.out.println("test");
            Search search = searchs.next();
            search.search("test");
        }  
    }  
}

3、JDK标准的SPI有一定不足,Dubbo基于JDK的SPI机制做了一定的扩展:

(1)JDK标准的SPI会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。

(2)如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK标准的ScriptEngine,通过getName();获取脚本类型的名称,但如果RubyScriptEngine因为所依赖的jruby.jar不存在,导致RubyScriptEngine类加载失败,这个失败原因被吃掉了,和ruby对应不起来,当用户执行ruby脚本时,会报不支持ruby,而不是真正失败的原因。

(3)增加了对扩展点IoC和AOP的支持,一个扩展点可以直接setter注入其它扩展点。

Dubbo约定:

在扩展类的jar包内,放置扩展点配置文件:META-INF/dubbo/接口全限定名,内容为:配置名=扩展实现类全限定名,多个实现类用换行符分隔。 

(注意:这里的配置文件是放在你自己的jar包内,不是dubbo本身的jar包内,Dubbo会全ClassPath扫描所有jar包内同名的这个文件,然后进行合并)

扩展Dubbo的协议示例: 

在协议的实现jar包内放置文本文件:META-INF/dubbo/com.alibaba.dubbo.rpc.Protocol,内容为:

xxx=com.alibaba.xxx.XxxProtocol