阅读前提示
为了更好地理解文章,建议先阅读 SPI 概念及案例实践(上)。
在 这篇文章 中我们学习并实践了 Java 原生的 SPI 机制。但我们还想更便捷的使用 SPI。设想这样一个场景:当公司 A 使用互联网连接服务时,我们直接这样调用服务:
1 2 3 4
| InternetService internetService1 = InternetServiceFactory("cn-mobile-beijing");
InternetService internetService2 = InternetServiceFactory("cn-unicom");
|
这样就不需要在 pom.xml
中导入不同的 jar 包以获取实现类,又或者在多个实现类中选择一个具体的服务实现。编写好一个 SPI 框架,我们能更好地调用和扩展 SPI 服务。
本文源代码详见文末。
写一个 SPI 框架
重写 ServiceLoader
现在,公司 A 决定写一套 SPI 框架。
新建模块 simple-company-frame
。在 simple-company-frame
模块 pom.xml
中导入以下模块以提供日志功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <dependencies> <dependency> <groupId>top.uuanqin</groupId> <artifactId>simple-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.16</version> </dependency>
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.3.12</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.32</version> <scope>compile</scope> </dependency> </dependencies>
|
重写 ServiceLoader :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
| package top.uuanqin.frame;
import cn.hutool.core.io.resource.ResourceUtil; import lombok.extern.slf4j.Slf4j;
import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.URL; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap;
@Slf4j public class SpiLoader {
private static Map<String, Map<String, Class<?>>> loaderMap = new ConcurrentHashMap<>();
private static Map<String, Object> instanceCache = new ConcurrentHashMap<>();
private static final String RPC_SYSTEM_SPI_DIR = "META-INF/rpc/system/";
private static final String RPC_CUSTOM_SPI_DIR = "META-INF/rpc/custom/";
private static final String[] SCAN_DIRS = new String[]{RPC_SYSTEM_SPI_DIR, RPC_CUSTOM_SPI_DIR};
public static <T> T getInstance(Class<?> tClass, String key) { String tClassName = tClass.getName(); Map<String, Class<?>> keyClassMap = loaderMap.get(tClassName); if (keyClassMap == null) { throw new RuntimeException(String.format("SpiLoader 未加载 %s 类型", tClassName)); } if (!keyClassMap.containsKey(key)) { throw new RuntimeException(String.format("SpiLoader 的 %s 不存在 key=%s 的类型", tClassName, key)); } Class<?> implClass = keyClassMap.get(key); String implClassName = implClass.getName(); if (!instanceCache.containsKey(implClassName)) { try { instanceCache.put(implClassName, implClass.newInstance()); } catch (InstantiationException | IllegalAccessException e) { String errorMsg = String.format("%s 类实例化失败", implClassName); throw new RuntimeException(errorMsg, e); } } return (T) instanceCache.get(implClassName); }
public static Map<String, Class<?>> load(Class<?> loadClass) { log.info("加载类型为 {} 的 SPI", loadClass.getName()); Map<String, Class<?>> keyClassMap = new HashMap<>(); for (String scanDir : SCAN_DIRS) { List<URL> resources = ResourceUtil.getResources(scanDir + loadClass.getName()); for (URL resource : resources) { try { InputStreamReader inputStreamReader = new InputStreamReader(resource.openStream()); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String line; while ((line = bufferedReader.readLine()) != null) { String[] strArray = line.split("="); if (strArray.length > 1) { String key = strArray[0]; String className = strArray[1]; keyClassMap.put(key, Class.forName(className)); } } } catch (Exception e) { log.error("spi resource load error", e); } } } loaderMap.put(loadClass.getName(), keyClassMap); return keyClassMap; } }
|
相当于一个工具类,提供了读取配置并加载实现类的方法。
关键实现如下:
- 用 Map 来存储已加载的配置信息 键名 => 实现类。
- 扫描指定路径,读取每个配置文件,获取到 键名 => 实现类 信息并存储在 Map 中。
- 定义获取实例方法,根据用户传入的接口和键名,从 Map 中找到对应的实现类,然后通过反射获取到实现类对象。可以维护一个对象实例缓存,创建过一次的对象从缓存中读取即可。
我们扫描的 SPI 目录有两个:
META-INF/my/system/
框架的 SPI 目录
META-INF/my/custom/
用户自定义的 SPI 目录(优先级高)
为框架编写自己 InternetService
的实现类
在 simple-company-frame
模块新建包 internet
,存放与互联网连接相关的代码。
写随便两个公司自己编写的实现类,用于后续测试。比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package top.uuanqin.frame.internet;
import top.uuanqin.InternetService;
public class LocalConnection implements InternetService { @Override public void connectInternet() { System.out.println("访问 127.0.0.1 ......"); } }
|
写一个 InternetService
工厂类
互联网连接服务对象是可以复用的,没必要每次执行连接操作前都创建一个新的对象。所以我们可以使用设计模式中的 工厂模式 + 单例模式 来简化创建和获取互联网连接服务对象的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| package top.uuanqin.frame.internet;
import top.uuanqin.InternetService; import top.uuanqin.frame.SpiLoader;
public class InternetServiceFactory {
static { SpiLoader.load(InternetService.class); }
private static final InternetService DEFAULT_SERIALIZER = new LocalConnection();
public static InternetService getInstance(String key) { return SpiLoader.getInstance(InternetService.class, key); }
}
|
写配置
测试
回到 simple-company
模块。
pom.xml
中引入框架:
1 2 3 4 5
| <dependency> <groupId>top.uuanqin</groupId> <artifactId>simple-company-frame</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
|
编写测试用的主函数:
1 2 3 4 5 6 7 8 9 10 11
| package top.uuanqin;
import top.uuanqin.frame.internet.InternetServiceFactory;
public class SpiMain { public static void main(String[] args) { InternetService internetService = InternetServiceFactory.getInstance("local"); internetService.connectInternet(); } }
|
返回结果:
如果 main
函数代码改成这样,将得到不一样的结果:
1 2 3 4 5 6
| InternetService internetService = InternetServiceFactory.getInstance("fake");
InternetService internetService = InternetServiceFactory.getInstance("ddd");
|
服务拓展
既然公司 A 写了一套框架,那么现在公司 A 要求所有的运营商:
- 把所有连接互联网的 SPI 实现通通放在
META-INF/my/system/
下!
- 实现服务类名前面加上个 Key!
中国移动和中国联通运营商们都乖乖照做了:
框架 simple-company-frame
的 pom.xml
中同时导入了这两个服务供应商的包:
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>cn.mobile</groupId> <artifactId>simple-isp-mobile</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>cn.unicom</groupId> <artifactId>simple-isp-unicom</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
|
下面回到 simple-company
模块测试:
1 2 3 4 5 6 7 8 9 10
| package top.uuanqin;
import top.uuanqin.frame.internet.InternetServiceFactory;
public class SpiMain { public static void main(String[] args) { InternetService internetService = InternetServiceFactory.getInstance("cn-mobile-beijing"); internetService.connectInternet(); } }
|
输出:
1
| 【中国移动(北京移动)】欢迎使用中国移动联网服务!
|
这样,公司 A 可以通过切换 key 随时指定需要的服务了!
用户自定义实现类
公司 A 想自己写几个临时实现类,但是不想让框架 simple-company-frame
导入。这时 META-INF/my/custom/
目录就起作用了。
simple-company
模块新建了自己的临时实现类:
1 2 3 4 5 6 7 8 9 10 11 12
| package top.uuanqin;
public class TempConnection implements InternetService { @Override public void connectInternet() { System.out.println("公司 A 的临时连接。"); } }
|
编写 SPI 配置:
测试:
1 2 3 4 5 6 7 8 9 10 11
| package top.uuanqin;
import top.uuanqin.frame.internet.InternetServiceFactory;
public class SpiMain { public static void main(String[] args) { InternetService internetService = InternetServiceFactory.getInstance("temp"); internetService.connectInternet(); } }
|
输出:
改进与拓展思路
SPI 框架复用
在上面的代码中,SpiLoader
并没有存在于「互联网服务」相关的硬编码。我们可以仿照上面的工厂写法,写出更多的服务工厂。这样我们的 Main 函数就可以是:
1 2 3 4
| InternetService internetService = InternetServiceFactory.getInstance("cn-mobile-suzhou"); TVService tvService = TVServiceFactory.getInstance("xiaomi"); FoodService foodService = FoodServiceFactory.getInstance("kfc");
|
常量存储
写一个接口类,专门存储字符串常量以供使用:
1 2 3 4 5 6 7 8 9 10 11 12 13
| package top.uuanqin.frame.internet;
public interface ConnectionKeys { String FAKE = "fake"; String LOCAL = "local"; String CN_MOBILE_SUZHOU = "cn-mobile-suzhou"; }
|
配合全局配置
结合 这篇文章,我们可以通过修改配置文件直接选择我们需要的服务。
源代码
GitHub仓
库地址:uuanqin/SPI-simple-example (github.com)
本文参考