首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

讓 JAR 直接跑起來:Main-Class 清單屬性注入與可執行 JAR 構建全攻略

     編輯:Java 相關知識

痛點:為什麼 `java -jar myapp.jar` 會報「無主清單屬性」?

打包好的 JAR 放在那裡,明明有主類,為何 JVM 拒絕執行?核心原因在於 META-INF/MANIFEST.MF 缺少 Main-Class 屬性。JVM 的 -jar 模式**強制要求**清單文件指定入口類,且忽略 classpath 參數。本文從原理到工程化,徹底解決「可執行 JAR」構建問題。

一、清單文件解剖

Manifest-Version: 1.0
Created-By: Maven Jar Plugin 3.3.0
Built-By: jenkins
Build-Jdk: 17.0.8
Main-Class: com.example.Application   # 關鍵入口
Class-Path: lib/dependency1.jar lib/dependency2.jar  # 相對路徑,僅 -jar 模式生效
  • 位置固定: 必須在 META-INF/MANIFEST.MF
  • 格式嚴格: 鍵值對以冒號空格分隔,行長超 72 字符需換行續寫(以空格開頭)
  • Class-Path 陷阱: 僅支持相對路徑,不支持絕對路徑、環境變量、通配符;依賴 JAR 必須與主 JAR 同目錄或子目錄

二、主流構建工具一鍵配置

Maven: maven-jar-plugin / maven-shade-plugin / maven-assembly-plugin

<!-- 方案 1:基礎 JAR + 外部依賴 (需配合 maven-dependency-plugin copy-dependencies) -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-jar-plugin</artifactId>
  <version>3.3.0</version>
  <configuration>
    <archive>
      <manifest>
        <mainClass>com.example.Application</mainClass>
        <addClasspath>true</addClasspath>
        <classpathPrefix>lib/</classpathPrefix>
      </manifest>
      <manifestEntries>
        <Build-Time>${maven.build.timestamp}</Build-Time>
        <Git-Commit>${git.commit.id}</Git-Commit>
      </manifestEntries>
    </archive>
  </configuration>
</plugin>

<!-- 方案 2:Fat/Shade JAR (單文件部署,內嵌依賴) -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-shade-plugin</artifactId>
  <version>3.5.0</version>
  <executions>
    <execution>
      <phase>package</phase>
      <goals><goal>shade</goal></goals>
      <configuration>
        <transformers>
          <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
            <mainClass>com.example.Application</mainClass>
          </transformer>
          <!-- 處理 SPI 服務文件合併 -->
          <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
        </transformers>
      </configuration>
    </execution>
  </executions>
</plugin>

Gradle: application / shadow 插件

// 方案 1:標準分發包 (bin/lib 結構)
plugins { id 'application' }
application { mainClass = 'com.example.Application' }
// 執行 ./gradlew installDist 生成可運行分發包

// 方案 2:Fat JAR (Shadow 插件)
plugins { id 'com.github.johnrengelman.shadow' version '8.1.1' }
shadowJar {
  manifest {
    attributes 'Main-Class': 'com.example.Application'
  }
  mergeServiceFiles() // 合併 META-INF/services
}
// 執行 ./gradlew shadowJar

Spring Boot: 內置 Fat JAR 支持

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <mainClass>com.example.Application</mainClass> <!-- 通常自動檢測 -->
    <layers><enabled>true</enabled></layers> <!-- Docker 分層優化 -->
  </configuration>
  <executions>
    <execution><goals><goal>repackage</goal></goals></execution>
  </executions>
</plugin>

三、手動修補現有 JAR:MakeJarRunnable 升級版

當無法修改構建腳本(如第三方 JAR),需事後注入 Main-Class。原文代碼有資源洩漏與異常處理缺陷,下面是生產級重寫:

import java.io.*;
import java.nio.file.*;
import java.util.jar.*;
import java.util.zip.*;

