简介面向需要在 Eclipse 中将 Java 工程导出为可执行 Jar、并包含第三方依赖库的开发者教程针对脱离 IDE 运行及迁移到新虚拟机等场景提供了从导出、配置到启动的可复用方案。PDF 文档围绕 Runnable JAR file 导出流程展开不只写出基本操作还点明三个易错点导出前 Run Configuration 中设置的 JVM 参数在 Jar 包中不再生效须在命令行另行指定第三方 Jar 包的三种处理方式应如何选择才能避免依赖缺失配置文件 conf/xx.properties 需与 Jar 包同级放置程序才能正常读取外部配置。同时给出 start java -Xmx256m -Xms128m -jar spider.jar 的标准命令以及 run.bat 批处理封装写法方便一键启动与后续改参。压缩包内为 1 个 PDF 文件约 197KB篇幅紧凑适合随时查阅。资源已有 2021 人学习内容体量虽精简但对刚接触打包部署的初中级 Java 开发者具有直接参考价值能节省自行试错的时间。1. Eclipse 导出可执行 Jar为什么你打的包总在别人机器上跑不起来把 Eclipse 里的 Java 工程导成可执行 Jar 文件并包含第三方依赖是很多新手的第一道坎双击没反应、找不到或无法加载主类、NoClassDefFoundError 轮着来。这篇围绕 Eclipse 导出可执行 Java 工程/可执行 Jar 文件包含第三方 Jar 包的实战笔记会把 MANIFEST.MF 的加载机制、三种 Library handling 模式、可复现的操作步骤和 5 个高频坑一次讲完。适合给同事交付小工具、做内部客户端分发以及还没接入自动化构建的 Java 开发者。2. 可执行 Jar 的底层机制MANIFEST 主类声明与类加载顺序2.1 可执行 jar 与普通 jar 的差别java -jar 到底读了什么Jar 本质就是 zip可执行与否差在一个 META-INF/MANIFEST.MF。java -jar xxx.jar启动时并不会扫描 jar 里所有 class 去猜哪个类有 main 方法而是直接读 MANIFEST 里的 Main-Class 键拿到完整限定类名再反射调用。这个键缺失、写错或者类名大小写不对JVM 都会直接退回只剩一句 no main manifest attribute。另一个容易被忽略的键是 Class-Path。当第三方依赖没有被打进同一个 jar 时你必须在 Class-Path 里列出它们的相对路径多个路径用空格分隔路径基准是当前 jar 所在的目录。Eclipse 导出可执行 Jar 文件时所有关于“包含第三方 Jar 包”的处理最终都变成这两个键的组合Main-Class 负责找入口Class-Path 负责找依赖。我见过不少人在 MANIFEST 上踩坑其实它语法很简单三行就能干活Manifest-Version: 1.0 Main-Class: com.example.Main Class-Path: lib/commons-io.jar lib/gson.jar三点细节Main-Class 必须是完整类名不是文件名不写 .class 后缀Class-Path 的多个 jar 用空格分隔如果依赖路径里有空格尽量把依赖放同级的无空格目录MANIFEST 每行理论长度有 72 字节限制Eclipse、Maven 这类工具自己会折叠续行人肉编写时别把整行写成长到百来字节的路径。近期看到有人用一行超长 Class-Path 手工改 jarLinux 下直接启动失败浪费了不少时间。另外值得提一句jar 里的路径是区分大小写的。Windows 上文件系统大小写不敏感同样的 Class-Path 在 Windows 能跑拷到 Linux 上就找不到类这类问题尤其隐蔽只会表现为启动时 NoClassDefFoundError。2.2 三种 Library handling 模式Extract、Package、Copy 的机制差异Eclipse 导出向导里那一组单选按钮是理解整个问题的枢纽。它们改变的不是你的代码而是依赖进入产物的方式。Extract required libraries into generated JAR最直接的方案。把每个依赖 jar 里的 .class 文件按包路径解压出来和你的 class 混进同一个 jar 文件系统。MANIFEST 里只需要 Main-Class因为所有依赖都已经躺在同一个 jar 里。优点是单文件、结构简单java -jar 就能启动缺点是依赖 jar 之间的同名类会被后处理的覆盖如果两个第三方库都带同一个老版本的 class最终跑起来的行为可能和 Eclipse 里不一样。这种问题不好定位属于打包阶段最磨人的一类。Package required libraries into generated JAR把第三方 jar 原样嵌到外层 jar 的根目录下不动里面的类文件。Eclipse 会把 Main-Class 改成 org.eclipse.jdt.internal.jarinjarloader.JarRsrcLoader再用 Rsrc-Main-Class 记录真实入口用 Rsrc-Class-Path 记录内嵌 jar 的相对位置。运行时加载器先构造一个包含所有内嵌 jar 的类加载器再反射调用真实 main。这样保住了依赖 jar 的完整目录也不会出现同名 class 互相覆盖的问题但它引入一个运行时引导加载器某些框架在扫描 jar 时可能扫不到内嵌依赖。Copy required libraries into a sub-folder next to the generated JAR第三方 jar 原样复制到生成的 jar 旁边某个子目录比如 myapp_libMANIFEST 里写 Class-Path 指向那些相对路径。这种模式透明度最高你能清楚看到主程序带了多少依赖也可以不动主 jar 单独替换某个依赖版本。代价是交付时必须连目录一起搬而且如果导入依赖时工程里用的是绝对路径Eclipse 生成的 Class-Path 可能被写成绝对路径打包输出在别的机器上直接失效。所以选择 Copy 模式后导出完第一件事就是检查 MANIFEST 里的 Class-Path 是不是相对路径。下面这张表是对三种模式的选型对照模式产物形态Class-Path 处理典型坑Extract单 jar依赖类嵌入不需要同名类互相覆盖Package单 jar内部嵌套依赖 jar由 JarRsrcLoader 处理框架扫描不到内嵌依赖Copyjar 外部 lib 目录写着相对/绝对路径路径变成绝对路径或漏拷目录选型口诀是交付一个文件给别人双击Extract 或 Package 都行依赖比较多、命名空间冲突风险高的选 Package要经常升级依赖或者你想让别人一眼看清用了哪些库选 Copy你的程序如果依赖 ServiceLoader 这类 SPI 机制先做小实验确认能否在对应模式下扫描到再决定。2.3 与 Maven 打包的边界什么时候该跳出 Eclipse 向导既然有 Maven Shade 和 Spring Boot 插件为什么还要在 Eclipse 里手动导出因为不少工程还没接 Maven或者使用者对 pom 构建不熟悉。Eclipse 向导能用最简单的方式把手工维护的 Java 工程变成可执行 jar不需要引入额外构建工具链这条路径对刚入门的人来说是最顺的。但也要认清边界。一旦工程用 Maven 管理依赖再回 Eclipse 点按钮就是重复劳动而且 Eclipse 导出并不会读 pom 的最终配置容易产生“开发时依赖 A 版本、打包时按工作空间引用 B 版本”的偏差。这个偏差平时不吭声直到用户机器上报 NoSuchMethodError 才会被发现。我的建议很简单手工作坊式工程用 Eclipse 导出走稳之后立刻用 Ant 脚本或 pom 固定下来当你发现同一个导出操作重复做了三次以上就该换脚本了。手工点按钮的次数越多不确定因素越多。3. 在 Eclipse 里完成一次可执行 Jar 导出从准备到产物3.1 导出前的两项准备编码统一与 Order and Export 勾选Eclipse 导出不是玄学但有一半的异常源于导出前的状态。第一项准备是编码。在 Window Preferences General Workspace 里把 Text file encoding 改成 UTF-8。如果你的代码文件是 UTF-8 但编译器按平台默认编码 GBK 编译中文字符串会变成乱码而且这种问题通常要到运行阶段才暴露改起来很费劲。建议连工程级右键 Properties Resource 也确认一遍两个地方都统一成 UTF-8 才踏实。第二项准备是 Java Build Path 里的导出勾选。右键工程 Properties Java Build Path Order and Export把所有需要随产物发布的第三方 jar 勾上。这里没勾的依赖即使编译、运行都正常导出时也会被 Eclipse 排除在外。为什么 Eclipse 会这么设计因为 Order and Export 这个列表不仅要管编译 classpath还要管发布时的依赖集合。你把依赖加进 Build Path 不意味着它要被打包只有勾了导出才算告诉 Eclipse“这个依赖属于交付物”。这项对多模块工程尤其容易翻车。如果你在主工程里引用了另一个工作区工程 A导出时如果 A 没有被勾选导出A 提供的类不会进产物运行时同样报 NoClassDefFoundError。处理方式是回到 Build Path Projects 页把 A 加入再到 Order and Export 确认 A 被勾选。3.2 实操File Export Runnable JAR file 的四个关键选项选中工程右键 Export展开 Java 节点选 Runnable JAR file。注意别和 Export JAR file 混淆后者只打包类文件不写 Main-Class导出来的多数情况不能直接 java -jar 运行。Runnable JAR file 才是负责生成可执行产物的入口。导出向导里四个选项逐个看。Launch configuration其实就是你运行主类时用的配置。Eclipse 只在真正跑过一次 Java Application 后才会生成对应的 Launch Configuration所以如果下拉框是空的先在 Package Explorer 里右键主类 Run As Java Application 跑一遍再回来选。这个配置里记录的 Main type 会变成 MANIFEST 的 Main-Class 值。Export destination填产物完整路径。建议选一个干净目录不要直接落在工程 output 目录避免旧 class 和 jar 混在一起后让人误判。Library handling三选一按第 2 章的准则。给不懂 Java 的人用选 Extract 或 Package给别人做二次开发的库选 Copy 并保留 lib 目录。Save as ANT script勾选后会在目标目录生成 build.xml。这个文件记录了本次导出的完整配置之后可以在命令行用 ant 重放。有人把它当一次性产物我更建议保留到工程里当参考。它是 Eclipse 给你留的“后悔药”万一你想把手工流程转成脚本直接改它比从零写 build.xml 省事得多。点 Finish 之后如果控制台没有报错再进入下一步验证。很多人点完就高高兴兴分发结果到了别人机器上各种问题就是因为少了验证这一步。3.3 验证产物解压看 MANIFEST别只看“能双击”把导出当成结束是新手习惯。我养成的习惯是导出完至少跑三条命令再交付jar tf my-tool.jar | grep -i MANIFEST.MF jar tf my-tool.jar | grep -i com/example/Main.class java -jar my-tool.jar第一条确认 META-INF/MANIFEST.MF 存在第二条确认入口类的 class 按预期路径存在第三条模拟使用者视角启动。若在 Eclipse 里能跑但这条命令失败优先看 MANIFEST 的 Main-Class。还想看依赖打包形态继续解压 MANIFESTjar xf my-tool.jar META-INF/MANIFEST.MF解出来的文本用编辑器打开。Package 模式会看到 Main-Class 被替换成 JarRsrcLoader真实入口写在 Rsrc-Main-ClassExtract 模式只有 Main-Class。这两者的差异能帮你确认当前产物是哪种形态而不是拿记忆里以为的配置去猜。这三件事做完产物才有资格交出去。验证的习惯一旦养成很多线上问题会在交付前就被拦下来。4. 把第三方 Jar 打进可执行 Jar三种常用方案的实战配置4.1 方案一Eclipse 原生 Package 模式单文件交付最省心最常见诉求是“一个 jar 文件我直接双击能用第三方库给我打包带走”。这时 Package required libraries into generated JAR 是最稳的一档。前提工程里所有第三方依赖已经在 Java Build Path Order and Export 中勾选。然后走第 3 章的导出向导Library handling 选 Package。导出后检查结构jar tf my-tool.jar | grep -i \.jar$输出里能看到 commons-io.jar 之类的依赖被完整保留在外层 jar 根目录。再看 MANIFESTMain-Class: org.eclipse.jdt.internal.jarinjarloader.JarRsrcLoader Rsrc-Main-Class: com.example.Main Rsrc-Class-Path: ./commons-io.jar ./gson.jar逻辑很清楚JarRsrcLoader 是 Eclipse 放进来的引导类它读 Rsrc-Class-Path把里面的 jar 逐个纳入自己维护的 URLClassLoader最后反射调用 Rsrc-Main-Class。这样内嵌依赖不会被解压也不会和你的 class 混在一起互相打扰。这个模式的边界是类加载行为。Java 标准 SPI 机制如 ServiceLoader会扫描 classpath 上的 META-INF/services 目录当依赖以 jar 嵌套 jar 的形式存在标准扫描可能看不见那些 jar 内部的资源。如果你做的东西用到 SPI或者依赖某些老框架做自动装配先写个最小用例验证别等交付完再发现问题。这类情况我一般直接换 Extract 或跳去 Maven Shade。4.2 方案二Copy 模式加相对 Class-Pathlib 与主程序分离另一种常见诉求是部署到服务器上希望 jar 体积小升级依赖时只替换 lib 文件。选 Copy required libraries into a sub-folder next to the generated JAR 后目标目录会出现主 jar 和一个 my-tool_lib 子目录目录名是按主 jar 的名字生成的。MANIFEST 里 Main-Class 保持不变Class-Path 指向子目录里的文件。如果依赖来自 Maven 仓库Eclipse 生成的 Class-Path 可能是绝对路径比如Class-Path: C:/Users/admin/.m2/repository/commons-io/commons-io/2.11.0/commons-io-2.11.0.jar这条路径在你自己机器上没有问题换一台机器直接 NoClassDefFoundError。所以 Copy 模式必须做一件事检查并修正 Class-Path 为相对路径。手动修正的做法jar xf my-tool.jar META-INF/MANIFEST.MF # 编辑 MANIFEST.MF把 Class-Path 改成 # Class-Path: my-tool_lib/commons-io.jar my-tool_lib/gson.jar jar ufm my-tool.jar META-INF/MANIFEST.MFjar ufm的第一个参数是被更新的 jar第二个是 Manifest 文件路径注意顺序别写反。更新完再运行一次确认。更可控的做法是直接从 Ant 脚本构建 jarproject namemy-tool defaultjar target namecompile mkdir dirbuild/classes/ javac srcdirsrc destdirbuild/classes encodingUTF-8 classpath fileset dirlib includes*.jar/ /classpath /javac /target target namejar dependscompile jar destfilebuild/my-tool.jar basedirbuild/classes manifest attribute nameMain-Class valuecom.example.Main/ attribute nameClass-Path valuemy-tool_lib/commons-io.jar my-tool_lib/gson.jar/ /manifest /jar /target /project这里把 Class-Path 写成了固定的相对路径不依赖任何本机信息。执行ant jar就得到一份可重复构建的产物。特别提醒一点lib 目录在工程里是用来编译的发布时拷贝到目标 jar 旁边的子目录名是否和 Class-Path 一致拷完看一眼目录很多部署翻车就是 jar_lib 和 lib 差了一个词。4.3 方案三Maven Shade 一键打 Fat Jar适合已有 pom 的工程一旦工程基于 Maven我建议放弃 Eclipse 的导出向导直接在 pom 里用 maven-shade-plugin。它的思路接近 Extract把依赖类解压进最终 jar生成一个 Fat Jar。插件同时会帮你合并 META-INF 下的服务描述文件处理同名资源冲突。pom.xml 中一段最小配置plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.4.1/version executions execution phasepackage/phase goals goalshade/goal /goals /execution /executions configuration transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.example.Main/mainClass /transformer /transformers filters filter artifact*:*/artifact excludes excludeMETA-INF/*.SF/exclude excludeMETA-INF/*.RSA/exclude excludeMETA-INF/*.DSA/exclude /excludes /filter /filters /configuration /pluginManifestResourceTransformer 负责写 Main-Classfilters 里的三段 exclude 是为了剔除旧 jar 自带的签名文件。如果不排除Fat Jar 运行时可能报 Invalid signature file digest原因是多个 jar 的签名信息叠在一起后校验失败。这个错误几乎只在打包第三方依赖时出现签名对普通 Java 程序没有意义直接排除最省事。执行mvn clean package。target 目录里会有两个 jar一个是你工程本来打的原始包另一个带 -shaded 后缀的才是 Fat Jar。交付时拿 -shaded 那个别拿错。再补一个 Shade 的进阶参数relocation。如果两个依赖 jar 里存在完全相同的类路径你可以把其中一个的包名整体改掉比如把 org.apache.http 迁移成 shaded.org.apache.http。这类问题在老项目里不少见配置方式是给 plugin 加 relocation 子节点。如果新工程从一开始就用 Shade一般不会遇到遇到时再用别一开始就堆参数。另外记住 Spring Boot 工程不要用 Shade 替代官方插件。Spring Boot 的可执行 jar 有特殊的目录结构官方插件生成的 Fat Jar 才能正确启动内嵌容器。Shade 更适合非 Spring 的普通 Java 服务或命令行工具。这个边界我见过不少人搞混结果 Spring Boot 打包出来连监听端口都没起。5. 导出可执行 Jar 的避坑手册5 个高频翻车点与排查路径下面五条是导出可执行 Jar 时我实际遇到过、也帮别人擦过的高频问题每条按现象、原因、解决来写方便你直接对着排查。5.1 现象双击 jar 没有反应命令行报“找不到或无法加载主类”现象准确说是一闪而过或者 java 命令直接打印三行错误退出。原因集中在 MANIFEST 缺失或写错。新手最常见的操作是用 Export JAR file 而不是 Runnable JAR file前者不会写 Main-Class。也有把主类写成 com.example.Main.class 的情况。解决步骤先确认导出入口是 Runnable JAR file再解压 MANIFEST 看 Main-Class 的值最后看主类签名必须满足 public、static、void main(String[])。按顺序排查大概率三分钟内定位。jar tf app.jar | grep -i Main.class jar xf app.jar META-INF/MANIFEST.MF java -cp app.jar com.example.Main第三条命令绕过 MANIFEST 直接指定入口如果这样能跑而 java -jar 跑不了那问题一定出在 MANIFEST 而不是代码。5.2 现象运行时 NoClassDefFoundError / ClassNotFoundExceptionEclipse 里明明有这是“包含第三方 Jar 包”主题下最高频的坑。原因分三类依赖没在 Order and Export 里勾选选了 Copy 模式但没把 lib 目录和主 jar 一起拷贝MANIFEST 里的 Class-Path 写成了本机绝对路径。解决先在 Order and Export 把依赖全部勾选再重新导出。很多人第一次失败后只改一个地方其他两个坑还留着结果在测试环境继续踩。建议按顺序检查 Dependency Export、产物目录结构、MANIFEST 的 Class-Path 三处三处全绿再分发。如果发现 Class-Path 是绝对路径参照 4.2 的手改或 Ant 脚本方案处理。5.3 现象jar 运行后中文乱码控制台成问号原因链一般是源码文件是 UTF-8编译时 Eclipse 用了系统默认编码 GBKclass 里的字符串已经以 GBK 字节存放运行时 JVM 又按 UTF-8 解码这些字节两边错位导致控制台输出乱码。在 Eclipse 里跑正常是因为 IDE 内部把编译参数和运行参数都对齐了脱离 IDE 后各种默认值不一致问题才暴露。解决统一三处编码。工程右键 Properties Resource 里把 Text file encoding 设为 UTF-8Window Preferences General Workspace 统一全局编码启动脚本里加-Dfile.encodingUTF-8。properties 资源文件建议用 native2ascii 转义或直接换 XML 格式。改完重新编译导出再用 javap 查看 class 里字符串常量的原始字节乱码问题会消失。5.4 现象导出后程序启动报找不到配置文件或者读到了旧配置原因代码用new File(config.properties)相对路径定位工作目录不是 jar 所在目录而是启动 java 命令时的当前目录。双击启动时Windows 的当前目录可能是 jar 所在目录也可能不是取决于启动方式用脚本启动时尤其不一致。这属于典型的运行时环境差异不是打包失败。解决外部配置方案在启动脚本里先 cd 到 jar 所在目录再执行 java -jar更规范的做法是把配置当资源打进 jar 用 getResourceAsStream 读取try (InputStream in Main.class.getResourceAsStream(/config.properties)) { Properties props new Properties(); props.load(in); }这里路径以 / 开头表示从 classpath 根目录查找不写 / 则相对于 Main 所在包目录。记住这层差异乱改代码找半天的问题就少一半。5.5 现象另一台机器报 UnsupportedClassVersionError或者方法签名对不上现象是相同的 jar在开发机上一切正常到生产机直接拒绝加载。原因多半是编译时用了高版本 JDK生产机只装了低版本 JRE。class 文件头部有版本号JVM 版本低于 class 版本时直接拒绝。解决把工程编译级别调到目标运行环境一致的版本。Eclipse 工程右键 Java Compiler 里的 Compiler compliance level 改成目标版本同时 Java Build Path 里的 JRE System Library 也指向对应版本。最好在自带 JRE 的目标机器上跑一遍作为验收环境。用 javap -verbose 查看 class 的 major version可以准确定位是不是版本问题major 52 对应 Java 855 对应 Java 1161 对应 Java 1765 对应 Java 21。分发现场的大多数环境差距逃不开这个坑。版本对齐之后再遇到诡异问题就值得看依赖内幕了。6. 把验证做成习惯三个命令确认一个 Jar 是否可以交付6.1 三个快速验证命令每次交付前我固定跑三件事。按顺序正好覆盖可执行 Jar 最容易出问题的三个环节类是否都在入口是否正确真实启动是否顺利。jar tf my-tool.jar | grep -E Main\.class|\.jar$ jar xf my-tool.jar META-INF/MANIFEST.MF java -jar my-tool.jar --version第一条检查主类和依赖的形态第二条拿到 MANIFEST 原始内容第三条用最终用户的启动方式跑通一遍。三条全绿我就把产物发给别人。如果其中任何一条不正常别修完一条就发把三条重跑一遍确保整体闭环。如果需要确认一个本地 jar 里的类结构javap 是比反编译更快的工具比如javap -classpath my-tool.jar com.example.Main能直接列出入口类的 public 方法省得解压翻目录。这个技巧在看第三方 jar 是否符合预期时同样好用。6.2 进阶用 Ant 脚本固化手工导出手工点向导的次数一多我就觉出不对劲同样的代码两次导出的产物可能不一致原因是工作空间状态变了。好在 Eclipse 导出向导留有 Save as ANT script可以帮你生成一个 build.xml。我一般会把它提交进版本库和同事一起用同一份构建脚本而不是各自点按钮。如果工程复杂度继续上升再迁移到 Maven Shade方向是进化的。但有一个原则不会变整个团队对“产物怎么来的”要有唯一答案。至于是 Eclipse 向导、Ant 脚本还是 Maven 插件只是通往答案的路径。我自己从 Eclipse 向导起步中间换过 Ant最后停在 Maven 上换来换去最大的收获不是工具而是“先验证再交付”的习惯。没有这个习惯换哪个工具都还是会在下一个环境里翻车。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?