Java中的SPI机制

概念

Java SPI(Service Provider Interface)是 Java 平台中的一种机制,用于实现模块化、可插拔的架构。它允许开发人员定义服务接口,并通过特定的配置文件,让不同的实现者提供其实现。这种方式使得应用程序能够在运行时动态地发现和加载服务实现,而无需在编译时确定具体的实现类。

Java里面很多场景都用到了该机制,典型的如JDBC,Java提供了标准的驱动接口(java.sql.Driver),不同的数据库厂商可以提供对应的实现,比如MySQL,Oracle,SQLServer等。

Alt

代码示例

概念很简单,下面我们动手写个简单的代码示例,来看下SPI是如何运作的。

我们首先定义一个日志服务接口,很简单就一个log方法:

/**
 * Log Service
 */
public interface Log {
    void log(String message);
}

接下来我们提供两个不同的实现类:

/**
 * 文件日志:将日志写入到文件
 */
public class FileLog implements Log {
    @Override
    public void log(String message) {
        System.out.println("FileLog: " + message);
    }
}
/**
 * ELK日志:将日志写入ELK日志中心
 */
public class ELKLog implements Log{
    @Override
    public void log(String message) {
        System.out.println("ELKLog: " + message);
    }
}

现在服务有了,服务实现也有了,那么Java里面如何发现并加载服务实现呢?其实很简单,在resource/META-INF/services目录下,新建一个以服务接口全限定类名命名的文件,文件的内容就是具体实现类的全限定类名,r如果有多个实现类,则用换行进行分割:

# 文件位于:resource/META-INF/services/com.haoyanbing.spi.Log
com.haoyanbing.spi.FileLog
com.haoyanbing.spi.ELKLog

接下来,我们写个main方法来试一下:

public static void main(String[] args) {
    // 使用Java提供的工具类ServiceLoader进行加载
    ServiceLoader<Log> serviceLoader = ServiceLoader.load(Log.class);
    // 通过迭代器的方式遍历找到的具体实现类
    Iterator<Log> iterator = serviceLoader.iterator();
    while (iterator.hasNext()) {
        Log log = iterator.next();
        log.log("hello world");
    }
}

控制台输出如下:

FileLog: hello world
ELKLog: hello world

如输出所示,我们写的两个实现类都被ServiceLoader发现并加载到了。

SPI在JDBC中的应用

JDBC是Java中SPI应用的典型场景,我们来回顾下我们JDBC连接的样板代码:

Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
   // 注册驱动 从jdk6开始不需要这一行
   // Class.forName("com.mysql.cj.jdbc.Driver");
  
   // 获取连接
   connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username", "password");
  
   // 获取Statement
   statement = connection.createStatement();
  
   // 执行查询SQL,并返回结果ResultSet
   resultSet = statement.executeQuery("select * from t");
  
   // 打印结果
   while (resultSet.next()) {
      System.out.println(resultSet.getString(1));
   }
} finally {
   // 资源关闭操作
   if (resultSet != null) {
      resultSet.close();
   }
   if (statement != null) {
      statement.close();
   }
   if (connection != null) {
      connection.close();
   }
}

从JDK6开始,我们不再需要注册驱动的那一行代码Class.forName("com.mysql.cj.jdbc.Driver")也可以正常运行,就是靠SPI机制来实现的。

我们到源码里面去看一下具体如何实现的,我们到DriverManager.getConnection()方法里面看一下:

