写在前面
本文是针对教你将java程序做成黑盒并跨平台分发文章的一篇补充说明。
补充内容:
- 解决在无
java运行环境
的机器上运行加密的分发包时无法找到java运行环境
的问题
一、问题原因
- 在使用
jpackage
进行打包时,会生成对应平台的启动器,这个启动器会调用打进来的runtime
文件夹,实际上这个文件夹就是一个精简版的JRE
(这个说法不准确,在大于JDK8
的版本中实际上它是一个丢弃了申明文件和可执行文件的JDK
) - 当运行启动器文件时,启动器会用当前的
runtime环境
启动前面我们写好的ExecApp.jar
。如果JAR
未加密到这里就结束了,一切正常。 - 对于加密的
JAR
,启动需要用加密启动器调用java命令
来再次启动一个新的java
程序,这是一个新的JVM进程
,并且在启动它的时候会限制JVM启动参数
设置它的一些功能,保证不能使用内存或者字节码替换等方式来破坏它。 - 需要将加密启动器的
java环境
指向到runtime环境
,前面就是这里没有进行指定导致异常了。
二、解决方案
- 新增一个脚本(我这里写的是bat),在加密启动器启动前指定
runtime环境
- 再次启动依旧错误,原因是前面提到的
runtime环境
丢弃了可执行文件,不能执行java命令
,于是需要将jdk_home/bin/java
放到runtime/bin
中。 - 再次启动即可正常。
三、附上调整的实现文件
- 调整
ExecApp.java
文件,由原来的命令改为调用对应平台的启动脚本
package com.exec;
import java.io.*;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* @author liucheng
* @version 1.0
* @description: 启动
* @date 2022/1/21 16:13
*/
public class ExecApp {
final static String WIN = "win", MAC = "mac", ENCRYPTION_JAR_NAME = "robot.jar";
static String CMD, JAVA_BIN, RUN_FILE, FILE_PATH, JDK_BIN_FILE, JDK_BIN_NAME = "java", GO_EXE_NAME = "exec";
public static void main(String[] args) {
File root = getFileRoot();
System.out.println("********** root_path:" + root.getPath());
if (OSinfo.isWindows()) {
mkdir(root, WIN);
JAVA_BIN = "runtime/bin";
FILE_PATH = "../../robot/";
GO_EXE_NAME = WIN + "/exec.exe";
RUN_FILE = WIN + "/run.bat";
JDK_BIN_NAME = JDK_BIN_NAME + ".exe";
JDK_BIN_FILE = WIN + "/" + JDK_BIN_NAME;
} else if (OSinfo.isMacOS() || OSinfo.isMacOSX()) {
mkdir(root, MAC);
JAVA_BIN = "runtime/Contents/Home/bin";
FILE_PATH = "../../robot/";
GO_EXE_NAME = WIN + "/exec";
RUN_FILE = MAC + "/run.sh";
JDK_BIN_FILE = MAC + "/" + JDK_BIN_NAME;
JDK_BIN_NAME = JDK_BIN_FILE;
}
install(root);
CMD = RUN_FILE;
System.out.println("********** JAVA_BIN:" + JAVA_BIN);
System.out.println("********** FILE_PATH:" + FILE_PATH);
System.out.println("********** CMD:" + CMD);
Thread thread = new Thread(() ->
exeCmd(CMD, null, FILE_PATH, JAVA_BIN, GO_EXE_NAME, ENCRYPTION_JAR_NAME, JDK_BIN_NAME));
thread.setDaemon(true);
thread.start();
System.out.println("********** 启动完成 ********** ");
try {
thread.join();
} catch (InterruptedException e) {
System.err.println("启动线程异常:" + e);
} finally {
System.exit(0);
}
}
static void install(File root) {
System.out.println("********** install");
System.out.println("********** RUN_FILE:" + RUN_FILE);
System.out.println("********** GO_EXE_NAME:" + GO_EXE_NAME);
System.out.println("********** ENCRYPTION_JAR_NAME:" + ENCRYPTION_JAR_NAME);
System.out.println("********** JDK_BIN_FILE:" + JDK_BIN_FILE);
InputStream runFileInputStream = Objects.requireNonNull(ExecApp.class.getResourceAsStream("/" + RUN_FILE));
RUN_FILE = Path.of(root.getPath(), RUN_FILE).toString();
InputStream jdkFileInputStream = Objects.requireNonNull(ExecApp.class.getResourceAsStream("/" + JDK_BIN_FILE));
write(Path.of(root.getPath(), GO_EXE_NAME).toString(), Objects.requireNonNull(ExecApp.class.getResourceAsStream("/" + GO_EXE_NAME)));
write(Path.of(root.getPath(), ENCRYPTION_JAR_NAME).toString(), Objects.requireNonNull(ExecApp.class.getResourceAsStream("/" + ENCRYPTION_JAR_NAME)));
write(RUN_FILE, runFileInputStream);
write(Path.of(root.getPath(), JDK_BIN_FILE).toString(), jdkFileInputStream);
}
static void mkdir(File root, String path) {
File file = Path.of(root.getPath(), path).toFile();
if (!file.exists()) file.mkdirs();
}
static File getFileRoot() {
String prefix = System.getenv("HOME");
if (null == prefix) prefix = System.getProperty("user.dir");
Path path = Path.of(prefix, "robot");
File file = path.toFile();
if (!file.exists()) file.mkdirs();
return file;
}
static void write(String outPath, InputStream inputStream) {
int index;
byte[] bytes = new byte[1024];
try (FileOutputStream downloadFile = new FileOutputStream(outPath)) {
while ((index = inputStream.read(bytes)) != -1) {
downloadFile.write(bytes, 0, index);
downloadFile.flush();
}
} catch (IOException e) {
System.err.println("写文件异常");
e.printStackTrace();
} finally {
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 执行shell命令并且打印执行结果
*
* @return 按行返回执行结果
* @throws IOException
*/
static List<String> exeCmd(String cmd, List<String> envp, String... args) {
List<String> list = new ArrayList<>();
if (null != args) cmd = cmd + " " + String.join(" ", args);
System.out.println("cmd:" + cmd);
Process process;
try {
if (null == envp) process = Runtime.getRuntime().exec(cmd);
else process = Runtime.getRuntime().exec(cmd, envp.toArray(new String[0]));
String finalCmd = cmd;
Thread t = new Thread(() -> {
try (BufferedReader inputError = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String line = "";
while ((line = inputError.readLine()) != null) {
System.err.println(line);
}
} catch (IOException e) {
System.err.println("error [" + finalCmd + " ]shell error:" + e);
}
});
t.start();
try (BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line = "";
while ((line = input.readLine()) != null) {
list.add(line);
System.out.println(line);
}
} catch (Exception e) {
System.err.println("run [" + cmd + " ]shell error:" + e);
}
process.waitFor();
process.destroy();
} catch (Exception e) {
System.err.println("exec error:" + e);
return list;
}
return list;
}
public static class OSinfo {
private static String OS = System.getProperty("os.name").toLowerCase();
public static boolean isLinux() {
return OS.indexOf("linux") >= 0;
}
public static boolean isMacOS() {
return OS.indexOf("mac") >= 0 && OS.indexOf("os") > 0 && OS.indexOf("x") < 0;
}
public static boolean isMacOSX() {
return OS.indexOf("mac") >= 0 && OS.indexOf("os") > 0 && OS.indexOf("x") > 0;
}
public static boolean isWindows() {
return OS.indexOf("windows") >= 0;
}
}
}
- 针对打包后的文件结构编写加密启动脚本(这里以
windows
下的bat
脚本为例)
set time=%date:~0,4%-%date:~5,2%-%date:~8,2%T%time:~0,2%:%time:~3,2%:%time:~6,2%
set "time=%time: =0%"
echo %time%******************** >> run.log
set FILE_PATH=%1%
set JAVA_BIN=%2%
set GO_EXE_NAME=%3%
set ENCRYPTION_JAR_NAME=%4%
set JDK_BIN_NAME=%5%
set JAVA_OPTIONS=--add-opens java.base/jdk.internal.loader=ALL-UNNAMED
set GO_EXE=%FILE_PATH%%GO_EXE_NAME%
set ENCRYPTION_JAR=%FILE_PATH%%ENCRYPTION_JAR_NAME%
echo JAVA_BIN:%JAVA_BIN% >> run.log
echo FILE_PATH:%FILE_PATH% >> run.log
echo JDK_BIN_NAME:%JDK_BIN_NAME% >> run.log
echo GO_EXE:%GO_EXE% >> run.log
echo ENCRYPTION_JAR:%ENCRYPTION_JAR% >> run.log
cd robot/win
move %JDK_BIN_NAME% "../../%JAVA_BIN%"
cd "../../%JAVA_BIN%"
"%GO_EXE%" java %JAVA_OPTIONS% -jar "%ENCRYPTION_JAR%"
- 我这里的编译结构是这样的,上面的路径需要根据编译的路径来
到这里就完事了,我将其分成了windows、mac os x、liunx
,便于更加方便的分发包。
四、问题
-
为什么把
java
或java.exe
放到项目中打包,不等jpackage
后移动当前打包的机器上的这个文件到对应目录?- 在
windows
免装版打包是可以的,但是安装版或mac、liunx
版本下是不行的,例如mac免装版
会生成一个包dmg
,你无法再次方便的操作它了,交由程序启动的时候动态生成反而更好
- 在
-
为什么不精简java运行环境大小
- 减少
lib/rt.jar
文件的大小 - 使用
jlink
减少jdkmodule
大小 - 这两种方式我都试过了,付出与收益不成正比,主要原因是现在一个
java
项目基本上都能覆盖到JDK
大部分依赖和module
。
- 减少
评论区