作为一个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
参考:
发表回复