本文主要是介绍纳尼?这就是SPI,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
引入
我们知道每种数据库都是一个完整的独立的系统,假设我们现在的产品要兼容大多数数据库厂商,比如MySQL,Oracle,DB2等,那么怎么做呢?可以制作一些约定,大家都基于这个约定进行相关操作,比如调用者可以通过约定去连接数据库,而数据库厂商则根据这个约定实现连接数据库的具体操作。
这个看起来有点像设计模式中的门面模式,调用者只需要对这个约定进行相关操作,而具体的实现由相应的厂商实现,假设要从MySQL切换到DB2,只需要在底层悄悄的替换即可,那么具体是怎么替换的呢?
在maven项目中,假设我们目前使用的是MySQL,那么我们的maven依赖是不是有 mysql-connector-java
,换言之,我们可以把这个依赖替换成我们想要的数据库依赖
那么它到底是怎么实现替换依赖就可以切换数据库的呢
以MySQL为例,打开MySQL的jar,看到了META-INF
和 com
目录
进入META-INF/services/ 目录下,发现了 java.sql.Driver 文件
查看就发现了我们熟悉的驱动,而这个驱动就是MySQL厂商实现的
那他是怎么和我们的java应用程序相关联的呢,通过源码,我们发现是实现了java的Driver接口
package com.mysql.cj.jdbc; import java.sql.SQLException;public class Driver extends NonRegisteringDriver implements java.sql.Driver {//// Register ourselves with the DriverManager//static {try {java.sql.DriverManager.registerDriver(new Driver());} catch (SQLException E) {throw new RuntimeException("Can't register driver!");}}/*** Construct a new driver and register it with DriverManager* * @throws SQLException* if a database error occurs.*/public Driver() throws SQLException {// Required for Class.forName().newInstance()}
}
这个接口正好是jdk的拓展包里的
现在可以知道上面描述的约定便是这个 java.sql.Driver
接口。大家都基于这个约定进行相应的操作
这个便是我们常说的SPI机制
SPI
什么是SPI
SPI的全名为Service Provider Interface,当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
JAVA中的实现
在Java中的实现就是ServiceLoader(源码部分省略)
public final class ServiceLoader<S>implements Iterable<S>
{private static final String PREFIX = "META-INF/services/";// The class or interface representing the service being loadedprivate final Class<S> service;// The class loader used to locate, load, and instantiate providersprivate final ClassLoader loader;// The access control context taken when the ServiceLoader is createdprivate final AccessControlContext acc;// Cached providers, in instantiation orderprivate LinkedHashMap<String,S> providers = new LinkedHashMap<>();// The current lazy-lookup iteratorprivate LazyIterator lookupIterator;.........
}
简单来说就是会在jar包下寻找 META-INF/services/
目录下的文件,文件名就是接口名,文件内容就是这个接口的实现类,可以有多个实现类。
解析文件的内容
private Iterator<String> parse(Class<?> service, URL u)throws ServiceConfigurationError{InputStream in = null;BufferedReader r = null;ArrayList<String> names = new ArrayList<>();try {in = u.openStream();r = new BufferedReader(new InputStreamReader(in, "utf-8"));//必须是utf-8的格式int lc = 1;while ((lc = parseLine(service, u, r, lc, names)) >= 0);} catch (IOException x) {fail(service, "Error reading configuration file", x);} finally {try {if (r != null) r.close();if (in != null) in.close();} catch (IOException y) {fail(service, "Error closing configuration file", y);}}return names.iterator();}
在这里进行加载,并且放进缓存中
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");}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
}
实战模拟
这是一个约定,在一个单独的模块中,打jar包
public interface IDataBase {String getDataBaseName();
}
这也是一个单独的模块,依赖上面制定的约定,实现DB2数据库,打jar包
public class DB2 implements IDataBase {public String getDataBaseName() {return "db2";}
}
这也是一个单独的模块,依赖上面制定的约定,实现MySQL数据库,打jar包
public class Mysql implements IDataBase{public String getDataBaseName() {return "mysql";}
}
测试
客户端依赖
<dependencies><dependency><groupId>com.luo</groupId><artifactId>db-interface</artifactId><version>1.0-SNAPSHOT</version></dependency><!--使用DB2--><dependency><groupId>com.luo</groupId><artifactId>db2</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies>
public class Test {public static void main(String[] args) {ServiceLoader<IDataBase> dataBases = ServiceLoader.load(IDataBase.class);//加载指定类的所有实现Iterator<IDataBase> iterator = dataBases.iterator();while (iterator.hasNext()){ //遍历每一个实现IDataBase next = iterator.next();System.out.println(next.getDataBaseName());}}
}
因为我只依赖了DB2,所以输出db2
优缺点
优点:解耦合,懒加载
缺点:会一次性加载配置文件中的所有实现类
这篇关于纳尼?这就是SPI的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!