能否制作一个可以执行的Java程序?

5
请清楚地理解,这里的“可执行文件”并不是指面向处理器的字节。例如,一个bash脚本,在加上一个指定应该使用/bin/bash/bin/sh或其他解释程序运行它的shebang后,就变成了可执行文件。需要注意的是,bash脚本实际上是被解释执行而不是编译执行的。
我在想是否可以用Java实现同样的功能。虽然Java并非技术上的脚本语言,但也并非可执行文件。看起来Java很难实现这个功能,因为用户实际上没有机会在编译后的文件中添加shebang,并且编译后的Java文件不能来自stdin。
5个回答

3

您肯定可以创建一个文件:

#!/any/executable/program args
...input goes here...

您可以使用Java来完成这项任务。

#!/path/bin/java mainclass
...this is System.in...

啊,所以用Java编程的话,.class文件必须存储在其他地方,并且仍然由可执行文件引用? - William Rosenbloom
是的,这是正确的,你必须确保在运行之前classpath是正确的。因此,通常更实用的方法是使用sh脚本。 - John Hascall
很有趣。我不知道在shebang之后也可以传递参数。我猜这就是自学的结果哈哈。答案已批准! - William Rosenbloom
参数的数量和大小可能会受到限制。通常你会在那里看到shell标志,例如#!/bin/sh -x或其他类似的标志。 - John Hascall
嘿,很抱歉让你失去这个答案已经很久的胜利,但是最近我发现了一个更令人满意的答案,决定分享给大家。原来Java二进制文件不需要存储在其他地方并引用。欢迎阅读我的帖子,并让我知道你是否有任何想法。 - William Rosenbloom

3

为了让Java可执行源代码而不写很多代码,你有几个选择:

使用Scala!你知道吗,Scala是基于Java构建的吗?它有一个解释器和编译器。你可以运行脚本、shell或者编译并运行它。Scala和Java可以无缝地协同工作。两者都编译成相同的字节码,并在JVM上运行。是的,这种语言会感觉奇怪,因为Scala是Java、R和Python的混合体,但大部分核心语言没有改变,所有的Java包都可以使用。试试Scala吧。如果你在Linux上,甚至可以看看Spark,即使只有一台机器。

如果你坚持只使用Java,你可以创建一个混合程序来做两件事情(我以前做过):编译代码和运行它。可执行文件甚至是bash脚本可以完成把源文件变成可执行文件的工作。如果你想要制作一个Java shell,那么你需要制作一个动态运行时编译器/加载器,但你只需要使用Java/Oracle已经提供给我们的东西。想象一下,在我放置一个打印语句的文件中插入Java语法。只要编译通过,你可以在里面放任何东西。看看这个例子:

package util.injection;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

public class Compiler {

    static final long t0 = System.currentTimeMillis();

    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder(64);
        String packageName = "util";
        String className = "HelloWorld";
        sb.append("package util;\n");
        sb.append("public class HelloWorld extends " + Function.class.getName() + " {\n");
        sb.append("    public void test() {\n");
        sb.append("        System.out.println(\"Hello from dynamic function!\");\n");
        sb.append("    }\n");
        sb.append("}\n");
        String code = sb.toString();

        String jarLibraryFile = "target/myprojectname.jar";

