XanderYe的个人小站

精简jre之rt.jar

作为一个java开发,让别人的电脑跑java程序是一件很头疼的事,因为官方没有java转exe的工具。虽然有其他第三方软件,但试了试,终归还是不太好用。

最早是用maven插件javafx-maven-plugin,可以打包native程序,看了下结构就是一个exe去调用同级的jre,打包下来就算一个hello world也要200M。

之后打包了一个一键安装jre的自解压程序,脚本自己写的,自动配置环境变量和添加注册表,可以双击jar文件打开java的gui程序。

偶然看到有大佬精简rt.jar压缩程序空间的,于是开始研究,有了点研究成果,分享下。

1.获取加载类

首先就是获取程序运行加载的类文件了,使用java自带的-XX:+TraceClassLoading命令可以监控类加载,导出txt,命令:

java -jar -XX:+TraceClassLoading main.jar >> classes.txt

这里有个坑:因为程序不是一打开就加载所有类的,所以需要进行程序的所有操作,保证所有类加载。

如果是控制台程序并使用了Scanner接收输入的,那还有一个坑:使用了>>命令导出到文件,那么控制台是什么都不显示的,我是打开txt看程序执行到哪一步了,再在控制台盲输,观察txt中的日志;不用>>导出文件,控制台虽然是有输出的,但是在控制台采集数据会丢失,不太好用。

https://wp.xanderye.cn/wp-content/uploads/2020/03/@9KH0@7E7DGHMVT8LR.png

得到几百kB的txt。

2.复制加载类并打包rt.jar

原理是处理txt文件,去掉Opened段落,截取Loaded段落中的包名转换成路径,从rt.jar中复制过去。网上几个java复制的代码都不太精简,我这里重写了下。需要先手动解压rt.jar。

import java.io.*;
import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入classes.txt文件路径(D:\\xxx.txt):");
        // class列表文件
        String classesFilePath = scanner.nextLine();
        System.out.print("请输入rt.jar解压后的rt文件夹路径(D:\\rt):");
        // rt文件夹
        String rtPath = scanner.nextLine();
        // 工作目录
        String newRtPath = System.getProperty("user.dir");
        // 检查目录
        rtPath = checkPath(rtPath);
        newRtPath = checkPath(newRtPath);
        System.out.println("当前工作目录:" + newRtPath);
        try {
            System.out.println("开始复制class");
            int count = copyClasses(classesFilePath, rtPath, newRtPath);
            System.out.println("已复制:" + count);
            Runtime runtime = Runtime.getRuntime();
            String[] jarPackage = {"jar", "cvf", "rt.jar"};
            String[] packages = {"com", "java", "javax", "jdk", "META-INF", "org", "sun"};
            // 合并数组
            String[] command = new String[packages.length + jarPackage.length];
            System.arraycopy(jarPackage, 0, command, 0, jarPackage.length);
            System.arraycopy(packages, 0, command, jarPackage.length, packages.length);
            runtime.exec(command);
            System.out.println("正在后台打包rt.jar");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 读取class文件并复制
     *
     * @param classesFilePath
     * @param rtPath
     * @param targetPath
     * @return int
     * @author XanderYe
     * @date 2020/3/9
     */
    private static int copyClasses(String classesFilePath, String rtPath, String targetPath) {
        File file = new File(classesFilePath);
        int count = 0;
        if (file.exists()) {
            try {
                FileReader fileReader = new FileReader(file);
                BufferedReader bufferedReader = new BufferedReader(fileReader);
                String line;
                while ((line = bufferedReader.readLine()) != null) {
                    line = formatClassPath(line);
                    if (line != null) {
                        String oldPath = rtPath + line;
                        String newPath = targetPath + line;
                        System.out.println("复制:" + newPath);
                        // 复制class
                        copyFile(oldPath, newPath);
                        count++;
                    }
                }
                if (count > 0) {
                    // 复制META-INF
                    copyFolder(rtPath + "META-INF", targetPath + "META-INF");
                }
            } catch (Exception e) {
                System.out.println("class列表文件读取错误");
            }
        } else {
            System.out.println("class列表文件不存在");
        }
        return count;
    }

    /**
     * 格式化类路径
     *
     * @param s
     * @return java.lang.String
     * @author XanderYe
     * @date 2020/3/9
     */
    private static String formatClassPath(String s) {
        if (s.contains("[Loaded ") && (s.contains("rt.jar"))) {
            s = substringBetween(s, "[Loaded ", " ");
            if (s != null) {
                return s.replace(".", File.separator) + ".class";
            }
        }
        return null;
    }

    /**
     * 分割字符串
     *
     * @param str
     * @param open
     * @param close
     * @return java.lang.String
     * @author XanderYe
     * @date 2020/3/9
     */
    private static String substringBetween(String str, String open, String close) {
        if (str != null && open != null && close != null) {
            int start = str.indexOf(open);
            if (start != -1) {
                int end = str.indexOf(close, start + open.length());
                if (end != -1) {
                    return str.substring(start + open.length(), end);
                }
            }
            return null;
        } else {
            return null;
        }
    }

    /**
     * 检查路径
     *
     * @param path
     * @return java.lang.String
     * @author XanderYe
     * @date 2020/3/9
     */
    private static String checkPath(String path) {
        if (!path.endsWith(File.separator)) {
            path += File.separator;
        }
        return path;
    }

    /**
     * 复制单个文件
     *
     * @param oldPath 原文件路径 如:c:/test.txt
     * @param newPath 复制后路径 如:f:/test.txt
     * @return void
     * @author XanderYe
     * @date 2020/3/9
     */
    private static void copyFile(String oldPath, String newPath) {
        try {
            File oldFile = new File(oldPath);
            if (oldFile.exists()) {
                String newFolderPath = newPath.substring(0, newPath.lastIndexOf(File.separator));
                // 目标路径不存在时自动创建文件夹
                new File(newFolderPath).mkdirs();
                // 文件存在时读入原文件
                InputStream inStream = new FileInputStream(oldPath);
                FileOutputStream fs = new FileOutputStream(newPath);
                byte[] buffer = new byte[1024];
                int byteSum = 0;
                int byteRead;
                while ((byteRead = inStream.read(buffer)) != -1) {
                    // 字节数(文件大小)
                    byteSum += byteRead;
                    fs.write(buffer, 0, byteRead);
                }
                inStream.close();
            }
        } catch (Exception e) {
            System.out.println("复制单个文件出错");
            e.printStackTrace();
        }
    }

    /**
     * 复制整个文件夹内容
     *
     * @param oldPath 原文件路径 如:c:/test
     * @param newPath 复制后路径 如:f:/test
     * @return void
     * @author XanderYe
     * @date 2020/3/9
     */
    private static void copyFolder(String oldPath, String newPath) {
        try {
            //如果文件夹不存在 则建立新文件夹
            (new File(newPath)).mkdirs();
            File a = new File(oldPath);
            String[] file = a.list();
            File temp;
            for (String s : file) {
                if (oldPath.endsWith(File.separator)) {
                    temp = new File(oldPath + s);
                } else {
                    temp = new File(oldPath + File.separator + s);
                }
                if (temp.isFile()) {
                    FileInputStream input = new FileInputStream(temp);
                    FileOutputStream output = new FileOutputStream(newPath + "/" + temp.getName());
                    byte[] b = new byte[1024 * 5];
                    int len;
                    while ((len = input.read(b)) != -1) {
                        output.write(b, 0, len);
                    }
                    output.flush();
                    output.close();
                    input.close();
                }
                if (temp.isDirectory()) {
                    //如果是子文件夹
                    copyFolder(oldPath + "/" + s, newPath + "/" + s);
                }
            }
        } catch (Exception e) {
            System.out.println("复制文件夹出错");
            e.printStackTrace();
        }
    }
}

