很久之前我就有一个疑问,为什么甲骨文官方不出exe打包工具,我导出来的jar包才1.9mb,我却要把226兆的jre一起打包。这样子太麻烦了,每个程序都要打包一个不同版本的jre,如果不是这个,或许java语言就是最好的编程语言了。虽然这样子java可以跨平台,但是却不像c++和c语言一样可以直接生成exe。还得带去一个好几百兆的jre才能操作。很多时候我们却不需要这种跨平台,所以我们只能精简jre,减少jre的体积,不然对于没有安装jre的用户来说就是灾难,在精简之前,我想说,其实精简没必要,因为对于一些电脑,需要的组件不一样,比如win7需要msvcr100.dll,但是win10却需要msvcp140.dll,你在精简的时候很有可能会漏掉一个动态链接库导致程序在别人的电脑运行不了,不过还是废话不多说,直接上步骤。在此之前先感谢这位博主https://www.cnblogs.com/cjdty/
是他的经验给我的启发,以下的内容基于他的博客和我个人的经验修改写的。
假设现在我们有
1. 需要打包的jar包 (我这里是client.jar)
2. 一台装有Java环境的win10 64位电脑 JDK是1.8版本
3. 能运行java代码的环境 (我这里是IntelliJ IDEA)
4. 火绒剑(我是内置版的)
5. notepad++
这是jdk1.8里面的jre目录 最主要的就是bin和lib目录(其实我们只需要这两个文件夹就行了)
bin目录就是java.exe等几个命令所在地了,应该也是JVM虚拟机的本体所在, 我的里面是这样的
这里有 java.exe 也即是启动jar的关键所在 以及一大堆的dll文件 精简bin目录的关键就是精简这些dll文件,思路就是满足java.exe的运行就可以了
那么 接下来就是想办法知道java.exe到底依赖哪些dll了
大家打开可以看到lib文件是一堆不知用途的文件以及各种jar包 那么这里的jar包里面就是我们在写代码的时候调用的类以及方法.
还有字体文件(font文件夹里) 图标资源(images文件夹里面) 还有一个最大的rt.jar文件 大小居然都是53多MB了 所以 精简lib目录的核心就是精简rt.jar文件 精简的思路依旧是删除不需要的就好了。
1. 把准备打包的jar包复制到bin
2. 新建一个批处理文件(新建文本文档) 添加内容如下 (意思是 运行jar文件 并且把所有输出信息导出到class.txt文件里)
java -jar -verbose:class client.jar >> class.txt
保存 更改名字为 run.bat (注意 这是改了后缀名的 如果没有后缀名去系统那里勾选文件扩展名)
3. 双击运行 run.bat 然后对你的应用进行各种操作 是所有的功能都使用一遍(这里指的是,即使是一个编辑框 你也要尝试输入不同的东西 使用方向键操作光标等等 )这个非常重要!!!!!!!!!!! 为的是把所有使用到的类都输出到class.txt文件 (class.txt文件是用来精简lib目录的 这里不管他)
4. 打开火绒剑 (其他的行为检测软件也可以 作用相同就行) 点击进程 -> 然后选择java.exe 然后就可以找到java.exe加载的所有dll文件
5. 新建一个在桌面上新建一个文件夹 然后 新建两个文件夹(bin和lib) 如图
6. 在上面火绒剑里面查看所用到的dll以及他们的路径 找到他们 复制到新建的文件夹里的bin文件夹 得到如下 发现瞬间少了很多 (如果不是jre目录下的dll文件不复制)
7. 在上图空白处按着shift然后右键菜单 在此处打开power shell 运行得到如下结果 原因是缺少jvm.cfg 这是JVM虚拟机的配置文件 然后直接复制完整的jre的lib目录下的所有文件到自己精简的lib目录下
然后再次运行一次./java (值得注意的是 power shell的话 必须加上./ 才会从当前目录下找文件)
做完这些之后 应该是可以在这里运行你准备打包的jar文件的
在bin目录下新建一个批处理文件 内容如下
java -jar client.jar
pause
复制你的jar文件 在这里我的是client.jar
双击run.bat就可以 运行你的jar文件了 如果不行 就是jar文件的原因
1. 思路就是 上面不是可是已经能够运行jar文件了吗 那么 我们删除一个文件 就运行一次(这就是上面用批处理运行jar的原因 )
2. 主要是删除jar文件 从大到小 还可以尝试删除一些其他的文件 比如图片文件 字体文件 不过可能会引起一些bug
3,这里提供几个jar包的说明(版本是1.8的)
rt.jar : 运行时包
dt.jar: dt.jar是关于运行环境的类库
tools.jar: tools.jar是工具类库,编译和运行需要的都是toos.jar里面的类分别是sun.tools.java. ; sun.tols.javac.;
ant-javafx.jar: javaFX包的ant工具
charsets.jar: Java 字符集,这个类库中包含 Java 所有支持字符集的字符
cldrdata.jar: http://cldr.unicode.org/ CLDR - Unicode Common Locale Data Repository
deploy.jar: deploy.jar是Java部署堆栈的一部分,用于applet和Webstart应用程序。 deploy.jar是Java安装目录的常见部分 - 该文件运行某些产品的安装。 正确设置Java路径后,用户可以执行此文件(只需双击它或按文件上的Enter键),要部署的应用程序将运行其安装程序。 例如。 诺基亚OVI套件通常使用这种部署形式。 作为彼此的JAVA包,如果您将其重命名为ZIP并打开内容,则可以检查包中的类。
dnsns.jar:与 DNS 有关
jaccess.jar: Java Access Bridge is a technology that exposes the Java Accessibility API in a Microsoft Windows DLL, enabling Java applications and applets that implement the Java Accessibility API to be visible to assistive technologies on Microsoft Windows systems. Java Accessibility API is part of Java Accessibility Utilities, which is a set of utility classes that help assistive technologies provide access to GUI toolkits that implement the Java Accessibility API. Java Access Bridge是一种在Microsoft Windows DLL中公开Java Accessibility API的技术,使实现Java Accessibility API的Java应用程序和applet对Microsoft Windows系统上的辅助技术可见。 Java Accessibility API是Java Accessibility Utilities的一部分,它是一组实用程序类,可帮助辅助技术提供对实现Java Accessibility API的GUI工具包的访问。
javafx-mx.jar: JavaFx 相关 Contains the following JavaFX utility jar files: ant-javafx.jar: Ant tasks for packaging and deployment.javafx-doclet.jar: A doclet for producing customized and nicely formatted documentation for the users of your JavaFX library.javafx-mx.jar: A file used for debugging.
javaws.jar: Java Web Start contains the JNLP (Java Network Launching Protocol) API and its reference implementation. JNLP(Java Network Launching Protocol )是java提供的一种可以通过浏览器直接执行java应用程序的途径,它使你可以直接通过一个网页上的url连接打开一个java应用程序。
jce.jar: Java 加密扩展类库,含有很多非对称加密算法在里面,但也是可扩展的。
jconsole.jar: jconsole Jconsole控制台 Java监视和管理控制台
jfr.jar: Flight Recorder Files 飞行记录器JFR(java flight recorder)
jfxrt.jar:javaFx相关的java包
jfxswt.jar: javaFx相关的与 swt有关java包
jsse.jar: The Java Secure Socket Extension
localedata.jar: contains many of the resources needed for non US English locales 本地机器语言的数据,比如日期在使用中文时,显示的是“星期四”之类的
management-agent.jar: JVM本身提供了一组管理的API,通过该API,我们可以获取得到JVM内部主要运行信息,包括内存各代的数据、JVM当前所有线程及其栈相关信息等等。各种JDK自带的剖析工具,包括jps、jstack、jinfo、jstat、jmap、jconsole等,都是基于此API开发的。
nashorn.jar: A Next-Generation JavaScript Engine for the JVM JVM的JavaScript解析引擎
packager.jar: The Java Packager tool can be used to compile, package, sign, and deploy Java and JavaFX applications from the command line. It can be used as an alternative to an Ant task or building the applications in an IDE. The Java Packager tool is not available for the Solaris platform.
plugin.jar: 按字面意思,应该是插件API的意思, 与UI和浏览器有关
resources.jar: 资源包(图片、properties文件)
sa-jdi.jar: ServiceAbility JDK SA工具 https://www.jianshu.com/p/7c40274441a4
sunec.jar: JCE providers for Java Cryptography APIs
sunjce_provider.jar: 为JCE 提供的加密安全套件
sunpkcs11.jar: PKCS#11 证书工具
zipfs.jar: Zip File System Provider
4. 这样的话 这是比较笨的方法 但是效果也还不错 然后我们发现 最大的就是rt.jar文件了 那么接下来就就是精简rt.jar
1. 记得上面的到的class.txt文件吗,把他复制到与你新建的jre目录下 也即是与bin和lib目录同目录 (这里是为了方便 不复制也行)
2. 现在 处理class.txt文件 用notepad++打开它 你会发现会有好多好多的东西,接下来用notepad++的话 就会很简单
3. 删除掉所有的 不带方括号的行
4. Ctrl + F 然后搜索 "[Opened " 然后把所有带"[Opened "个的行删除掉 (是删除一整行)
5. Ctrl + F 然后搜索 "[Loaded " 点击计数,然后点击替换选项卡 全部替换 (替换的时候注意点一下计数,对比一下行数 应该是一样的,,,,)
6. 点击"正则表达式"的单选框 在查找目标里面填上正则表达式 点击计数,然后 替换->全部替换
7.这样 我们就得到了如下的东西 保存
8. jar文件本质上是zip文件 那么 我们把rt.jar文件复制到class.txt同一个目录 这样的话 精简的jre目录下就有 bin、lib、class.txt、rt.jar文件或文件夹了 然后右键 "解压到rt/" 这样我么就得到了一个rt文件夹 里面是rt.jar的完整内容
9. 然后新建一个文件夹名字是ort文件夹 用来放需要的class文件
10. 然后执行如下代码
package ch1;
/**
*
*/
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import com.wordpress.iwillaccess.classes.global.InputOutput;
//import InputOutput;
/**
* @author knowyourself1010
*
*/
public class CopyUsefulClasses {
// 文件拷贝
private static boolean copy(String sourceFileLocation,
String objectFileLocation, String fileName) {
try // must try and catch,otherwise will compile error
{
if (sourceFileLocation.substring(sourceFileLocation.length() - 1) != "/") {
sourceFileLocation += "/";
}
if ((objectFileLocation.substring(objectFileLocation.length() - 1)) != "/") {
objectFileLocation += "/";
}
InputOutput inputOutput = new InputOutput();
byte[] b = inputOutput
.DataOutputFully(sourceFileLocation, fileName);
inputOutput.DataInputFully(objectFileLocation, fileName, b);
return true; // if success then return true
} catch (Exception e) {
System.out.println("Error!");
return false; // if fail then return false
}
}
// 读取路径,copy
private static int dealClass(String needfile, String sdir, String odir)
throws IOException {
int sn = 0; // 成功个数
if (odir.length() > 0 && sdir.length() > 0) {
if ((sdir.substring(sdir.length() - 1)) != "/") {
sdir += "/";
}
if (odir.substring(odir.length() - 1) != "/") {
odir += "/";
}
File usedclass = new File(needfile);
if (usedclass.canRead()) {
String line = null;
LineNumberReader reader = new LineNumberReader(
new InputStreamReader(new FileInputStream(usedclass),
"UTF-8"));
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.contains(".") || line.contains("/")) {
// format the direction from package name to path
String dir = line.replace(".", "/");
// filter the file name.
String tmpdir = dir.substring(0, dir.lastIndexOf("/"));
String sourceFileLocation = sdir + tmpdir;
String objectFileLocation = odir + tmpdir;
String fileName = dir.substring(
dir.lastIndexOf("/") + 1, dir.length())
+ ".class";
File fdir = new File(objectFileLocation);
if (!fdir.exists())
fdir.mkdirs();
boolean copy_ok = copy(sourceFileLocation,
objectFileLocation, fileName);
if (copy_ok)
sn++;
else {
System.out.println(line);
}
} else {
sn = -1;
}
}
}
}
return sn;
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
BufferedReader lineOfText = null;
// get need classes log file direction
System.out
.println("要读取的class.txt文件的绝对路径 :");
lineOfText = new BufferedReader(new InputStreamReader(System.in));
String needfile = lineOfText.readLine();
// get source folder direction
System.out
.println(needfile
+ "\n输入jre/lib/rt.jar解压後的rt文件夹所在的路径 :");
lineOfText = new BufferedReader(new InputStreamReader(System.in));
String sdir = lineOfText.readLine();
// get object folder direction
System.out
.println(sdir
+ "\n再输入ort(所要存放拷贝过来的有用的.class文件的文件夹):");
lineOfText = new BufferedReader(new InputStreamReader(System.in));
String odir = lineOfText.readLine();
System.out.println(odir + "\n");
int sn = dealClass(needfile, sdir, odir);
System.out.print(sn);
} catch (IOException e) {
// TODO 自动生成 catch 块
e.printStackTrace();
}
}
}
package com.wordpress.iwillaccess.classes.global;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class InputOutput {
private static String _fileLocation;
private static String _fileName;
private static String _fileDirectory;
private static String _fileContent;
private static byte[] _byte;
/**
* @return the _fileDirection
*/
public String get_fileLocation() {
return _fileLocation;
}
/**
* @return the _fileName
*/
public String get_fileName() {
return _fileName;
}
/**
* @return the _fileDirectory
*/
public String get_fileDirectory() {
return _fileDirectory;
}
/**
* @return the _fileContents
*/
public String get_fileContent() {
return _fileContent;
}
/**
* @return the _byte
*/
public byte[] get_byte() {
return _byte;
}
/**
* @param fileDirection
* the _fileDirection to set
*/
private static synchronized void set_fileLocation(String fileLocation) {
_fileLocation = fileLocation;
}
/**
*
* @param fileName
* the _fileName to set
*/
private static synchronized void set_fileName(String fileName) {
_fileName = fileName;
}
/**
*
* @param fileLocation
* the _fileLocation to set
* @param fileName
* the _fileName to set
*
* fileLocation and fileName is the _fileDirectory to set.
*/
public synchronized void set_fileDirectory(String fileLocation,
String fileName) {
set_fileLocation(fileLocation);
set_fileName(fileName);
_fileDirectory = fileLocation + fileName;
}
/**
*
* @param fileDirectory
* the _fileDirectory to set
*
* _fileDirectory = _fileLocation + _fileName;
* _fileLocation and _fileName will be set automatically in this
* method.
*/
public synchronized void set_fileDirectory(String fileDirectory) {
set_fileLocation(fileDirectory.substring(0, fileDirectory
.lastIndexOf("/") + 1));
set_fileName(fileDirectory
.substring(fileDirectory.lastIndexOf("/") + 1));
_fileDirectory = fileDirectory;
}
/**
* @param fileContents
* the _fileContents to set
*/
public synchronized void set_fileContent(String fileContent) {
_fileContent = fileContent;
}
/**
* @param _byte
* the _byte to set
*/
private synchronized void set_byte(byte[] _byte) {
InputOutput._byte = _byte;
}
public InputOutput() {
// TODO Auto-generated constructor stub
}
/**
* check if the file is exist or not
*
* @param fileDirectory
* the _fileDirectory to set, and the directory of the file will
* be checked.
* @return true: file is exist;
* false: file is not exist.
*/
public boolean checkIfFileExist(String fileDirectory) {
set_fileDirectory(fileDirectory);
File file = new File(get_fileDirectory());
// System.out.println("file.exists():" + file.exists());
return file.exists();
}
//…………
//……省去的无关方法的代码………
//…………
/**
* input get_byte() to get_fileDirectory. If object file is exist, it will
* be replaced without warning.
*
* NOTE:
* use the set_fileDirectory(), set_byte() and
* checkIfFileExist(String fileDirectory) methods first
* to set a righdis.readFully(b)t file directory and file content
* before using this method;
*
*/
public synchronized void DataInputFully() {
if (get_fileLocation() != null && get_fileName() != null
&& get_fileContent() != null && get_byte() != null) {
try {
FileOutputStream fos = new FileOutputStream(get_fileDirectory());
DataOutputStream dos = new DataOutputStream(fos);
dos.write(get_byte());
dos.close();
} catch (Exception e) {
e.printStackTrace();
}
} else {
System.out.println("fileLocation:" + get_fileLocation()
+ "; or fileName:" + get_fileName() + "; or byte[]:"
+ get_byte() + " is null.");
}
}
/**
* input get_byte() to get_fileDirectory. If object file is exist, it will
* be replaced without warning.
*
* NOTE:
* please use set_fileDirectory() to set the file directory,
* and use checkIfFileExist(String fileDirectory) first
* before using this method.
*
*
* @param b
* the _byte to set
*/
public void DataInputFully(byte[] b) {
set_byte(b);
DataInputFully();
}
/**
* input get_byte() to get_fileDirectory. If object file is exist, it will
* be replaced without warning.
*
* NOTE:
* create a file, whose directory as fileDirectory
* and file content as get_byte()
.
* Use checkIfFileExist(String fileDirectory) first
*
*
* @param fileDirectory
* the _fileDirectory to set
* @param b
* the _byte to set, contains the content of file.
*/
public void DataInputFully(String fileDirectory, byte[] b) {
set_fileDirectory(fileDirectory);
set_byte(b);
DataInputFully();
}
/**
* input get_byte() to get_fileDirectory. If object file is exist, it will
* be replaced without warning.
*
* NOTE:
* create file named fileName
in
* fileLocation
input get_byte()
to file named as
* fileName
in fileLocation
.
* Use checkIfFileExist(String fileDirectory) first
*
*
* @param fileLocation
* the _fileLocation to set and will be used to set the
* _fileDirectory
* @param fileName
* the _fileName to set and will be used to set the
* _fileDirectory
* @param b
* the _byte to set and will be write to the file with
* _fileDirectory directory. _byte Contains the content of file.
*/
public void DataInputFully(String fileLocation, String fileName, byte[] b) {
set_fileDirectory(fileLocation, fileName);
set_byte(b);
DataInputFully();
}
/**
* Out put a file by using DataOutputStream class.
*
* NOTE:
* use the set_fileDirectory() method first
* to set a right file directory
* before using this method;
*
*
* @return null:if fileDirectory is null;
* file content byte[];
*/
public synchronized byte[] DataOutputFully() {
if (get_fileDirectory() != null) {
File file = new File(get_fileDirectory());
if (file.exists()) {
try {
FileInputStream fis;
fis = new FileInputStream(get_fileDirectory());
DataInputStream dis = new DataInputStream(fis);
byte[] b = new byte[dis.available()];
while (dis.available() > 0) {
dis.readFully(b);
// System.out.println(" :" + b.length);
}
set_byte(b);
String tmpStr = "";
for (int i = 0; i < b.length; i++) {
tmpStr += String.valueOf(b[i]);
}
set_fileContent(tmpStr);
// System.out.println("\n" + get_fileContent() + "\n"
// + get_fileLocation() + "\n" + get_fileName());
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return get_byte();
} else {
System.out.println("fileDirectory:" + get_fileDirectory()
+ "; and the file is not exist.");
return null;
}
} else {
System.out.println("fileDirectory:" + get_fileDirectory()
+ "; is null");
return null;
}
}
/**
* Out put a file, which directory as fileDirectory
,
* by using DataOutputStream class.
*
* @param fileDirectory
* the _fileDirectory to set and will be used to find the file,
* which is supposed to be opened.
* @return byte[] _byte() the content of the chose file.
*/
public byte[] DataOutputFully(String fileDirectory) {
set_fileDirectory(fileDirectory);
return DataOutputFully();
}
/**
* Out put a file, which directory as fileLocation
plus
* fileName
,
* by using DataOutputStream class.
*
* @param fileLocation
* the _fileLocation to set and will be used to set the
* _fileDirectory
* @param fileName
* the _fileName to set and will be used to set the
* _fileDirectory
* @return byte[[] get_byte() the content of the chose file.Data
*/
public byte[] DataOutputFully(String fileLocation, String fileName) {
set_fileDirectory(fileLocation, fileName);
return DataOutputFully();
}
}
运行的时候填写这样的内容
首先输入usedClasses.txt的绝对路径,回车,在输入jre/lib/rt.jar解压後的rt文件夹所在的路径,回车,再输入ort(所要存放拷贝过来的有用的.class文件的文件夹)。然后等上几分中,期间会提示,你的产品程序所用到的jre不包含的类不存在,不用管,因为我们呢只拷贝rt文件中的.class文件 (这一段话复制自上面代码所在博客)
Ps:经检验,导出来的ort文件夹上有的文件建议从rt.jar上找到相应的文件夹都替换,不然运行不了。
11. 这样的话 在ort文件夹里面就有了很多文件夹 像是java、javax什么的
12. 用java本身自带的工具打包文件夹成jar(不建议用winrar压缩改后缀名,这样子很容易出问题) cmd cd 进入到ort所在的文件夹,比如:
新建一个文件,名为:mymanifest,在该目录下拿到rt.jar。
Cmd输入操作指令:jar cvfm rt.jar mymanifest -C ort/ .
mymanifest里面没有什么内容,就是空的记事本去掉后缀名,改为文件格式而已。然后在cmd转到当前目录(ps:当前目录里有文件夹ort,文件夹rt,文件mymanifest(再说明一次,这个文件没有后缀名,也没有内容)),cmd执行jar cvfm rt.jar mymanifest -C ort/ . (ps:注意后面有一个点。斜杠和点之间有一个空格。)等待片刻就会在当前目录下生成一个rt.jar包。说明一下:jar cvfm rt.jar mymanifest -C ort/ . 的意思,将ort文件夹打包成一个jar包,包名叫rt。所以ort和rt.jar这两个是可以改的。
13. 删除lib目录下的rt.jar文件 用上面的rt.jar文件(也就是我们自己打包的)替换
14. 在bin目录下运行run.bat文件 然后如果成功了就测试一下各种功能 确认没问题的话 jre环境就已经精简成功了 可以直接看下面的exe打包阶段了
15 如果有错误 应该是这样的 那么应该是这样的 这里的话 明显是class文件缺失
16.解压rt.jar重新加入class文件,重新用cmd的方式打包
17. 再运行bin目录下的run.bat批处理文件 可能还会有错 有错的话 就继续上一个步骤 直到没有错误
18. 这样的话 jre环境就已经精简成功了 : )