浅谈JAVA中的SPI机制
一、SPI的机制定义
SPI,全称Service Provider Interface, 是Java中提供的一种服务发现机制,它允许应用程序动态地加载和使用第三方提供的服务实现,而无需在代码中显示引用这些实现类。通过将服务接口和其实现分离,从而具备更加好的可扩展性和可维护性。
Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制,将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。允许服务提供者通过实现调用者特定的接口来扩展系统,并且调用者能够动态地发现和加载这些服务提供者的实现类。
二、SPI的实现原理
2.1 Java SPI 实际上是"基于接口的编程+策略模式+配置文件"组合实现的动态加载机制。
2.2 实现SPI的步骤如下:
-
在服务提供者的JAR文件内,创建一个名为
META-INF/services
的目录。 -
在该目录下创建一个文件,文件名为服务接口的全限定名。
-
在该文件中列出服务接口的所有实现类。
2.3 ServiceLoader动态加载接口实现类的原理:类加载ClassLoader+迭代器模式
1. 初始化ServiceLoader类,传入自己想要加载的接口类
ServiceLoader<MyDriver> loader = ServiceLoader.load(MyDriver.class);2. 内部还是用的当前线程上下文的ClassLoader
public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}3. 创建懒加载迭代器LazyIterator
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);
}4. 通过遍历迭代器,寻找服务的实现类
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
}private static final String PREFIX = "META-INF/services/";
5. 发现服务提供者的目录为fullName:"META-INF/services/"+服务类名
private boolean hasNextService() {if (nextName != null) {return true;}if (configs == null) {try {String fullName = PREFIX + service.getName();if (loader == null)configs = ClassLoader.getSystemResources(fullName);elseconfigs = loader.getResources(fullName);} catch (IOException x) {fail(service, "Error locating configuration files", x);}}while ((pending == null) || !pending.hasNext()) {if (!configs.hasMoreElements()) {return false;}pending = parse(service, configs.nextElement());}nextName = pending.next();return true;
}
三、SPI在框架中的使用场景
调用者根据实际使用需要,动态启用或扩展的实现策略
-
日志门面接口实现类加载SLF4J加载不同提供商的日志实现类
-
数据库驱动加载接口实现类的加载JDBC加载不同类型数据库的驱动
-
SpringBoot的SPI机制:我们可以在 spring.factories 中加上我们自定义的自动配置类,事件监听器或初始化器等;
四、实际使用案例:
-
比如我现在有一个获取业务方数据的功能,获取数据的方式和想要的数据信息由我自己决定,但是想要的业务方数据很多,而且数据提供方不确定,也不知道有多少。此时可以采用JAVA中的SPI机制思想,由自己决定获取数据的接口,未来服务方只需要提供这个接口的实现类即可。
调用方接口制定:
public interface ThirdPartyQueryService {/*** 获取第三方服务信息** @param projectId 项目id* @param infoId 信息id* @param userId 用户id* @return 信息*/ThirdPatyData queryThirdPartyData(Integer projectId, Long infoId, Long userId);
}