本来是写个程序一键提取复制打包删除临时文件的,但是提取和打包涉及到控制台操作,提取的时候用java调用cmd获取到的类加载文件不太对,打包用的jar命令是多线程操作,没来得及打包就删除了文件,这里就分开了。

3.删除临时文件

复制类文件后那几个文件夹,强迫症想把它删了,代码如下:

import java.io.File;
import java.util.Scanner;

/**
 * Created on 2020/3/9.
 *
 * @author XanderYe
 */
public class DeletePackages {
    public static void main(String[] args) {
        String[] packages = {"com", "java", "javax", "jdk", "META-INF", "org", "sun"};
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入临时文件所在文件夹路径:");
        String path = scanner.nextLine();
        if (path == null || "".equals(path)) {
            path = System.getProperty("user.dir");
        }
        path += File.separator;
        // 删除包
        deletePackages(path, packages);
        System.out.println("删除完毕");
    }

    /**
     * 删除包文件夹
     *
     * @param path  文件或文件夹所在目录,如d:/test
     * @param names 要删除的文件或文件夹数组
     * @return void
     * @author XanderYe
     * @date 2020/3/9
     */
    public static void deletePackages(String path, String[] names) {
        for (String name : names) {
            String deleteFilePath = path + name + File.separator;
            File file = new File(deleteFilePath);
            if (file.exists()) {
                try {
                    deleteFile(deleteFilePath);
                    System.out.println("删除:" + deleteFilePath);
                } catch (Exception e) {
                    System.out.println("删除:" + deleteFilePath + "失败");
                }
            }
        }
    }

    /**
     * 删除文件或文件夹
     *
     * @param path
     * @return void
     * @author XanderYe
     * @date 2020/3/9
     */
    private static void deleteFile(String path) {
        File file = new File(path);
        if (file.isFile()) {
            file.delete();
        } else {
            File[] files = file.listFiles();
            if (files == null) {
                file.delete();
            } else {
                for (int i = 0; i < files.length; i++) {
                    deleteFile(files[i].getAbsolutePath());
                }
                file.delete();
            }
        }
    }
}

看下精简效果:精简前52M,精简后1.5M。

https://wp.xanderye.cn/wp-content/uploads/2020/03/精简前.png
https://wp.xanderye.cn/wp-content/uploads/2020/03/精简后.png

至此,rt.jar已精简完毕,替换jre/lib中的rt.jar,测试一下,不保证100%可以,可能会有缺少类的问题,看报错信息手动丢进去即可。

源码地址已放github,稍有调整: https://github.com/XanderYe/jre-lite

参考:

Java 精简Jre jar打包成exe

关于精简JRE文件之精简rt.jar包

赞赏

发表评论

textsms
account_circle
email

XanderYe的个人小站

精简jre之rt.jar
作为一个java开发,让别人的电脑跑java程序是一件很头疼的事,因为官方没有java转exe的工具。虽然有其他第三方软件,但试了试,终归还是不太好用。 最早是用maven插件javafx-maven-pl…
扫描二维码继续阅读
2020-03-09