作为一个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中的日志;不用>>导出文件,控制台虽然是有输出的,但是在控制台采集数据会丢失,不太好用。
得到几百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。
至此,rt.jar已精简完毕,替换jre/lib中的rt.jar,测试一下,不保证100%可以,可能会有缺少类的问题,看报错信息手动丢进去即可。
源码地址已放github,稍有调整: https://github.com/XanderYe/jre-lite
参考:
发表回复