Ohhnews

分类导航

$ cd ..
foojay原文

Java 脚本化:现代自动化任务的新选择

#java#自动化#脚本开发#jbang#编程效率

目录


脚本编写的困境

作为一名 Java 开发人员,你是否和我一样,总是记不住 Bash 或 Python 的语法来编写简单的脚本?

你最终只能靠“凭感觉编码”(vibe coding),然后在需要修改时又陷入困境。要是能用 Java 来写该多好!

你可能会说:“Java 不适合写脚本”,或者“我不想用 Maven 或 Gradle”。现代 Java 已经悄然消除了那些使其不适合脚本编写的传统障碍。凭借即时执行、Shebang 支持和零配置自动化,Java 已经演变成一种精简的脚本语言。你可以编写精确、可维护的代码,而无需使用不熟悉的语言进行“凭感觉编码”。

在本文中,我将向你展示 Java 如何成为一流的脚本语言。它或许会成为你新的自动化工具首选。

告别手动编译

如果你想编写 Java 脚本,面临的第一个问题就是编译。对于一个简单的脚本,你可能不想运行 javac 并管理 .class 文件。但自 Java 11 起(通过 JEP 330),你可以通过 java 启动器直接运行源代码程序。

单文件执行

假设你想用 Java 重写 ls 命令,你可以这样做:

$ java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermissions;
import java.text.DecimalFormat;

public class ListFiles {

    static void main(String[] args) throws IOException {
        try (var stream = Files.list(Path.of("."))) {
            stream
                .filter(path -> !path.getFileName().toString().startsWith("."))
                .sorted()
                .forEach(ListFiles::printEntry);
        }
    }

    private static void printEntry(Path path) {
        var isDir = Files.isDirectory(path);
        var name = path.getFileName().toString();
        var display = isDir ? "\u001B[34m" + name + "\u001B[0m" : name;

        System.out.printf(
                "%-12s %-10s %s%n", permissions(path, isDir),
                size(path, isDir),
                display
        );
    }

    private static String size(Path path, boolean isDir) {
        if (isDir) return "-";
        try {
            long bytes = Files.size(path);
            if (bytes == 0) return "0 B";

            var units = new String[]{"B", "KB", "MB", "GB", "TB"};
            int group = (int) (Math.log(bytes) / Math.log(1024));
            return new DecimalFormat("#,##0.#").format(bytes / Math.pow(1024, group)) + " " + units[group];
        } catch (IOException _) {
            return "?";
        }
    }

    private static String permissions(Path path, boolean isDir) {
        try {
            return (isDir ? "d" : "-") + PosixFilePermissions.toString(Files.getPosixFilePermissions(path));
        } catch (Exception _) {
            return (isDir ? "d" : "-") +
                   (Files.isReadable(path) ? "r" : "-") +
                   (Files.isWritable(path) ? "w" : "-") +
                   (Files.isExecutable(path) ? "x" : "-") + "------";
        }
    }
}

如果你想运行它,无需 javac,直接执行:

$ bash
java ListFiles.java

无需 javac,也没有 .class 文件堆积在你的目录中。只需即时执行。

多文件支持

Java 22 起(通过 JEP 458),你不再局限于单文件程序。现在你可以编写多文件程序并直接运行。Java 启动器会自动定位并编译子目录中相关的源文件。例如:

$ java
public class Greet {

    public static void main(String[] args) {
        var message = new Message("Hello", "Folks");
        new MessagingService().sendMessage(message);

    }
}
$ java
record Message(String welcomingWord, String name) {
}
$ java
class MessagingService {

    public void sendMessage(Message message){
        System.out.println(message.welcomingWord() + " " + message.name());
    }

}

这只是一个过于复杂的“Hello World!”示例,但它展示了多文件程序可以被直接运行:

$ bash
java Greet.java  # 自动查找并编译依赖项

该功能对于需要多个类的复杂脚本特别有用,也非常适合不想处理项目配置的学习者。

在底层,启动器会自动调用编译器并将编译结果存储在内存中。没有构建工具的冗余,没有中间文件,只有纯粹的执行。就脚本编写而言,Java 现在感觉和 Python 或 Ruby 一样即时。

为何如此繁琐?

好的脚本语言的一个关键点是消除样板代码。老实说,Java 有时确实显得有些冗长。最臭名昭著的例子就是 public static void main(String[] args) 签名。如果你只是想写个简单的脚本,这些代码大多毫无意义。打印“Hello World!”至少需要 5 行代码,其中大部分都是样板。

$ java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

Java 的演进

Java 25 在 Java 21 和 22 的预览版基础上,引入了多项直接解决冗余问题的特性。

紧凑源文件 (Compact Source Files)实例 main 方法 (Instance Main Methods) 消除了对样板签名的需求。

  • 紧凑源文件:可以在顶层直接编写方法和字段,无需类声明。
  • 实例 main 方法:将 public static void main(String[] args) 简化为 void main()

