侧边栏壁纸
博主头像
术业有道之编程博主等级

亦是三月纷飞雨,亦是人间惊鸿客。亦是秋霜去叶多,亦是风华正当时。

  • 累计撰写 99 篇文章
  • 累计创建 50 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

跨平台分发加密的java程序后无法找到运行环境的问题

Administrator
2022-02-17 / 0 评论 / 0 点赞 / 307 阅读 / 10742 字

写在前面

本文是针对教你将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,便于更加方便的分发包。

四、问题

  • 为什么把javajava.exe放到项目中打包,不等jpackage后移动当前打包的机器上的这个文件到对应目录?

    • windows免装版打包是可以的,但是安装版或mac、liunx版本下是不行的,例如mac免装版会生成一个包dmg,你无法再次方便的操作它了,交由程序启动的时候动态生成反而更好
  • 为什么不精简java运行环境大小

    • 减少lib/rt.jar文件的大小
    • 使用jlink减少jdkmodule大小
    • 这两种方式我都试过了,付出与收益不成正比,主要原因是现在一个java项目基本上都能覆盖到JDK大部分依赖和module

    个人公众号

0

评论区