public static Connection getConnection(String url,
    String user, String password) throws SQLException {
    java.util.Properties info = new java.util.Properties();

    if (user != null) {
        info.put("user", user);
    }
    if (password != null) {
        info.put("password", password);
    }
		
  	// 这里调了重载的方法
    return (getConnection(url, info, Reflection.getCallerClass()));
}
private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
  
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    if (callerCL == null || callerCL == ClassLoader.getPlatformClassLoader()) {
        callerCL = Thread.currentThread().getContextClassLoader();
    }

    if (url == null) {
        throw new SQLException("The url cannot be null", "08001");
    }

    println("DriverManager.getConnection(\"" + url + "\")");

    // 我们重点开这一行代码,这里就是确保驱动正确初始化了
    ensureDriversInitialized();

    SQLException reason = null;
    for (DriverInfo aDriver : registeredDrivers) {
        if (isDriverAllowed(aDriver.driver, callerCL)) {
            try {
                println("    trying " + aDriver.driver.getClass().getName());
                Connection con = aDriver.driver.connect(url, info);
                if (con != null) {
                    println("getConnection returning " + aDriver.driver.getClass().getName());
                    return (con);
                }
            } catch (SQLException ex) {
                if (reason == null) {
                    reason = ex;
                }
            }
        } else {
            println("    skipping: " + aDriver.driver.getClass().getName());
        }
    }

    if (reason != null)    {
        println("getConnection failed: " + reason);
        throw reason;
    }

    println("getConnection: no suitable driver found for "+ url);
    throw new SQLException("No suitable driver found for "+ url, "08001");
}
private static void ensureDriversInitialized() {
    if (driversInitialized) {
        return;
    }

    synchronized (lockForInitDrivers) {
        if (driversInitialized) {
            return;
        }
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty(JDBC_DRIVERS_PROPERTY);
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
								// 就是在这里加载驱动
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                // 因为ServerLoader使用的是懒加载,要等到遍历该类的时候才会去加载
                try {
                    while (driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch (Throwable t) {
                    // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers != null && !drivers.isEmpty()) {
            String[] driversList = drivers.split(":");
            println("number of Drivers:" + driversList.length);
            for (String aDriver : driversList) {
                try {
                    println("DriverManager.Initialize: loading " + aDriver);
                    Class.forName(aDriver, true,
                            ClassLoader.getSystemClassLoader());
                } catch (Exception ex) {
                    println("DriverManager.Initialize: load failed: " + ex);
                }
            }
        }

        driversInitialized = true;
        println("JDBC DriverManager initialized");
    }
}

MySQL驱动实现类被加载的时候,会自己把自己注册到java的DriverManager里面,所有我们通过DriverManager.getConnection()就可以获取到连接,我们看下MySQL实现的Driver类:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
		// 该静态代码块,在类被加载的时候会执行
    // 代码逻辑很简单,就是把自己注册到DriverManager里面去
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

后面通过DriverManager获取Connection的流程就不过多分析了。这就是SPI自动发现并加载JDBC驱动的整个流程。

ServiceLoader源码分析

知道了SPI的原理,也了解了Java中典型的应用场景,接下来我们具体看下ServiceLoader的源码,它是如何发现并加载类的。

按我们上面说的,其实不难想象,大致流程其实就是,ServiceLoader拿到需要加载的目标接口,然后在类路径扫描所有META-INF/services/xxx.xxx.xxx的文件,再将文件里面的内容读取到,进行处理,再逐一加载类。

我们从ServiceLoader.load()方法开始看:

// 只截取了相关代码,省略了无关代码
public final class ServiceLoader<S> implements Iterable<S>{
  	// 缓存加载的服务实现类
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 懒加载迭代器,加载类是通过这个来实现的
    private LazyIterator lookupIterator;	
  
  	// 我们调用的方法
		public static <S> ServiceLoader<S> load(Class<S> service) {
        // 获取线程上下文类加载器(后面会具体讲)
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
      	// 调用了重载的方法
        return ServiceLoader.load(service, cl);
    }

    public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
      	// 直接创建了一个 ServiceLoader 对象并返回
        return new ServiceLoader<>(service, loader);
    }
  	
  	// 构造方法,里面对参数做了一些校验然后调用了 reload()方法
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }
  
    // 这里清了一下缓存,创建了一个查找迭代器
    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }
}

看到这里,我们发现这个时候只是创建了一个ServiceLoader对象,并没有执行任何的加载,所以ServiceLoader是懒加载的,等到遍历的时候才会去加载,这也就是上面JDBC里面加载完要遍历一下的原因了。

那我们再看一下hasNext()next()方法:

