1
猿与故事
今天的故事发生在程序猿菜菜身上。
凡是对接过三方的都知道,代码中难免要加载很多三方分配的证书等相关配置文件。
菜菜同学每天的工作便是与三方对接,而且这次的任务是接入 N 套证书相关配置文件。考虑到后期易于上线,于是菜菜开启了从硬编码到易维护的代码优化之路。
2
情景再现
由于特殊的接入诉求,需要获取到证书及属性文件的输入流,你平时都是怎么实现的呢?
菜菜同学代码实现如下。
public static boolean initEnv() {
Map<InputStream, InputStream> map = Maps.newHashMap();
InputStream certLsStream = NewB.class.getResourceAsStream("/cool/ls/cert_ls.key");
InputStream configLsStream = NewB.class.getResourceAsStream("/cool/ls/config.properties");
map.put(certLsStream, configLsStream);
// ... ...
// 采用 map 进行后续特殊操作(省略)
// ... ...
return true;
}
菜菜通过 Xxx.class.getResourceAsStream("") 轻松实现。
但是,后期业务扩展,三方接入的证书配置会很多,该怎么办?
这当然难不倒菜菜同学,发挥一下 CV 大法,于是乎一坨硬生生的代码出现了(估计有很多同学都这么干,🤭)。
public static boolean initEnv() {
Map<InputStream, InputStream> map = Maps.newHashMap();
InputStream certLsStream = NewB.class.getResourceAsStream("/cool/ls/cert_ls.key");
InputStream configLsStream = NewB.class.getResourceAsStream("/cool/ls/config.properties");
map.put(certLsStream, configLsStream);
InputStream certZsStream = NewB.class.getResourceAsStream("/cool/zs/cert_zs.key");
InputStream configZsStream = NewB.class.getResourceAsStream("/cool/zs/config.properties");
map.put(certZsStream, configZsStream);
InputStream certWwStream = NewB.class.getResourceAsStream("/cool/ww/cert_ww.key");
InputStream configWwStream = NewB.class.getResourceAsStream("/cool/ww/config.properties");
map.put(certWwStream, configWwStream);
InputStream certSqStream = NewB.class.getResourceAsStream("/cool/sq/cert_sq.key");
InputStream configSqStream = NewB.class.getResourceAsStream("/cool/sq/config.properties");
map.put(certSqStream, configSqStream);
InputStream certZlStream = NewB.class.getResourceAsStream("/cool/zl/cert_zl.key");
InputStream configZlStream = NewB.class.getResourceAsStream("/cool/zl/config.properties");
map.put(certZlStream, configZlStream);
// ... ...
// 采用 map 进行后续特殊操作(省略)
// ... ...
return true;
}
不过还真别说,虽然代码很粗糙,但是跑起来却很顺溜。
鉴于菜菜同学怀有一颗程序员的匠心,考虑到后期每次加证书,上线时都要修改代码,便开启代码改进之路。
代码该如何改进呢?如何灵活加载文件呢?菜菜同学陷入了思考。
思考 1:如何获取资源文件夹下所有的子目录?
思考 2:如何获取子目录下的 .key 以及 .properties 文件?
思考 3:如何获取文件对应的输入流?
通过一番思考,菜菜花了一根烟的功夫,把代码撸好了。
private static final String EXTENSIN_KEY = "key";
private static final String EXTENSIN_PROPERTIES = "properties";
public static boolean initEnv() {
String parentPath = NewB.class.getResource("/cool").getPath();
// 1. 获取 cool 目录下所有的子目录(ls、sq、ww等子目录)
String[] subDirAry = FileUtil.listDirs(new File(parentPath));
Map<InputStream, InputStream> map = Maps.newHashMap();
for (String subDir : subDirAry) {
String subFileDir = parentPath + "/" + subDir;
// 2. 获取子目录下的 .key 以及 .properties 文件
Collection files = FileUtils.listFiles(new File(subFileDir), new String[]{EXTENSIN_KEY, EXTENSIN_PROPERTIES}, false);
// 3. 获取文件对应的输入流
InputStream certInputStream = null;
InputStream configInputStream = null;
for (Object obj : files) {
File subFile = (File) obj;
if (EXTENSIN_KEY.equalsIgnoreCase(FilenameUtils.getExtension(subFile.getName()))) {
certInputStream = new FileInputStream(subFile);
} else if (EXTENSIN_PROPERTIES.equalsIgnoreCase(FilenameUtils.getExtension(subFile.getName()))) {
configInputStream = new FileInputStream(subFile);
}
}
// 4. 文件流放入map
map.put(certInputStream, configInputStream);
}
// ... ...
// 采用 map 进行后续特殊操作(省略)
// ... ...
return true;
}
菜菜在本地 Idea 跑起来贼爽,但是一部署到测试环境上就犯傻啦(与测试人员扯皮,怪环境不好使,笑傻)。
菜菜轻声嘀咕:「在本地 IDEA 程序跑着没问题,能够成功读取资源文件,单元测试都跑过了,为啥部署到测试环境就不好使了呢?」
菜菜边嘀咕边开启了 Debug 模式。
首先,发现 NewB.class.getResource("/cool").getPath() 输出的路径貌似跟想象中的不一样。
file:/app/yyxjService/lib/yyxj_service-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/cool
然后,通过 FileUtil.listDirs(new File(parentPath)); 获取子目录文件夹结果确实空空如也。
问题很清晰了,通过上面这种方式获取 SpringBoot 打好的 jar 中的资源文件不太可行,需要换方案。
菜菜同学花费了很长时间,百思不得其解,最终找到 Spring 核心包提供的 PathMatchingResourcePatternResolver 类,感觉能解决问题。
private static final String EXTENSIN_KEY = "key";
private static final String EXTENSIN_PROPERTIES = "properties";
public static boolean initEnv() {
try {
Map<String, Map<String, InputStream>> resourceMap = Maps.newHashMap();
// 1. 定义资源匹配规则,会在所有的JAR包的根目录下搜索指定文件
String matchPattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "/cool/*/*.*";
// 2. 返回指定路径下所有的资源对象(子目录下的资源对象)
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resourcePatternResolver.getResources(matchPattern);
for (Resource resource : resources) {
// 3. 获取文件名称
String fileFlag = resource.getFilename().endsWith(EXTENSIN_KEY) ? EXTENSIN_KEY : EXTENSIN_PROPERTIES;
// 4. 获取子目录名称
String[] filePathAry = resource.getFile().getPath().split("/");
String mb = filePathAry[filePathAry.length - 2];
Map<String, InputStream> mbMap = resourceMap.get(mb);
if (MapUtils.isEmpty(mbMap)) {
mbMap = Maps.newHashMap();
resourceMap.put(mb, mbMap);
}
mbMap.put(fileFlag, resource.getInputStream());
}
// 5. 封装资源文件流的Map
Map<InputStream, InputStream> map = Maps.newHashMap();
resourceMap.forEach((mainBody, inputStreamMap) -> {
map.put(inputStreamMap.get(EXTENSIN_KEY), inputStreamMap.get(EXTENSIN_PROPERTIES));
});
} catch (Exception e) {
return false;
}
// ... ...
// 采用 map 进行后续特殊操作(省略)
// ... ...
return true;
}
部署到测试环境上,一探究竟。
java.io.FileNotFoundException: URL [jar:file:/app/yyxjService/lib/yyxj_service-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/cool/zs/cert_zs.key] cannot be resolved to absolute file path because it does not reside in the file system: jar:file:/app/yyxjService/lib/yyxj_service-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/cool/zs/cert_zs.key
分析原因 resource.getFile().getPath().split("/") ,Spring 没办法通过 File 的形式访问 jar 包里面的文件。
还能怎么实现?Spring 提供的 Resource 提供了 getURL 方法。
采用 resource.getURL().getPath().split("/") 改进代码如下。
private static final String EXTENSIN_KEY = "key";
private static final String EXTENSIN_PROPERTIES = "properties";
public static boolean initEnv() {
try {
Map<String, Map<String, InputStream>> resourceMap = Maps.newHashMap();
// 1. 定义资源匹配规则,会在所有的JAR包的根目录下搜索指定文件
String matchPattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "/cool/*/*.*";
// 2. 返回指定路径下所有的资源对象(子目录下的资源对象)
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resourcePatternResolver.getResources(matchPattern);
for (Resource resource : resources) {
// 3. 获取文件名称
String fileFlag = resource.getFilename().endsWith(EXTENSIN_KEY) ? EXTENSIN_KEY : EXTENSIN_PROPERTIES;
// 4. 获取子目录名称
String[] filePathAry = resource.getURL().getPath().split("/");
String mb = filePathAry[filePathAry.length - 2];
Map<String, InputStream> mbMap = resourceMap.get(mb);
if (MapUtils.isEmpty(mbMap)) {
mbMap = Maps.newHashMap();
resourceMap.put(mb, mbMap);
}
mbMap.put(fileFlag, resource.getInputStream());
}
// 5. 封装资源文件流的Map
Map<InputStream, InputStream> map = Maps.newHashMap();
resourceMap.forEach((mainBody, inputStreamMap) -> {
map.put(inputStreamMap.get(EXTENSIN_KEY), inputStreamMap.get(EXTENSIN_PROPERTIES));
});
} catch (Exception e) {
return false;
}
// ... ...
// 采用 map 进行后续特殊操作(省略)
// ... ...
return true;
}
测试环境部署后,跑起来贼爽,问题完美解决。
只见菜菜同学偷摸给自己竖起了大拇指,为自己点了个大大的赞。
因为菜菜心里最清楚,若后续有新的三方资源文件,只需把资源文件维护下就行了,代码已经实现了动态加载资源文件了,以后上线不用再动了,一劳永逸,so 酷。
3
菜菜侃大山
1、如何获取 SpringBoot jar 包中的指定文件夹下的资源文件子目录?
菜菜曰:借助 Spring 提供的 PathMatchingResourcePatternResolver 类得以解决。
2、获取文件路径时 resource.getFile().getPath() 为啥出现 FileNotFoundException?
菜菜曰:SpringBoot 没办法通过 File 的形式访问 jar 包里面的文件,借助 resource.getURL().getPath() 获取当前资源对应的URL的路径得以解决。
3、若已知道要加载的资源文件的名称与目录,该怎么加载呢?
菜菜曰:Xxx.getClass().getResourceAsStream("") 轻松解决。
程序员就是在解决问题中,能力得以不断提升,所以不要放过任何一个可以让自己成长的机会。久经码场,能静下来写 Bug、找 Bug 真是一件非常幸福的事情。
一起聊技术、谈业务、喷架构,少走弯路,不踩大坑。会持续输出精彩分享,敬请期待!