打包好的 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.MFmaven-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>
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
<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),需事後注入 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 零拷貝、覆蓋提示而非報錯退出、參數校驗。
| 場景 | 清單屬性 | 說明 |
|---|---|---|
| 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 |
清單中聲明編譯版本,運行時需顯式啟用預覽 |
.jar 文件關聯是否指向 javaw.exe (無控制台) 而非 java.exe;GUI 程序用 javaw,控制台程序需 java 或批處理腳本包裹。Class-Path 失效: 僅在 -jar 模式生效;路徑含空格需 URL 編碼 (%20);依賴 JAR 必須真實存在於相對路徑。META-INF/services/、module-info.class、.kotlin_module 覆蓋。必須配置 ServicesResourceTransformer、AppendingTransformer 或排除策略。jarsigner)。-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 都能體面地「跑起來」,是對部署下游最大的善意。