golang知识集

用 Go 手搓一个 Java 构建工具:当 IDE 不在身边时的

2025-11-05  本文已影响0人  Mgx_无心

"工欲善其事,必先利其器" ——但有时候,你连工具箱都丢了。

最近,我遇到了一个略显尴尬的场景:需要紧急修改一段老 Java 项目代码,但手头的电脑只装了 JDK,没有安装任何 Java IDE(比如 IntelliJ IDEA 或 Eclipse)。更糟的是,这个项目没有现成的构建脚本(Maven/Gradle),甚至连一个简单的 build.xml 都没有。

怎么办?难道要花半小时下载安装 IDE?还是手动敲 javac 和 jar 命令?

不!我决定用 Go 写一个轻量级的构建工具——既解决燃眉之急,也顺便重温一下 Java 编译和打包的底层知识。

于是,就有了这个不到 200 行的 Go 程序:一个能自动编译 .java 源码、处理依赖库、生成可执行 JAR 的命令行工具。

🛠️ 工具功能一览

这个工具名为 buildjar,用法极其简单:

buildjar <src_dir> <lib_dir> <main_class> <output_jar>

例如:

buildjar src _lib Test Test.jar

它会自动完成以下五步:

  1. 扫描源码目录,生成 sources.txt(供 javac @sources.txt 使用)
  2. 编译所有 Java 文件 到 bin/ 目录,自动包含 lib_dir 下的所有 .jar 依赖
  3. 复制依赖库 到 <output_jar>_lib/(如 Test.jar_lib/)
  4. 生成符合规范的 MANIFEST.MF,包含 Main-Class 和自动换行的 Class-Path
  5. 打包成可执行 JAR,并提示用户运行时需保持 JAR 与依赖库同目录

整个过程全自动、零配置,专为"裸机环境"设计。

🔍 技术细节剖析

1. 为什么用 sources.txt?

Java 编译器 javac 支持通过 @filename 读取文件列表,避免命令行参数过长(尤其在 Windows 上有 8191 字符限制)。

我们递归遍历 src_dir,把所有 .java 文件路径写入 sources.txt,并用 GBK 编码保存——因为某些老项目源码文件名或路径含中文,而 Windows 的 javac 默认用系统编码(通常是 GBK),UTF-8 反而会报错。

gbkWriter := transform.NewWriter(file, simplifiedchinese.GBK.NewEncoder())

💡 小知识:javac 在 Windows 上对路径编码非常敏感,这是很多"中文路径编译失败"的根源。

2. 依赖管理:复制而非嵌入

为了保持简单和兼容性,没有把依赖 JAR 打进主 JAR(即不使用 jar -uf 合并),而是采用经典的"JAR + 同级 lib 目录"模式。

这种方式兼容性极佳,连 Java 1.2 都能跑,且避免了"JAR-in-JAR"加载类的复杂问题。

3. MANIFEST.MF 的自动换行规则

Java 的 MANIFEST.MF 有严格格式要求:

我们的 writeWrappedLine 函数精准实现了这一规则:

// 第一行:0 ~ 71 字符
// 后续行:以空格开头,再接最多 71 个字符

例如:

Class-Path: . very-long-lib1.jar very-long-lib2.jar very-long-lib3.jar ve
 ry-long-lib4.jar

⚠️ 重要提醒:如果违反此规则,java -jar 会直接报错:Invalid or corrupt jarfile。

4. 跨平台路径处理

虽然工具主要面向 Windows(因 GBK 编码需求),但我们也做了兼容:

relPath = strings.ReplaceAll(relPath, "\\", "/")

🧪 使用示例

假设项目结构如下:

my-project/
├── src/
│   └── Test.java
├── _lib/
│   ├── gson-2.8.9.jar
│   └── commons-lang3-3.12.0.jar
└── buildjar.exe  # 我们的 Go 工具

执行:

buildjar src _lib Test Test.jar

输出:

✅ 生成 sources.txt 完成
✅ 编译完成
✅ 复制依赖库到 Test.jar_lib
✅ 生成 MANIFEST.MF 完成
🎉 成功生成可执行 JAR: Test.jar
📌 请确保运行时 Test.jar 与 Test.jar_lib 在同一目录下

