从jar中拷贝资源文件

时间:2022-07-23 15:59:31

why?

在代码中读取各种各样的资源文件对程序猿们来说是屡见不鲜的事情,对于集成环境下(eclipse、idea)这些代码也总是能运行得好好的,一点都不会让程序猿操心,因此很多人会就此打住。直到这些代码被打成jar包A,然后模块B依赖A时可能就会出现找不到资源文件等各种各样的问题。比如说,我们使用System.load来装载库文件的话,即便我们能得到存放资源文件的路径/D:/project/demo.jar!/resource/test.dll,但是这种路径并不能成功的加载。我们可能马上会想到以下的解决办法:
1、把配置文件拷贝一份到模块B里面来呗,简单粗暴。
2、把配置文件放在硬盘某一固定位置,代码里面直接写死路径(或者根据当前环境dev/beta/product读取对应的路径)呗,一劳永逸。
以上方案似乎能解决燃眉之急,然后程序又运行得好好的,一点问题都没有,瞥一眼电脑右下角,原来这么晚了,关机下班走人。。。

对于1方案,我们既然把它独立出来一个模块,肯定是有其原因的,比如这个A要被BCDEFG等模块依赖,难道要我们在BCDEFG等模块都放一份资源文件?如果资源文件经常变动,到时就要各个模块去更新文件,想想都觉得很可怕,有木有?
对于2方案,生产环境上的每台服务器都得丢一份文件上去,会不会有的服务器忘记呢?可能有人说我们用共享。不管怎样,当资源文件变了之后,项目发布之前,你依旧得去更新一下,很难说哪天真的忘记了…

综上,上面两种方案只能适合救急,绝非长久之计。要是程序运行时能自动把jar包里我们需要的资源文件拷贝到某一位置,这样代码就能正常的读取资源了。不管你有几个依赖模块A,不管你最终是war包,还是jar包,只管修改模块A的资源,发布项目的自动拷贝资源,这是就完全不需要额外的操作了。
java.net.URL和java.net.URLConnection类就可以帮我们拿到jar中的资源文件的输入流,然后我们把这个输入流写到指定位置就可以了。

how?

假设部署后模块A的路径在/usr/local/project/demo.jar

1、首先要确定的这个指定位置
getClass().getProtectionDomain().getCodeSource().getLocation()如果直接执行.class文件那么会得到当前class的绝对路径。如果封装在jar包里面执行jar包那么会得到当前jar包的绝对路径。

URL url = getClass().getProtectionDomain().getCodeSource().getLocation();
recourseFolder = java.net.URLDecoder.decode(url.getPath(), "utf-8");

此时我们得到的recourseFolder为/usr/local/project/demo.jar

if (recourseFolder.endsWith(".jar")) {
    recourseFolder = recourseFolder.substring(0, recourseFolder.lastIndexOf('/') + 1);
}

取它的前部分路径/usr/local/project/,此时我们得到的recourseFolder为/usr/local/project/

if (System.getProperty("os.name").toLowerCase().indexOf("linux") >= 0) {
    recourseFolder += EXT;
    /*其他你需要做的事情,比如只拷贝so文件,loadRecourseFromJar("filename.so")*/
} else {
    recourseFolder = recourseFolder.substring(1) + EXT; 
    /*其他你需要做的事情,比如只拷贝dll文件,loadRecourseFromJar("filename.dll")*/
}

因为得到的路径是以/开头的,如果是Windows环境则去掉开头的/,linux的路径就完整保留了,然后创建的EXT文件夹来存放待”释放”出来的资源文件,比如EXT=recourseFromJar,此时我们得到的recourseFolder为/usr/local/project/recourseFromJar。这个路径就是最终存放jar包里面资源文件的位置了。

2、开始拷贝资源文件到指定位置

    public void loadRecourseFromJar(String path) throws IOException {
        if (!path.startsWith("/")) {
            throw new IllegalArgumentException("The path has to be absolute (start with '/').");
        }

        if(path.endsWith("/")){
            throw new IllegalArgumentException("The path has to be absolute (cat not end with '/').");
        }

        int index = path.lastIndexOf('/');

        String filename = path.substring(index + 1);
        String folderPath = recourseFolder + path.substring(0, index + 1);

        // If the folder does not exist yet, it will be created. If the folder
        // exists already, it will be ignored
        File dir = new File(folderPath);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        // If the file does not exist yet, it will be created. If the file
        // exists already, it will be ignored
        filename = folderPath + filename;
        File file = new File(filename);

        if (!file.exists() && !file.createNewFile()) {
            log.error("create file :{} failed", filename);
            return;
        }

        // Prepare buffer for data copying
        byte[] buffer = new byte[1024];
        int readBytes;

        // Open and check input stream
        URL url = getClass().getResource(path);
        URLConnection urlConnection = url.openConnection();
        InputStream is = urlConnection.getInputStream();

        if (is == null) {
            throw new FileNotFoundException("File " + path + " was not found inside JAR.");
        }

        // Open output stream and copy data between source file in JAR and the
        // temporary file
        OutputStream os = new FileOutputStream(file);
        try {
            while ((readBytes = is.read(buffer)) != -1) {
                os.write(buffer, 0, readBytes);
            }
        } finally {
            // If read/write fails, close streams safely before throwing an
            // exception
            os.close();
            is.close();
        }

    }