有了这些特性,“Hello World”可以变得如此简单:

$ java
void main() {
    System.out.println("Java");
}

JVM 会自动选择入口点,优先使用实例 main() 方法。这使得你的脚本看起来更自然,不再像传统的企业级 Java 代码。

更简单的控制台交互

过去,与控制台交互有些麻烦。首先,每次想打印内容都要输入 System.out.println("...")。当需要读取输入时,情况更糟,你不得不使用 BufferedReaderScanner。于是代码很快变成了这样:

$ java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

void main() {
    System.out.println("Please enter your name:");
    String name = "";
    try {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        name = reader.readLine();
    } catch (IOException ioe) {
        ioe.printStackTrace();
    }
    System.out.println("Hello, " + name);
}

得益于“紧凑源文件”,现在你可以使用 IO 类:

$ java
void main() {
    String name = IO.readln("Please enter your name:");
    IO.print("Hello, ");
    IO.println(name);
}

IO 类位于 java.lang 包中,因此每个源文件都会隐式导入它。

如果你想了解更多信息,建议阅读 JEP 512

Java 原生脚本:Shebang 支持

任何脚本语言的一个定义性特征是能够像 ./myscript 那样直接运行,而无需显式调用解释器。许多脚本语言通过“Shebang”行 (#!) 来实现这一点。文件开头的这一行会告诉操作系统使用哪个程序来运行它。

Java 11(通过 JEP 330)起,java 启动器支持此约定。

让我们使用上述现代 Java 特性,将之前的 ListFiles 示例重构为一个可执行脚本:

$ bash
#!/usr/bin/java --source 25

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermissions;
import java.text.DecimalFormat;

void main(String[] args) throws IOException {
    var dir = Path.of(args.length > 0 ? args[0] : ".");
    if (!Files.exists(dir)) {
        IO.println("Error: Directory '" + dir + "' does not exist.");
        System.exit(1);
    }
    if (!Files.isDirectory(dir)) {
        IO.println("Error: '" + dir + "' is not a directory.");
        System.exit(1);
    }

    try (var stream = Files.list(dir)) {
        stream
            .filter(path -> !path.getFileName().toString().startsWith("."))
            .sorted()
            .forEach(this::printEntry);
    }
}

void printEntry(Path path) {
    var isDir = Files.isDirectory(path);
    var name = path.getFileName().toString();
    var display = isDir ? "\u001B[34m" + name + "\u001B[0m" : name;

    IO.println("%-12s %-10s %s".formatted(
        permissions(path, isDir),
        size(path, isDir),
        display
    ));
}

String size(Path path, boolean isDir) {
    if (isDir) return "-";
    try {
        long bytes = Files.size(path);
        if (bytes == 0) return "0 B";

        var units = new String[]{"B", "KB", "MB", "GB", "TB"};
        int group = (int) (Math.log(bytes) / Math.log(1024));
        return new DecimalFormat("#,##0.#").format(bytes / Math.pow(1024, group)) + " " + units[group];
    } catch (IOException _) {
        return "?";
    }
}

String permissions(Path path, boolean isDir) {
    try {
        return (isDir ? "d" : "-") + PosixFilePermissions.toString(Files.getPosixFilePermissions(path));
    } catch (Exception _) {
        return (isDir ? "d" : "-") +
               (Files.isReadable(path) ? "r" : "-") +
               (Files.isWritable(path) ? "w" : "-") +
               (Files.isExecutable(path) ? "x" : "-") + "------";
    }
}

保存文件(例如命名为 ls,无需 .java 后缀)并使其可执行:

$ bash
chmod +x ls
./ls

你的 Java 代码现在就是一个一流的 CLI 命令,与 Bash 或 Python 脚本别无二致。

可移植性提示:将脚本移动到 PATH 中的目录,例如 /usr/local/bin/

$ bash
sudo mv ls /usr/local/bin/
ls  # 现在可以在任何目录下运行

突然间,Java 感觉就像一门原生的脚本语言。

使用 JBang 进行高级自动化

这些内置特性使 Java 成为了一门有能力的脚本语言。但 JBang 通过消除设置开销并启用高级功能,将其提升到了一个新的水平。

什么是 JBang?

JBang 让你能够以前所未有的简便方式创建、编辑和运行独立的 Java 程序。有了 JBang,你的脚本可以自动获取依赖,无需深入研究 Maven 或 Gradle。它可以在任何平台(包括 Docker 和 Github Actions)上运行。最棒的是,如果缺失所需的 Java 版本,JBang 会自动下载。

你还可以使用 JBang 运行任何 JAR 文件,无论是本地还是在线(通过 Maven Central)。

运行 JBang 脚本非常简单:

$ bash
jbang MyScript.java

依赖管理

这是 JBang 真正大放异彩的地方。使用特殊的注释在文件中直接声明依赖,无需 Maven 或 Gradle。

注意 /// 开头的 Shebang 行,这是 JBang 特有的,允许脚本直接执行。

$ java
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS com.google.code.gson:gson:2.10.1
//DEPS org.apache.commons:commons-lang3:3.14.0

import com.google.gson.Gson;
import org.apache.commons.lang3.StringUtils;

void main() {
    Gson gson = new Gson();
    var person = new Person("John", 30);
    String json = gson.toJson(person);
    IO.println(StringUtils.capitalize(json));
}

record Person(String name, int age) {}

JBang 会自动下载并管理这些依赖。无需 Maven,无需 Gradle,只需声明并使用即可。

强大的功能

JBang 提供了许多高级功能:

IDE 集成:JBang 可以安装 VSCodium,生成项目结构,并在你的 IDE 中打开脚本:

$ bash
jbang edit MyScript.java  # 在你的 IDE 中打开,并提供完整的自动补全

原生二进制文件:它支持使用 GraalVM 生成原生镜像二进制文件,以实现近乎即时的启动:

$ bash
jbang export native MyScript.java
./MyScript  # 闪电般快速的原生执行

使用原生镜像时要小心,特别是在反射方面。更多信息请参阅此处

模板:JBang 提供了一套模板,帮助你快速启动脚本。

$ bash
# 创建 CLI 应用
jbang init --template=cli myapp.java

# 创建 Web 服务器
jbang init --template=qcli webapp.java

# 创建 JavaFX 应用
jbang init --template=javafx gui.java

提升脚本能力:使用 Picocli 实现丰富的 CLI

简单的脚本是一个很好的起点。然而,为了获得健壮的 CLI 体验,你通常需要选项、位置参数和帮助菜单。这时 Picocli 就派上用场了。

专业级工具

Picocli 与 JBang 完美集成,可实现带 ANSI 颜色的帮助消息和强类型参数解析。构建具有自动生成帮助、版本信息、类型检查和美观错误消息的专业 CLI。

$ java
///usr/bin/env jbang "$0" "$@" ; exit $?

//DEPS info.picocli:picocli:4.7.7
//DEPS info.picocli:picocli-codegen:4.7.7
//NATIVE_OPTIONS --no-fallback -H:+ReportExceptionStackTraces

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;

@Command(name = "greet", mixinStandardHelpOptions = true, version = "1.0",
         description = "Greets users with style")
class Greet implements Runnable {
    @Option(names = {"-c", "--count"}, description = "Number of greetings")
    int count = 1;

    @Parameters(description = "Name(s) to greet")
    String[] names;

    public void run() {
        for (int i = 0; i < count; i++) {
            for (String name : names) {
                IO.println("Hello, " + name + "!");
            }
        }
    }
}

void main(String[] args) {
    new CommandLine(new Greet()).execute(args);
}

运行它:

$ bash
jbang greet.java --help
jbang greet.java -c 3 Alice Bob

零代码 CLI 体验

Picocli 处理了以下复杂性:

  • 使用 --help 自动生成帮助
  • 使用 --version 显示版本
  • 类型转换和验证
  • 用于提高可读性的 ANSI 颜色
  • 子命令和命令层级

你的脚本现在可以媲美专业构建的 CLI 工具了。## Java 的新纪元

我希望你现在已经明白,Java 不仅仅代表着企业级的复杂性和冗长的样板代码。你可以轻松地利用它构建强大的自动化脚本。

为什么这很重要

可维护性:Java 脚本提供了 Bash 往往缺乏的类型系统和结构。六个月后,强类型和 IDE 的支持会让理解和修改代码变得更加容易。

熟悉感:你已经掌握了 Java。既然你可以利用现有的专业知识,为什么还要为了自动化而切换到另一种语言呢?

强大功能:整个 Java 生态系统触手可及。数以百万计的库和经过实战检验的框架,都可以集成在简单的脚本中。

性能表现:GraalVM 原生镜像(Native Images)让你的脚本能够编译成具有即时启动速度的原生二进制文件,足以与 Go 或 Rust 媲美。

总结

得益于 Shebang 支持、JBang 以及实例 main 方法,Java 现在已成为一台精简的自动化机器。繁文缛节已成过去,阻碍已被消除。剩下的就是一个功能强大、类型安全、易于维护的脚本语言,而它恰好就是 Java。

下次当你需要编写快速自动化脚本时,不要再习惯性地去用 Bash 或 Python。尝试一下 Java,你可能会惊讶于它的体验是多么出色。IDE 为你提供助力,类型检查在运行时之前就能捕获 Bug,而精准性取代了“凭感觉编程”(vibe coding)。

Java 脚本并非遥不可及的未来,它已经到来,而且表现非凡。

本文转自 JavaScript (No, Not That One): Modern Automation with Java,发布于 foojay