public Iterator<S> iterator() {
    // 返回一个匿名内部类
    return new Iterator<S>() {

        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();
				
      	// 这里主要调的是lookupIterator里面的hasNext方法
        public boolean hasNext() {
            // 这个判断是已经加载过了才进这个判断,我们第一次加载providers是空的,所以进不了这个判断
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }
				
      	// 同样,next调用的也是lookupIterator里面的next方法
        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }

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

那我们到lookupIterator这个类里面去看看吧:

// 这个是个私有的内部类,实现了对服务实现的懒加载,为了方便阅读,把方法的顺序做了调换
private class LazyIterator implements Iterator<S>{
    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;

    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }

  	// 这个方法调用了内部的hasNextService方法
    // 里面的if-else判断主要是安全策略的逻辑,我们暂不去管他,
    // 不管if还是else最终都是调用hasNextService方法
    public boolean hasNext() {
        if (acc == null) {
            return hasNextService();
        } else {
            PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                public Boolean run() { return hasNextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }
  
    private boolean hasNextService() {
      	// 初始nextName为空
        if (nextName != null) {
            return true;
        }
        // 初始configs也为空
        if (configs == null) {
            try {
              	// 这个的常量 PREFIX = "META-INF/services/"
                // fullName就是我们需要查询的文件名
                String fullName = PREFIX + service.getName();
                // 这里查询所有符合名称条件的文件,返回的是Enumeration<URL>
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else
                    configs = loader.getResources(fullName);
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }
        }
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
         		// 这个调用了内部的parse方法,这个方法很简单就是把文件内容读取并返回一个Iterator<String>
            // 不是直接返回String而是Iterator<String>,是因为文件里面可以写多个实现类
            pending = parse(service, configs.nextElement());
        }
        nextName = pending.next();
        return true;
    }

  	// next方法也是直接调用了nextService方法
  	public S next() {
        if (acc == null) {
            return nextService();
        } else {
            PrivilegedAction<S> action = new PrivilegedAction<S>() {
                public S run() { return nextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }
  
    private S nextService() {
      	// 简单的判断是否已经遍历完成
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
      	// 这里就是加载类了
        try {
            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");
        }
        // 实例化该实现类并放入到providers中进行缓存
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service, "Provider " + cn + " could not be instantiated", x);
        }
        throw new Error();          // This cannot happen
    }

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

到这里整个服务的发型和加载流程就走完了,整个流程还是非常简单和清晰的。

线程上下文加载器

这里面设计到一个类加载器的知识,这里简单提一下。Java里面一共有四种类加载器,类加载器之间是有父子关系的,按照从上到下分别是:BootstrapClassLoader、ExtClassLoader、AppClassLoader和用户自定义的类加载器。

每个加载器能加载类的范围是有限制的,例如:BootStrapClassLoader只能加载jdk自己的类,不能加载用户自己编写的类和第三方jar包里面的类。

类加载器里面有一条规则,是双亲委托机制,就是每个加载器在加载一个类的时候,首先是交由自己的父加载器进行加载,如果父加载器加载不了再由子加载器加载。而且一个类去加载另外一个类的时候,默认是使用加载自己的类加载器去加载。

回到我们的SPI上面,ServiceProvider类去加载各个厂商的具体实现时,如果不指定类加载器,直接去加载则是用的BootStrapClassLoader这个类加载器,但是这个类加载器是不能加载三方jar包里面的类的。那么这样SPI岂不是没办法玩了。

所以Java提供了线程上下文类加载器,也就是可以从线程上下文里面获取类加载器来加载类,这样就绕过了BootStrapClassLoader不能加载三方类的限制。

关于类加载器相关的知识大家可以查询资料再详细了解,这里不做展开说明。

总结

本文先介绍了SPI的概念,然后通过简单的代码示例,来说明SPI如何使用。接着分析了JDBC这个在Java中典型的SPI应用场景,然后我们把ServiceLoader的源码也进行了详细的分析,最后也提到了线程上下文类加载器的相关知识。通过概念讲解,代码示例分析,典型应用场景分析,源码分析等多个方面来说明SPI机制,算是比较详细的了,希望对大家理解SPI能有所帮助!

  • 17
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值