最终目录:

my-project/
├── Test.jar
├── Test.jar_lib/
│   ├── gson-2.8.9.jar
│   └── commons-lang3-3.12.0.jar
└── ...

运行:

java -jar Test.jar

完美!

💡 为什么不用 Maven/Gradle?

这个工具的哲学是:最小依赖,最大兼容,开箱即用。

📦 源码与扩展

完整源码:

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
    "os/exec"
    "path/filepath"
    "strings"

    "golang.org/x/text/encoding/simplifiedchinese"
    "golang.org/x/text/transform"
)

func main() {
    if len(os.Args) != 5 {
        fmt.Println("Usage: buildjar <src_dir> <lib_dir> <main_class> <output_jar>")
        fmt.Println("Example: buildjar src _lib Test Test.jar")
        os.Exit(1)
    }

    srcDir := os.Args[1]
    libDir := os.Args[2]
    mainClass := os.Args[3]
    outputJar := os.Args[4]

    // Step 0: 创建/清空 bin 目录
    os.RemoveAll("bin")
    os.MkdirAll("bin", 0755)

    // Step 1: 生成 sources.txt
    sourcesFile := "sources.txt"
    err := generateSourcesList(srcDir, sourcesFile)
    if err != nil {
        fmt.Printf("❌ 生成 sources.txt 失败: %v\n", err)
        os.Exit(1)
    }
    fmt.Println("✅ 生成 sources.txt 完成")

    // Step 2: 编译 Java 文件
    err = compileJava(sourcesFile, libDir)
    if err != nil {
        fmt.Printf("❌ 编译失败: %v\n", err)
        os.Exit(1)
    }
    fmt.Println("✅ 编译完成")

    // Step 3: 复制 _lib 到 outputJar_lib
    libTargetDir := outputJar[:len(outputJar)-4] + "_lib"
    err = copyDir(libDir, libTargetDir)
    if err != nil {
        fmt.Printf("❌ 复制 %s → %s 失败: %v\n", libDir, libTargetDir, err)
        os.Exit(1)
    }
    fmt.Printf("✅ 复制依赖库到 %s\n", libTargetDir)

    // Step 4: 生成 MANIFEST.MF(使用相对路径)
    err = generateManifest(libTargetDir, mainClass)
    if err != nil {
        fmt.Printf("❌ 生成 MANIFEST.MF 失败: %v\n", err)
        os.Exit(1)
    }
    fmt.Println("✅ 生成 MANIFEST.MF 完成")

    // Step 5: 打包 JAR
    err = createJar(outputJar)
    if err != nil {
        fmt.Printf("❌ 打包 JAR 失败: %v\n", err)
        os.Exit(1)
    }
    fmt.Printf("🎉 成功生成可执行 JAR: %s\n", outputJar)
    fmt.Printf("📌 请确保运行时 %s 与 %s 在同一目录下\n", outputJar, libTargetDir)
}

// 生成 sources.txt,递归列出所有 .java 文件
func generateSourcesList(srcDir, outputFile string) error {
    // 创建文件
    file, err := os.Create(outputFile)
    if err != nil {
        return err
    }
    defer file.Close()

    // 创建 GBK 编码的 Writer
    gbkWriter := transform.NewWriter(file, simplifiedchinese.GBK.NewEncoder())
    writer := bufio.NewWriter(gbkWriter)
    defer writer.Flush()

    return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if !info.IsDir() && strings.HasSuffix(path, ".java") {
            _, err := writer.WriteString(path + "\r\n") // Windows 换行用 \r\n 更稳妥
            return err
        }
        return nil
    })
}

// 编译 Java 文件
func compileJava(sourcesFile, libDir string) error {
    // 构建 classpath: 当前目录 + 所有 lib/*.jar
    classpath := "."
    jars, err := filepath.Glob(filepath.Join(libDir, "*.jar"))
    if err != nil {
        return err
    }
    for _, jar := range jars {
        classpath += string(os.PathListSeparator) + jar
    }

    cmd := exec.Command("javac", "-d", "bin", "-cp", classpath, "@"+sourcesFile)
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    return cmd.Run()
}