public class JarMainClassInjector {
    public static void main(String[] args) throws Exception {
        if (args.length != 3) {
            System.err.println("Usage: JarMainClassInjector   ");
            System.exit(1);
        }
        Path in = Paths.get(args[0]);
        String mainClass = args[1];
        Path out = Paths.get(args[2]);

        if (!Files.exists(in)) throw new FileNotFoundException("Input JAR not found: " + in);
        if (Files.exists(out) && !Files.isWritable(out)) throw new IOException("Output not writable: " + out);

        try (JarInputStream jis = new JarInputStream(Files.newInputStream(in));
             JarOutputStream jos = new JarOutputStream(Files.newOutputStream(out))) {

            Manifest manifest = jis.getManifest();
            if (manifest == null) manifest = new Manifest();
            Attributes attrs = manifest.getMainAttributes();
            
            String old = attrs.putValue("Main-Class", mainClass);
            if (old != null) 
                System.out.println("WARNING: Overwriting existing Main-Class: " + old);

            jos.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME));
            manifest.write(jos);

            byte[] buf = new byte[8192];
            JarEntry entry;
            while ((entry = jis.getNextJarEntry()) != null) {
                if (JarFile.MANIFEST_NAME.equals(entry.getName())) continue; // 跳過舊清單
                jos.putNextEntry(entry);
                jis.transferTo(jos); // JDK 9+ 高效傳輸
                jos.closeEntry();
            }
        }
        System.out.println("Done. Run: java -jar " + out);
    }
}

關鍵改進: try-with-resources 保證關閉、transferTo 零拷貝、覆蓋提示而非報錯退出、參數校驗。

四、進階主題:模塊化 (JPMS) 與 多版本兼容

場景清單屬性說明
JDK 9+ 模塊化應用 Main-Class + Module-Version 需配合 --module-path,或打包為 jlink 運行時鏡像
多版本 JAR (MRJAR) Multi-Release: true 類文件放在 META-INF/versions/{9,11,17}/,JVM 自動選擇最高匹配版本
啟用預覽特性 JDK-Version + 運行時 --enable-preview 清單中聲明編譯版本,運行時需顯式啟用預覽

五、常見坑與避雷指南

  1. 雙擊無反應 (Windows): 檢查 .jar 文件關聯是否指向 javaw.exe (無控制台) 而非 java.exe;GUI 程序用 javaw,控制台程序需 java 或批處理腳本包裹。
  2. Class-Path 失效: 僅在 -jar 模式生效;路徑含空格需 URL 編碼 (%20);依賴 JAR 必須真實存在於相對路徑。
  3. Fat JAR 衝突: 多模塊打包導致 META-INF/services/、module-info.class、.kotlin_module 覆蓋。必須配置 ServicesResourceTransformer、AppendingTransformer 或排除策略。
  4. 簽名失效: 修改清單會破壞 JAR 簽名。若需保留簽名,必須在構建階段注入,或重新簽名 (jarsigner)。
  5. 類加載器差異: -jar 模式下 ClassLoader.getSystemClassLoader() 為 URLClassLoader 僅含清單 Class-Path;不含 -cp 指定路徑。避免依賴系統類加載器載入插件/配置。

六、容器化部署最佳實踐

# Dockerfile 多階段構建
FROM eclipse-temurin:17-jdk AS builder
WORKDIR /app
COPY . .
RUN ./gradlew shadowJar --no-daemon

FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/build/libs/*-all.jar app.jar
# 關鍵:容器入口點直接用 java -jar,無需啟動腳本
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]

結語

「可執行 JAR」看似微不足道,實則是 Java 部署鏈路的關鍵一環。從 Maven/Gradle 插件配置、手動注入工具、到模塊化適配、容器化交付,每一層都暗藏細節陷阱。建議團隊將「構建產物標準化」納入工程規範:統一 Fat JAR 策略、強制 Main-Class 聲明、自動化簽名驗證、CI 門禁檢查 java -jar 煙霧測試。讓每一個 JAR 都能體面地「跑起來」,是對部署下游最大的善意。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved