博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
使用SPI解耦你的实现类
阅读量:3960 次
发布时间:2019-05-24

本文共 8949 字,大约阅读时间需要 29 分钟。

什么是SPI机制

最近我建了另一个文章分类,用于扩展JDK中一些重要但不常用的功能。

SPI,全名Service Provider Interface,是一种服务发现机制。它可以看成是一种针对接口实现类的解耦方案。我们只需要采用配置文件方式配置好接口的实现类,就可以利用SPI机制去加载到它们了,当我们需要修改实现类时,改改配置文件就可以了,而不需要去改代码。

当然,有的同学可能会问,spring也可以做接口实现类的解耦,是不是SPI就没用了呢?虽然两者都可以达到相同的目的,但是不一定所有应用都可以引入spring框架,例如JDBC自动发现驱动并注册,它就是采用SPI机制,它就不大可能引入spring来解耦接口实现类。另外,druiddubbo等都采用了SPI机制。

怎么使用SPI

需求

利用SPI机制加载用户服务接口的实现类并测试。

工程环境

JDK:1.8.0_201

maven:3.6.1

IDE:eclipse 4.12

主要步骤

  1. 编写用户服务类接口和实现类;
  2. classpath路径下的META-INF/services文件夹下配置好接口的实现类;
  3. 利用SPI机制加载接口实现类并测试。

创建项目

项目类型Maven Project,打包方式jar

引入依赖

junit
junit
4.12
test

编写用户服务类接口

路径:cn.zzs.spi

public interface UserService {
void save();}

编写接口实现类

路径:cn.zzs.spi。这里就简单实现就好了。

public class UserServiceImpl1 implements UserService {
@Override public void save() {
System.err.println("执行服务1的save方法"); }}// ------------------------public class UserServiceImpl2 implements UserService {
@Override public void save() {
System.err.println("执行服务2的save方法"); }}

配置接口文件

resources路径下创建META-INF/services文件夹,并以UserService的全限定类名为文件名,创建一个文件。如图所示。

UserService接口实现类配置文件

文件中写入接口实现类的全限定类名,多个用换行符隔开。

cn.zzs.spi.UserServiceImpl1cn.zzs.spi.UserServiceImpl2

编写测试方法

路径:test下的cn.zzs.spi。如果实际项目中配置了比较多的接口文件,可以考虑抽取工具类。

public class UserServiceTest {
@Test public void test() {
// 1. 创建一个ServiceLoader对象 ServiceLoader
userServiceLoader = ServiceLoader.load(UserService.class); // 2. 创建一个迭代器 Iterator
userServiceIterator = userServiceLoader.iterator(); // 3. 加载配置文件并实例化接口实现类 while(userServiceIterator.hasNext()) {
UserService userService = userServiceIterator.next(); userService.save(); System.out.println("=================="); } }}

测试结果

执行服务1的save方法==================执行服务2的save方法==================

SPI在JDBC中的应用

本文以mysql 8.0.15版本的驱动来说明。首先,当我们调用Class.forName("com.mysql.cj.jdbc.Driver")时,会去执行这个类的静态代码块,在静态代码块中就会完成驱动注册。

static {
try {
//静态代码块中注册当前驱动 java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) {
throw new RuntimeException("Can't register driver!"); } }

JDK6后不再需要Class.forName(driver)也能注册驱动。因为从JDK6开始,DriverManager增加了以下静态代码块,当类被加载时会执行static代码块的loadInitialDrivers方法。

而这个方法会通过查询系统参数(jdbc.drivers)和SPI机制两种方式去加载数据库驱动。

注意:考虑篇幅,以下代码经过修改,仅保留所需部分。

static {
loadInitialDrivers(); } //这个方法通过两个渠道加载所有数据库驱动: //1. 查询系统参数jdbc.drivers获得数据驱动类名 //2. SPI机制 private static void loadInitialDrivers() {
//通过系统参数jdbc.drivers读取数据库驱动的全路径名。该参数可以通过启动参数来设置,其实引入SPI机制后这一步好像没什么意义了。 String drivers; try {
drivers = AccessController.doPrivileged(new PrivilegedAction
() {
public String run() {
return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) {
drivers = null; } //使用SPI机制加载驱动 AccessController.doPrivileged(new PrivilegedAction
() {
public Void run() {
//读取META-INF/services/java.sql.Driver文件的类全路径名。 ServiceLoader
loadedDrivers = ServiceLoader.load(Driver.class); Iterator
driversIterator = loadedDrivers.iterator(); //加载并初始化类 try{
while(driversIterator.hasNext()) {
// 这里才会去实例化驱动 driversIterator.next(); } } catch(Throwable t) {
// Do nothing } return null; } }); if (drivers == null || drivers.equals("")) {
return; } //加载jdbc.drivers参数配置的实现类 String[] driversList = drivers.split(":"); for (String aDriver : driversList) {
try {
Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex); } } }

mysql的驱动包中,我们可以看到SPI的配置文件。

Driver接口实现类配置文件

源码分析

本文将根据测试例子中方法的调用顺序来分析。

@Test	public void test() {
// 1. 创建一个ServiceLoader对象 ServiceLoader
userServiceLoader = ServiceLoader.load(UserService.class); // 2. 创建一个迭代器 Iterator
userServiceIterator = userServiceLoader.iterator(); // 3. 加载配置文件并实例化接口实现类 while(userServiceIterator.hasNext()) {
UserService userService = userServiceIterator.next(); userService.save(); System.out.println("=================="); } }

注意:考虑篇幅,以下代码经过修改,仅保留所需部分。

创建一个ServiceLoader

我们从load(Class service)方法开始分析,可以看到,调用这个方法时还不会去加载配置文件和初始化接口实现类。因为SPI采用延迟加载的方式,只有去调用hasNext()才会去加载配置文件,调用next()才会去实例化对象。

public static  ServiceLoader load(Class service) {
// 获得当前线程上下文的类加载器 ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } public static ServiceLoader load(Class service, ClassLoader loader) { // 创建一个ServiceLoader对象 return new ServiceLoader<>(service, loader); } private ServiceLoader(Class 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(); } // 存放接口实现类对象。形式为全限定类名=实例对象 private LinkedHashMap
providers = new LinkedHashMap<>(); // 迭代器,有加载和实例化接口实现类的方法 private LazyIterator lookupIterator; public void reload() { // 清空存放的接口实现类对象 providers.clear(); // 创建一个LazyIterator lookupIterator = new LazyIterator(service, loader); } // LazyIterator是ServiceLoader的内部类 private class LazyIterator implements Iterator
{ private LazyIterator(Class service, ClassLoader loader) { this.service = service; this.loader = loader; } }

创建一个迭代器

因为SPI机制采用了延迟加载的方式,所以在没有调用next()之前,providers会是一个空的Map,也就是说以下的knownProviders也会是一个空的迭代器,所以,这个时候都必须去调用lookupIterator的方法,本文讨论的正是这种情况。

public Iterator iterator() {
return new Iterator() {
// providers的迭代器,一般为空 Iterator
> knownProviders = providers.entrySet().iterator(); public boolean hasNext() {
if (knownProviders.hasNext()) return true; return lookupIterator.hasNext(); } public S next() {
if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next(); } public void remove() {
throw new UnsupportedOperationException(); } }; }

加载配置文件

前面已经提到,当调用hasNext()时才会去加载配置文件。那么,我们直接看LazyIteratorhasNext()方法

// 接口类型  	Class service;	// 类加载器	ClassLoader loader;	// 配置文件列表,一般只有一个	Enumeration
configs = null; // 所有实现类全限定类名的迭代器 Iterator
pending = null; // 下一个实现类全限定类名 String nextName = null; public boolean hasNext() {
return hasNextService(); } private boolean hasNextService() {
// 判断是否有下一个实现类全限定类名,有的话直接返回true // 第一次调用这个方法nextName肯定是null的 if(nextName != null) {
return true; } // 下面就是加载配置文件了 if(configs == null) {
// 本文例子中:fullName = META-INF/services/cn.zzs.spi.UserService String fullName = PREFIX + service.getName(); if(loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } // pending是所有实现类全限定类名的迭代器,此时是空 while((pending == null) || !pending.hasNext()) {
// 如果文件中没有配置实现类,直接返回false if(!configs.hasMoreElements()) {
return false; } // 解析配置文件,并初始化pending迭代器 pending = parse(service, configs.nextElement()); } // 将第一个实现类的全限定类名赋值给nextName nextName = pending.next(); return true; }

解析的过程就是简单的IO操作,这里就不再扩展了。

实例化接口实现类

前面已经提到,当调用next()时才会去实例化接口实现类。那么,我们直接看LazyIteratornext()方法。

public S next() {
return nextService(); } private S nextService() {
// 判断是否有下一个接口实现类。因为前面已经有nextName,所以直接返回true if (!hasNextService()) throw new NoSuchElementException(); // 获得下一个接口实现类的全限定类名 String cn = nextName; // 将nextName置空,这样下次调用hasNext()就会重新赋值nextName nextName = null; Class
c = null; // 加载接口实现类 c = Class.forName(cn, false, loader); // 判断是否是指定接口的实现类 if (!service.isAssignableFrom(c)) {
fail(service,"Provider " + cn + " not a subtype"); } // 转化为指定类型 S p = service.cast(c.newInstance()); // 放入providers的Map中 // 前面提到过,只有调用了next()方法,这个Map才会放入元素 providers.put(cn, p); return p; }

以上,SPI的源码基本分析完。

参考资料

本文为原创文章,转载请附上原文出处链接:https://github.com/ZhangZiSheng001/01-spi-demo

你可能感兴趣的文章
屏蔽usb的方法- -
查看>>
JSP编程进度条设计
查看>>
精心收集的面试笔试题库,网络上很难找到这么齐全的,推荐给大家
查看>>
教学视频
查看>>
JS操作Cookie详解
查看>>
Java正则表达式详解
查看>>
myeclipse 快捷键
查看>>
对div排序
查看>>
读写blob类型字段
查看>>
js类型转换
查看>>
spring实例化Bean理解
查看>>
Mac下配置JAVA_HOME
查看>>
fedora 安装mp3播放器插件
查看>>
赏心悦目的宏代码
查看>>
理解套接字recv(),send()
查看>>
发一个C++写的跨平台的BlockingQueue
查看>>
Linux TCP/IP协议栈剖析【体系结构篇】
查看>>
游戏开发中预防内存泄露的一些措施
查看>>
以前的文章全部移除了。
查看>>
几首歌
查看>>