// 生成 MANIFEST.MF 文件,自动换行,每行最多72字符(含空格)
func generateManifest(libDir, mainClass string) error {
    manifestFile := "MANIFEST.MF"
    file, err := os.Create(manifestFile)
    if err != nil {
        return err
    }
    defer file.Close()

    writer := bufio.NewWriter(file)
    defer writer.Flush()

    // 写入 Manifest-Version 和 Main-Class
    fmt.Fprintln(writer, "Manifest-Version: 1.0")
    fmt.Fprintf(writer, "Main-Class: %s\n", mainClass)

    // 收集所有 jar 文件名(使用相对于 JAR 文件的路径)
    jars, err := filepath.Glob(filepath.Join(libDir, "*.jar"))
    if err != nil {
        return err
    }

    // 构建 Class-Path 行(使用相对路径)
    var classPath string = "Class-Path: ."
    for _, jar := range jars {
        // 获取 jar 文件名,拼接为:outputJar_lib/filename.jar
        relPath := filepath.Join(filepath.Base(libDir), filepath.Base(jar))
        // 统一使用正斜杠(兼容所有系统)
        relPath = strings.ReplaceAll(relPath, "\\", "/")
        classPath += " " + relPath
    }

    // 按 72 字符限制写入,支持多行
    writeWrappedLine(writer, classPath, 72)

    // 最后必须有一个空行
    fmt.Fprintln(writer, "")

    return writer.Flush()
}

// 写入带自动换行的行(用于 MANIFEST.MF 的 Class-Path)
func writeWrappedLine(writer *bufio.Writer, line string, maxLen int) {
    if len(line) <= maxLen {
        fmt.Fprintln(writer, line)
        return
    }

    // 写第一行(0 ~ maxLen-1)
    fmt.Fprintln(writer, line[:maxLen])

    // 剩余部分,每行 maxLen-1 个字符(因为开头要加一个空格,占1位)
    rest := line[maxLen:]
    for len(rest) > 0 {
        if len(rest) <= maxLen-1 {
            fmt.Fprintln(writer, " "+rest)
            break
        }
        fmt.Fprintln(writer, " "+rest[:maxLen-1])
        rest = rest[maxLen-1:]
    }
}

// 打包 JAR
func createJar(outputJar string) error {
    cmd := exec.Command("jar", "cfm", outputJar, "MANIFEST.MF", "-C", "bin", ".")
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    return cmd.Run()
}

// 递归复制目录
func copyDir(src, dst string) error {
    srcInfo, err := os.Stat(src)
    if err != nil {
        return err
    }

    err = os.MkdirAll(dst, srcInfo.Mode())
    if err != nil {
        return err
    }

    entries, err := os.ReadDir(src)
    if err != nil {
        return err
    }

    for _, entry := range entries {
        srcPath := filepath.Join(src, entry.Name())
        dstPath := filepath.Join(dst, entry.Name())

        if entry.IsDir() {
            err = copyDir(srcPath, dstPath)
            if err != nil {
                return err
            }
        } else {
            err = copyFile(srcPath, dstPath)
            if err != nil {
                return err
            }
        }
    }
    return nil
}

// 复制单个文件
func copyFile(src, dst string) error {
    srcFile, err := os.Open(src)
    if err != nil {
        return err
    }
    defer srcFile.Close()

    dstFile, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer dstFile.Close()

    _, err = io.Copy(dstFile, srcFile)
    return err
}

你还可以扩展一下:

✨ 结语:程序员的“瑞士军刀”精神

在这个 IDE 和框架高度自动化的时代,我们很容易忘记底层发生了什么。但当环境受限、工具缺失时,理解编译、链接、打包的本质,才能真正掌控代码。

用 Go 写一个 Java 构建工具,看似"跨界",实则是对两种语言生态的融会贯通。它不仅是应急方案,更是一次对"构建系统"本质的致敬。

真正的自由,不是拥有最强大的工具,而是在任何环境下都能创造工具。

上一篇 下一篇

猜你喜欢

热点阅读