        Function dynFunction = code2class(packageName, className, code, jarLibraryFile);
        dynFunction.test();
    }

    public static Function code2class(String packageName, String className, String code, String jarLibraryFile) {
        String wholeClassName = packageName.replace("/", ".") + "." + className;
        String fileName = wholeClassName.replace(".", "/") + ".java";//"testcompile/HelloWorld.java";
        File javaCodeFile = new File(fileName);
        string2file(javaCodeFile, code);
        Function dynFunction = null;
        try {

            boolean success = compile(jarLibraryFile, javaCodeFile);

            /**
             * Load and execute
             * ************************************************************************************************
             */
            System.out.println("Running... " + (System.currentTimeMillis() - t0) + " ms");
            Object obj = load(wholeClassName);
            // Santity check
            if (obj instanceof Function) {
                dynFunction = (Function) obj;
                // Run it 
                //Edit: call dynFunction.test(); to see something
            }
            System.out.println("Finished... " + (System.currentTimeMillis() - t0) + " ms");
        } catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException exp) {
            exp.printStackTrace();
        }
        return dynFunction;
    }

    public static boolean compile(String jarLibraryFile, File javaCodeFile) throws IOException {
        /**
         * Compilation Requirements
         * ********************************************************************************************
         */
        System.out.println("Compiling... " + (System.currentTimeMillis() - t0) + " ms");
        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);

        // This sets up the class path that the compiler will use.
        // I've added the .jar file that contains the DoStuff interface within in it...
        List<String> optionList = new ArrayList<>(2);
        optionList.add("-classpath");
        optionList.add(System.getProperty("java.class.path") + ";" + jarLibraryFile);

        Iterable<? extends JavaFileObject> compilationUnit
                = fileManager.getJavaFileObjectsFromFiles(Arrays.asList(javaCodeFile));
        JavaCompiler.CompilationTask task = compiler.getTask(
                null,
                fileManager,
                diagnostics,
                optionList,
                null,
                compilationUnit);
        fileManager.close();

        /**
         * *******************************************************************************************
         * Compilation Requirements *
         */
        if (task.call()) {
            return true;
            /**
             * ***********************************************************************************************
             * Load and execute *
             */
        } else {
            for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
                System.out.format("Error on line %d in %s%n",
                        diagnostic.getLineNumber(),
                        diagnostic.getSource().toUri());
                System.out.printf("Code = %s\nMessage = %s\n", diagnostic.getCode(), diagnostic.getMessage(Locale.US));

            }
        }
        return false;
    }

    public static void string2file(File outputFile, String code) {
        if (outputFile.getParentFile().exists() || outputFile.getParentFile().mkdirs()) {

            try {
                Writer writer = null;
                try {
                    writer = new FileWriter(outputFile);
                    writer.write(code);
                    writer.flush();
                } finally {
                    try {
                        writer.close();
                    } catch (Exception e) {
                    }
                }
            } catch (IOException exp) {
                exp.printStackTrace();
            }
        }
    }

    public static Object load(String wholeClassName) throws IllegalAccessException, InstantiationException, ClassNotFoundException, MalformedURLException {
        // Create a new custom class loader, pointing to the directory that contains the compiled
        // classes, this should point to the top of the package structure!
        URLClassLoader classLoader = new URLClassLoader(new URL[]{new File("./").toURI().toURL()});
        // Load the class from the classloader by name....
        Class<?> loadedClass = classLoader.loadClass(wholeClassName);
        // Create a new instance...
        Object obj = loadedClass.newInstance();
        return obj;
    }

}

..

package util.injection;

public class Function {

    private static final long serialVersionUID = 7526472295622776147L;

    public void test() {
                System.out.println("Hello from original Function!");
    }

    public int getID() {
        return -1;
    }

    public void apply(float[] img, int x, int y) {

    }

    public double dot(double[] x, double[] y) {
            return 0;
    }
}

2
不,将she bang放在任何脚本上并不能使其执行。Bash依赖于带有shebang的文件忽略以“#”开头的行的事实。因此,任何可以忽略带有shebang的第一行的脚本语言或字节码都可以工作。
如果您的语言不支持“#”作为注释或忽略第一行,则需要通过另一种忽略该语言的脚本语言进行处理。
话虽如此,您可以编写包含二进制代码块的bash脚本,并将其内联调用。游戏安装程序就是这样做的。

你如何在Bash脚本中使用二进制Blob? - William Rosenbloom
1
将二进制负载添加到您的Shell脚本中。请参考:http://www.linuxjournal.com/content/add-binary-payload-your-shell-scripts - Alexander Oh

2

自JDK11以来,您可以直接使用源代码进行操作:

#!/usr/lib/jvm/jdk-11/bin/java --source 8

public class Oneliner {
  public static void main(String[] args){
    System.out.println("ok");
  }
}

请注意,如果文件扩展名不是.java,则必须使用--source参数。支持值6-11,但6被标记为已弃用。


1
在两年半之后,我发现了一个比2016年给出的更完整的答案。与John Hascall的回答相反,Java二进制文件可以嵌入可执行文件中。这篇文章解释了如何在类Unix系统中通过向shell脚本添加二进制有效载荷来实现此操作。
我将提供该过程的简要概述。
假设有一个名为any_java_executable.jar的可执行jar文件, 您想创建一个名为my_executable的可执行文件, 并且有一个名为basis.sh的脚本文件,其内容如下:
#!/bin/sh
MYSELF=`which "$0" 2>/dev/null`
[ $? -gt 0 -a -f "$0" ] && MYSELF="./$0"
java=java
if test -n "$JAVA_HOME"; then
    java="$JAVA_HOME/bin/java"
fi
exec "$java" $java_args -jar $MYSELF "$@"
exit 1

可以通过运行以下两个命令创建本地可执行文件。

cat basis.sh any_java_executable.jar > my_executable;
chmod +x my_executable;

那么my_executable是一个本地可执行文件,能够在不依赖于jar文件位置的情况下运行Java程序。可以通过运行以下命令来执行它:

./my_executable [arg1 [arg2 [arg3...]]]

如果将其放置在/usr/local/bin中,它可以作为CLI工具在任何地方使用。


这个“有效”的原因是Java忽略了其jar文件开头的“垃圾”脚本。然而,我找不到任何地方记录这种行为作为您可以依赖的内容。所以,除非它是一个记录的特性 - 可能有效,可能无效,可能停止工作,可能让猴子从你的鼻子里飞出来。 - John Hascall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接