前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >为什么SpringBoot jar包中的文件读取不到?

为什么SpringBoot jar包中的文件读取不到?

作者头像
一猿小讲
发布2021-10-08 15:22:38
1.1K0
发布2021-10-08 15:22:38
举报
文章被收录于专栏:一猿小讲一猿小讲

1

猿与故事

今天的故事发生在程序猿菜菜身上。

凡是对接过三方的都知道,代码中难免要加载很多三方分配的证书等相关配置文件。

菜菜同学每天的工作便是与三方对接,而且这次的任务是接入 N 套证书相关配置文件。考虑到后期易于上线,于是菜菜开启了从硬编码到易维护的代码优化之路。

2

情景再现

由于特殊的接入诉求,需要获取到证书及属性文件的输入流,你平时都是怎么实现的呢?

菜菜同学代码实现如下。

代码语言:javascript
复制
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 大法,于是乎一坨硬生生的代码出现了(估计有很多同学都这么干,🤭)。

代码语言:javascript
复制
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:如何获取文件对应的输入流?

通过一番思考,菜菜花了一根烟的功夫,把代码撸好了。

代码语言:javascript
复制
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() 输出的路径貌似跟想象中的不一样。

代码语言:javascript
复制
file:/app/yyxjService/lib/yyxj_service-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/cool

然后,通过 FileUtil.listDirs(new File(parentPath)); 获取子目录文件夹结果确实空空如也。

问题很清晰了,通过上面这种方式获取 SpringBoot 打好的 jar 中的资源文件不太可行,需要换方案。

菜菜同学花费了很长时间,百思不得其解,最终找到 Spring 核心包提供的 PathMatchingResourcePatternResolver 类,感觉能解决问题。

代码语言:javascript
复制
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;
}

部署到测试环境上,一探究竟。

代码语言:javascript
复制
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("/") 改进代码如下。

代码语言:javascript
复制
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 真是一件非常幸福的事情。

一起聊技术、谈业务、喷架构,少走弯路,不踩大坑。会持续输出精彩分享,敬请期待!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-09-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一猿小讲 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档