为什么要讲SPI呢?因为在Dubbo就用到了SPI机制,所以掌握了这部分对于后面的学习还是很有帮助的。
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。我们先通过一个很简单的例子来看下它是怎么用的。
案例介绍
先定义接口项目
然后创建一个扩展的实现,先导入上面接口项目的依赖
<dependencies> <dependency> <groupId>com.bobo</groupId> <artifactId>JavaSPIBase</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>
然后创建接口的实现
/** * SPI:MySQL对于 baseURL 的一种实现 */ public class MySQLData implements BaseData { @Override public void baseURL() { System.out.println("mysql 的扩展实现...."); } }
然后在resources目录下创建 META-INF/services 目录,然后在目录中创建一个文件,名称必须是定义的接口的全类路径名称。然后在文件中写上接口的实现类的全类路径名称。
同样的再创建一个案例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hLdPTrbV-1626441864846)(img\image-20210716162539114.png)]
然后在测试的项目中测试
public static void main(String[] args) { ServiceLoader<BaseData> providers = ServiceLoader.load(BaseData.class); Iterator<BaseData> iterator = providers.iterator(); while(iterator.hasNext()){ BaseData next = iterator.next(); next.baseURL(); } }
根据不同的导入,执行的逻辑会有不同
源码查看
ServiceLoader
首先来看下ServiceLoader的类结构
// 配置文件的路径 private static final String PREFIX = "META-INF/services/"; // 加载的服务 类或者接口 private final Class<S> service; // 类加载器 private final ClassLoader loader; // 访问权限的上下文对象 private final AccessControlContext acc; // 保存已经加载的服务类 private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); // 内部类,真正加载服务类 private LazyIterator lookupIterator;
load
load方法创建了一些属性,重要的是实例化了内部类,LazyIterator。
public final class ServiceLoader<S> implements Iterable<S> 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(); //实例化内部类 LazyIterator lookupIterator = new LazyIterator(service, loader); } }
查找实现类和创建实现类的过程,都在LazyIterator完成。当我们调用iterator.hasNext和iterator.next方法的时候,实际上调用的都是LazyIterator的相应方法。
private class LazyIterator implements Iterator<S>{ Class<S> service; ClassLoader loader; Enumeration<URL> configs = null; Iterator<String> pending = null; String nextName = null; private boolean hasNextService() { //第二次调用的时候,已经解析完成了,直接返回 if (nextName != null) { return true; } if (configs == null) { //META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件 //META-INF/services/com.viewscenes.netsupervisor.spi.SPIService String fullName = PREFIX + service.getName(); //将文件路径转成URL对象 configs = loader.getResources(fullName); } while ((pending == null) || !pending.hasNext()) { //解析URL文件对象,读取内容,最后返回 pending = parse(service, configs.nextElement()); } //拿到第一个实现类的类名 nextName = pending.next(); return true; } }
创建实例对象,当然,调用next方法的时候,实际调用到的是,lookupIterator.nextService。它通过反射的方式,创建实现类的实例并返回。
private class LazyIterator implements Iterator<S>{ private S nextService() { //全限定类名 String cn = nextName; nextName = null; //创建类的Class对象 Class<?> c = Class.forName(cn, false, loader); //通过newInstance实例化 S p = service.cast(c.newInstance()); //放入集合,返回实例 providers.put(cn, p); return p; } }
看到这儿,我想已经很清楚了。获取到类的实例,我们自然就可以对它为所欲为了!
为帮助开发者们提升面试技能、有机会入职BATJ等大厂公司,特别制作了这个专辑——这一次整体放出。