比如我们想”释放”/usr/local/project/demo.jar里面的myrecourse/aaa.txt文件,我们只需要

loadRecourseFromJar("/myrecourse/aaa.txt");

如果还需要myrecourse/bbb.txt,myrecourse/ccc.txt,你只需要多写两行代码

loadRecourseFromJar("/myrecourse/aaa.txt");
loadRecourseFromJar("/myrecourse/bbb.txt");
loadRecourseFromJar("/myrecourse/ccc.txt");

执行一下代码,你会发现/usr/local/project/recourseFromJar目录下多出一个文件夹myrecourse,而且myrecourse文件夹里面有aaa.txt、bbb.txt、ccc.txt三个文件。
到这里,革命已经成功了一大半。因为资源文件已经拷贝出来了,代码也能正常跑了,看似很完美。但是如果经常往myrecourse里面添加文件X,是不是每次都得为对应的文件添加一行代码loadRecourseFromJar(“/myrecourse/X”);这并不是我们所能接受的,我们需要的是能够拷贝整个文件夹下面的所有文件的工具。

3、拷贝目录下所有文件
其实就是文件夹的遍历问题了…直接上代码…

    public void loadRecourseFromJarByFolder(String folderPath) throws IOException {
        URL url = getClass().getResource(folderPath);
        URLConnection urlConnection = url.openConnection();
        if(urlConnection instanceof FileURLConnection){
            copyFileResources(url,folderPath);
        }else if(urlConnection instanceof JarURLConnection){
            copyJarResources((JarURLConnection)urlConnection);
        }
    }

    /** * 当前运行环境资源文件是在文件里面的 * @param url * @param folderPath * @throws IOException */
    private void copyFileResources(URL url,String folderPath) throws IOException{
        File root = new File(url.getPath());
        if(root.isDirectory()){
            File[] files = root.listFiles();
            for (File file : files) {
                if (file.isDirectory()) {
                    loadRecourseFromJarByFolder(folderPath+"/"+file.getName());
                } else {
                    loadRecourseFromJar(folderPath+"/"+file.getName());
                }
            }
        }
    }

    /** * 当前运行环境资源文件是在jar里面的 * @param jarURLConnection * @throws IOException */
    private void copyJarResources(JarURLConnection jarURLConnection) throws IOException{
        JarFile jarFile = jarURLConnection.getJarFile();
        Enumeration<JarEntry> entrys = jarFile.entries();
        while(entrys.hasMoreElements()){
            JarEntry entry = entrys.nextElement();
            if (entry.getName().startsWith(jarURLConnection.getEntryName()) && !entry.getName().endsWith("/")) {
                loadRecourseFromJar("/"+entry.getName());
            }
        }
        jarFile.close();
    }

现在我们想”释放”/usr/local/project/demo.jar里面的myrecourse文件夹下的所有文件,我们只需要这样

loadRecourseFromJarByFolder("/myrecourse");

执行一下代码,你会发现/usr/local/project/recourseFromJar目录下多出一个文件夹myrecourse,而且myrecourse文件夹里面有aaa.txt、bbb.txt、ccc.txt、X等N个文件。

when

资源的拷贝肯定不需要每次都去操作的,一般在项目启动时执行就行了,等你更新了项目代码,下次启动会自己更新资源文件,然后只需要返回存放资源文件的recourseFolder,其他地方去使用就可以了。比如还是System.load来装载库文件,我们就可以使用System.load(recourseFolder+”/resource/test.dll”);recourseFolder是固定的,”/resource/test.dll”也是固定的,因此每次上线根本不需要额外的去修改现有的代码,尽情的修改资源文件吧,I dont care…

the end

解决问题的方法很多种,你会选哪一种?
如果你的工具只有一柄铁锤,你就可能认为所有的问题都是铁钉。 ——马斯乐