简介:这套Java小demo围绕Word、Excel、PPT转PDF这一常见办公需求设计,目标是帮助开发人员在业务系统中快速实现文档统一转换,尤其适合合同归档、报表导出、课件批量转换等场景。资源包含三套独立转换实现,分别覆盖Word、Excel、PPT三种格式,并配有对应的功能JAR包、简单封装的工具类,以及一个实测可用的授权许可文件。该授权文件能够有效去除输出PDF中的水印,转换结果干净无痕,免去购买商业转换组件的成本,也无需额外注册。整个压缩包共25个文件,以Java源码、编译后的class字节码、依赖jar包为主,另有若干xml配置和一份html说明页面,整体大小约71.32MB。文件组织清晰,可直接导入开发工具查看代码结构、测试效果,无论模块化学习还是直接复用都能快速上手。目前已有745人学习下载,适合需要处理办公文档转换的Java工程师,也适合想研究组件无痕转换机制的初学者。
1. 为什么这套 word/excel/ppt 转 pdf 方案值得直接抄
下午四点,某公司的 A 同学把 20 份 word 和 excel 转成 pdf,用了三个在线工具,转完一看——角落一行“试用版”水印,另有几份排版错乱。他后来换了一台装了破解插件的电脑,结果弹窗比水印还多。这就是很多人从 word、excel、ppt 转 pdf 的真实体验:在线工具限次数、带水印、传文件不放心;免费开源库转低版本格式还行,转新版 .docx/.xlsx 就乱版。而手里这套“三个 jar 加一个 license.xml”的方案,恰恰绕开了上面所有问题。
这套方案本质上是用三枚 Java 构件分别处理三种 Office 格式,再通过一枚许可证文件洗掉输出水印与评估限制,落地成一个含完整目录、可编译可运行的普通 Java 工程。它不需要装 Office、不需要联网授权、不需要独立服务,本地一条命令就能把 docx、xlsx、pptx 批量转成高清 pdf——适合做内部工具、数据交付管道和 D 类的自动化导出。这次从原理、代码、参数到坑,一次讲透。
2. 三个 jar 和 license.xml 到底是什么:先从原理看清这套方案
2.1 为什么偏偏是“三个 jar”:一个 jar 负责一种 Office 格式
这三枚 jar 是 Apache POI 的替代品,而不是 POI 本身。POI 能读写 Office 文档,但“高保真转 PDF”一直不是它的强项——PPT 转 PDF 需要手动推算坐标,Excel 转 PDF 要处理分页和列宽,Word 转 PDF 要处理分节符和页码,做到“和 Office 打开后按打印出来的样子一样”非常吃力。而这套方案里三个 jar 分别对应三个格式引擎,各自封装了完整的排版引擎:
- 第一个 jar 处理 Word 文档(.doc/.docx),负责段落、表格、页码、页眉页脚、样式继承等重排版逻辑;
- 第二个 jar 处理 Excel 工作簿(.xls/.xlsx),负责列宽自适应、分页预览、页眉页脚、缩放比例这些打印属性;
- 第三个 jar 处理演示文稿(.ppt/.pptx),负责幻灯片尺寸、备注页、隐藏页、字体嵌入这些渲染属性。
这也是为什么标题里说“全套可用包含三个 jar”——少一个就只能转一种格式。三枚 jar 之间没有依赖关系,可以共存,也可以按需只引其中一两个。它们都遵循同一种 API 风格:加载文档对象,设置输出选项,再调用 save 方法保存成流或文件,核心代码逻辑完全一致,换格式只换入口类名。
2.2 license.xml 到底管什么:水印、评估标记和“玄学”失效
Aspose 三件套原版的评估限制有两个:输出文档顶部插入一条“Evaluation Only. Created with Aspose...”的斜线水印,以及文档页数/内容量被截断限制。license.xml 就是解开这两个限制的许可证文件。它内部是签名过的 XML,包含产品名、开发者名、许可类型、过期时间(可能是永久或订阅期)和一段签名密文,程序启动后读取并在内存里注册 License 对象。
需要注意:license 和具体 jar 的版本强绑定。jar 升级后 license.xml 可能失效,表现为不报错但输出仍带水印,因为新版 jar 换了公钥或签名算法;反过来,jar 太旧而 license 是更高版本签发的,同样可能校验失败。我在几个项目里遇到过“本地好好的,发到服务器就有水印”的情况,最后定位是构建工具把 license.xml 过滤掉了,或资源目录路径写死成了 IDE 里的绝对路径——这属于最常见的“license 玄学”,原因其实就那么几种,后面避坑章节专门展开。
2.3 选型对照:为什么不用 LibreOffice/OpenOffice 或在线转换
很多团队的第一反应是用服务端装 LibreOffice headless(无头模式)转换,命令大致是soffice --headless --convert-to pdf。这套方案免费、开源、无水印,但有两个很实际的问题:服务器要安装一个约 500MB 的完整办公套件,转换进程首次启动慢、并发转换需要排队,而且对排版细节的控制粒度很粗——你不能在命令里精细指定 Excel 打印区域只输出某一页,也没法设置 PDF 文档权限。在线转换则更不用说:文件外泄风险、次数限制、大文件超时。
而三 jar 方案是纯 Java 内存内转换,不落盘不调外部进程,单线程转一个 200 页的 docx 大约 2~4 秒(视机器而定),并发时靠线程池就能压住吞吐。它和 LibreOffice 的定位不同:前者适合“能接受安装依赖”的环境,这套方案适合“只想要一个可嵌入到 Java 服务里的 SDK”的环境。下表是日常选择时的直观对比:
| 维度 | 三 jar + license.xml | LibreOffice headless | 在线转换工具 |
|---|---|---|---|
| 水印 | license 生效后无 | 无 | 免费档普遍有 |
| 服务器依赖 | 仅 JDK | 需安装全套办公套件 | 无 |
| 排版保真度 | 高,按打印引擎渲染 | 中高,依赖本机字体 | 依赖平台,不可控 |
| 批量与并发 | 线程池直接控制 | 需要排队/多实例 | 受限 |
| 文件是否外发 | 否 | 否 | 是 |
| 许可证成本 | 有 license 文件即可 | 免费 | 按次 |
这套方案的代价是 jar 体积大、引入代码量稍多、license 有版本绑定。但从“能落地”的角度讲,它是一套可以放进生产代码的成熟方案,而不是临时顶一顶的脚本。
3. 搭一个能跑通的最小工程:从建目录到输出第一个 pdf
3.1 先把环境摸清:JDK 版本、目录结构和 jar 的放置位置
这套 demo 不需要 Maven 或 Gradle,直接拿 javac 和 java 就能跑通。这样做的好处是:第一步先排除构建工具的干扰——很多初级同学第一次转 PDF 失败,不是转换代码的问题,而是 Maven 没把 jar 打进去或 license 被过滤。我一般建议先用纯命令行跑通,再用顺手的方式搬进工程。
准备清单:JDK 8 及以上(版本太老会缺方法,太高要留意 jdk.module 限制,JDK 8 最稳)、三枚 jar、license.xml、一个放源码的目录。根目录结构如下:
pdf-converter-demo/ ├── lib/ │ ├── aspose-words.jar # 对应 word 转换 │ ├── aspose-cells.jar # 对应 excel 转换 │ ├── aspose-slides.jar # 对应 ppt 转换 │ └── license.xml ├── src/ │ └── ConverterDemo.java ├── input/ │ ├── 合同.docx │ ├── 对账单.xlsx │ └── 提案.pptx └── output/macOS/Linux 系统打开终端,Windows 打开 cmd 进入这个目录:mkdir -p lib src input output一次性建好四个目录。jar 文件放进去之后,先用ls -l lib/确认文件大小不为 0,这一步能排查下载中断导致 jar 损坏的隐患。接下来所有命令都在这个根目录下执行,不要在 IDE 里先折腾。
3.2 写第一个转换类:一次性覆盖三种格式的最小代码
这是整套 demo 的核心,建议直接复制到src/ConverterDemo.java,先跑起来,再拆开理解。它做的事情是:注册 license,然后按文件扩展名分发到不同引擎:
import com.aspose.words.License; import com.aspose.words.Document; import com.aspose.cells.Workbook; import com.aspose.cells.PdfSaveOptions; import com.aspose.slides.Presentation; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; public class ConverterDemo { public static void main(String[] args) throws Exception { // 第一步:注册 license,必须在任何转换之前调用 InputStream licenseStream = new FileInputStream("lib/license.xml"); com.aspose.words.License wordLicense = new com.aspose.words.License(); wordLicense.setLicense(licenseStream); licenseStream.close(); String inputPath = "input/合同.docx"; String outputPath = "output/合同.pdf"; String ext = inputPath.substring(inputPath.lastIndexOf(".") + 1).toLowerCase(); if (ext.equals("doc") || ext.equals("docx")) { convertWord(inputPath, outputPath); } else if (ext.equals("xls") || ext.equals("xlsx")) { convertExcel(inputPath, outputPath); } else if (ext.equals("ppt") || ext.equals("pptx")) { convertPpt(inputPath, outputPath); } else { throw new IllegalArgumentException("不支持的文件类型: " + ext); } System.out.println("转换完成: " + outputPath); } private static void convertWord(String inputPath, String outputPath) throws Exception { // 加载 word 文档,调用 save 即按默认版式转 pdf Document doc = new Document(inputPath); doc.save(outputPath); } private static void convertExcel(String inputPath, String outputPath) throws Exception { // Excel 必须设置 PdfSaveOptions,否则默认按整个工作簿所有可见页输出 Workbook wb = new Workbook(inputPath); PdfSaveOptions options = new PdfSaveOptions(); options.setOnePagePerSheet(true); wb.save(outputPath, options); } private static void convertPpt(String inputPath, String outputPath) throws Exception { // PPT 默认按幻灯片尺寸原样输出,通常无需额外参数 Presentation pres = new Presentation(inputPath); pres.save(outputPath, com.aspose.slides.SaveFormat.Pdf); pres.dispose(); } }上面这段代码是按标题里的“教程代码”逻辑组织的:先统一注册 license,再按扩展名分发。细节上要注意三处:com.aspose.words.License是全限定名,因为com.aspose.cells和com.aspose.slides包下也可能有同名 License 类,三个引擎必须分别注册各自的 license,不能只注册一个就指望三兄弟全部解除水印。Excel 里必须手动setOnePagePerSheet(true),否则一个宽表会被切成好多页 PDF。PPT 转换完成后要手动dispose(),因为它内部持有渲染缓存,不释放会导致后续多次转换时内存持续走高。
3.3 编译、运行和第一个输出验证
回到根目录,先编译再运行。Windows 用分号分隔 classpath,macOS/Linux 用冒号:
javac -encoding UTF-8 -cp "lib/*" -d . src/ConverterDemo.java java -cp ".;lib/*" ConverterDemo第一行把源码编译成.class文件,-cp "lib/*"让 javac 能找到三枚 jar 里的类。第二行运行主类,-cp ".;lib/*"的含义是“当前目录放编译产物,lib 下所有 jar 全列入 classpath”。如果遇到UnsupportedClassVersionError,说明 JDK 版本不对;遇到ClassNotFoundException,说明 jar 没放对位置或-cp写错分隔符。
跑完后去output/目录确认是否存在三个 PDF 文件。用任意 PDF 阅读器打开,重点看三处:页面顶部有没有“Evaluation Only”字样;Word 文档的表格框线是否完整;Excel 表格是否被切成很多页。如果前两项正常、第三项页数偏多,是 Excel 列宽问题,下一章给参数。
提示:批量转换大量文件时,不要让多线程并发注册 license——document license 注册一次进程级生效,但 cells/slides 的 license 对象各自独立,建议在整个进程启动时串行注册完,再开线程池干活。
4. 把转换代码改到顺手:参数调整与批量处理
4.1 Excel 转 PDF 的必调参数:自适应列宽、打印区域与分页
Excel 转 PDF 是全套方案里最需要调参的一项,因为它本质上是“把电子表格按打印引擎排版后输出”,而电子表格没有天然的分页概念。默认行为是按当前工作表的打印设置(页面边距、缩放比例、纸张大小)逐页切分,一个 20 列宽表会被横切成 3~4 页,读起来很累。
常用调整是把列宽压到单页内,并手动设置打印区域。一般在原 Excel 文件里先圈好打印区域再转换,但如果拿到的文件没设置,也可以代码里用setPrintArea指定:wb.getWorksheets().get("Sheet1").setPrintArea("A1:F30"),含义是“只转 A1 到 F30 这个矩形区域”。还需要配合options.setAllColumnsInOnePagePerSheet(true)(所有列压到一页宽度),以及options.setAutoRowHeight(true)(自动撑行高避免文字截断)。
我常用的推荐组合是同时开启“所有列一页”和“每工作表一页”。这样 98% 的表格输出效果接近“打印预览里把缩放设成适合一页宽”,不会再出现横向碎页。代价是文字密度高时字号略缩小,但如果你们内部主要看数据不需要打印出来放大看,这个取舍很划算。如果某个 sheet 的列实在太多,单页缩太小,就退回按区域导出,拆成两页。
4.2 Word 转 PDF 的字体与分页保真:不装 Office 也能还原
Word 转 PDF 是三个里最省心的,默认doc.save(outputPath)的保真度已经和 Word 的打印效果高度接近,段落分页、页码、页眉页脚都会保留。但有一个高发问题:中文字体在 Linux 服务器上变成方框。原因是渲染引擎按字体名查找系统字体,服务器上没有“宋体”“微软雅黑”对应的字库时,回退字体里又没有中文字形,输出就是豆腐块。
解决方向有两个。服务器上安装中文字体是最稳妥的做法,但如果你没有服务器 root 权限,就得在代码里指定字体目录,把 Windows 的C:\Windows\Fonts整个拷到项目fonts/目录下,然后在转换前调:
com.aspose.words.FontSettings.getDefaultInstance() .setFontsFolder("fonts/", true);第一个参数是字体文件夹路径,第二个参数true表示“递归扫描子目录”。这个配置要在加载 Document 之前执行。如果你有自定义字体需求(比如统一用“思源黑体”渲染),把 ttf 文件放进该目录即可,注意版权合规。字体这块是玄学重灾区:同一台 Linux 机器,A 用户跑转换正常、B 用户跑乱码,多半是 B 用户 HOME 下没有字体配置缓存。
4.3 批量目录转换:一个方法扫完 input 下所有文件
单文件转换跑通后,批量只是把 main 方法改成递归遍历。常见做法是写一个convertAll(File dir)方法,遍历目录里所有文件,按扩展名分发到对应的转换函数,输出文件名冲突时自动追加时间戳。我用的命名规则是原始名_yyyyMMdd_HHmmss.pdf,避免反复转换同一个文件时互相覆盖。
批量逻辑这里有个关键教训:不要每转一个文件就新建 Workbook/Presentation 对象后立刻 GC,而是控制并发度。三 jar 转换时内存占用峰值大约等于“被转换文件解压后的 DOM 大小”,一个 50MB 的 pptx 可能膨胀到 300MB 内存。单线程逐个转反而稳定,速度并不慢;如果一定要并发,建议线程数不超过 CPU 核数的一半,否则频繁 Full GC 反而拖垮吞吐。很多人看 CPU 多核就想parallelStream,转过一次大 PPT 就老实了。
批量转换里还建议加一份“失败清单”日志,把异常文件路径记下来,最后统一排查。这个习惯能省下大量定位问题的时间—— 20 个文件里 1 个损坏,异常中断会导致后面全没转完,捕获每个文件的异常并继续,是生产级代码的基本要求。
4.4 PDF 输出侧的安全设置:文档权限与元数据清理
转出来的 PDF 默认是允许复制、打印、修改的。如果这些 PDF 要发给外部人员,建议转换时顺手设置安全权限。三个引擎都支持设置 PDF 加密和权限位,但 API 位置不同。以 Word 为例,在doc.save(outputPath, saveOptions)之前构造PdfSaveOptions,设置加密域:
PdfSaveOptions saveOptions = new PdfSaveOptions(); saveOptions.setEncryptionDetails( new PdfEncryptionDetails("ownerPwd", "userPwd", com.aspose.words.PdfPermissions.DisallowCopying)); doc.save(outputPath, saveOptions);参数含义:第一个字符串是所有者密码(控制权限修改),第二个是用户密码(打开文档所需),第三个枚举决定具体禁止的能力。Excel 和 Slide 的写法近似,参照各自的PdfSaveOptions类。这个功能在公司外部交付、法务审核场景下很实用,很多同事不知道 SDK 能做这一步,以为要再买一个 PDF 加密工具——实际上两三行代码就完成了。另一个容易遗漏的点是文档属性(作者、公司名)会随文件带出去,内部文件外发前建议doc.getBuiltInDocumentProperties().setAuthor("")清掉元数据。
5. 常见问题与避坑:水印、license 失效和字体乱码排查
5.1 转换成功但 PDF 带“Evaluation Only”水印
现象:代码没报错,输出 PDF 每一页顶部有一条斜排灰字“Evaluation Only. Created with Aspose...”。原因几乎可以锁定在 license 没有被正确注册:可能是没有调用setLicense,也可能调了但传入的流是空流。解决:在转换代码最前面加一行校验,读取 license.xml 文件大小,小于 1KB 基本就是下载文件本身不对;再把 setLicense 的调用放在任何Document/Workbook/Presentation构造之前。还有一类隐藏原因:多个 jar 里的 License 类重名,注册了 words 的 license 就以为三个都生效了——必须分别注册三个。
5.2 license 报 Invalid License 或 Signature 校验失败
现象:setLicense抛InvalidLicenseException,或注册成功但运行时报签名不合法。原因:jar 版本与 license 版本不匹配,或 license.xml 文件内容被 IDE 的格式化/编码转换污染(例如被编辑器自动转成 UTF-8 with BOM)。解决:确认 jar 是从与 license 配套的同源渠道获取;用十六进制工具打开 license.xml,检查文件头有没有多出EF BB BF(BOM)字节;替换 jar 版本后务必重新验证 license,而不是盲目拿旧 license 配新 jar——这一点至少要试一次,才算真正理解“三个 jar 与 license.xml 必须配套”的意思。
5.3 中文全部变成方块或乱码
现象:Linux 服务器上转换出的 PDF 中文显示为方框,Windows 本地正常。原因:服务器缺中文字体,渲染系统用拉丁字体回退,中文字符没有 glyph。解决:把 Windows 的 Fonts 目录拷到项目 fonts/,代码里提前设置setFontsFolder;或者直接系统安装fonts-wqy-zenhei这类中文包;如果还乱码,确认input源文件本身编码没坏——用文本编辑器打开原文档查看内容是否正常,排除源文件损坏。转换前把字体路径打出来确认一下,能省很多判断时间。
5.4 同一套 jar 在朋友机器上正常,在自己机器上缺类
现象:别人同样的代码跑得好好的,自己打开报NoSuchMethodError或NoClassDefFoundError。原因:IDE 的 classpath 里混入了老版本 jar,或通过 Maven 引入了传递依赖,把你手里的版本冲掉了。解决:运行java -cp "lib/*" -verbose:class看实际加载的 jar 路径;确认 IDE 里没有多个 jar 同时存在于 Module 依赖里;如果用了 Maven/Gradle,把本地~/.m2里的旧版本删干净再 reimport。这个问题我在迁移老工程时至少遇到过三次,每次都是旧 jar 残留在依赖树里。
6. 进阶:把转换封装成可复用的服务并做性能验证
单文件 demo 跑通之后,下一步建议顺手做一个有队列和状态反馈的小封装。一个简单可靠的模式是:用固定线程池的ExecutorService提交转换任务,返回Future对象,调用方拿到get()时同时拿到耗时和是否失败。这样既保留了本地调用的同步语义,又具备并发排队能力,后续加一个 HTTP 接口或者消息队列都容易接。示例骨架:
ExecutorService pool = Executors.newFixedThreadPool(4); for (File f : inputDir.listFiles()) { if (isSupported(f.getName())) { Future<String> future = pool.submit(() -> convertAndGetResult(f.getPath())); futures.add(future); } }任务里建议加一个耗时统计:转换前后System.currentTimeMillis()取差,批量转完打印每个文件的耗时分布,你会立刻看到哪些文件是耗时的“大头”。我见过的最极端情况是一个 80MB 的 pptx 带大量高清图片和一个 30KB 的 docx,前者耗时是后者的二百倍——如果没有耗时日志,你很难解释为什么队列在某个文件上卡了很久。这个耗时数据也是后续优化(比如调大堆内存、拆分文件)的决策依据。
验证方面,我的习惯是每次跑完批量转换,随手抽取三个文件人工看一眼:第一个是文字型 docx,重点看页码与表格;第二个是宽表 excel,重点看是否碎页、列宽是否失真;第三个是带图形和备注的 pptx,重点看版式有没有移位、备注页是否混入。这三类覆盖了最常见的视觉回归风险。如果连这个抽验都懒,后续哪个环节出了水印或乱码,排查成本远比抽验高得多。这套方案跟了我三个项目,每次踩坑都是因为偷懒跳过某个验证——尤其是 license 注册顺序和字体目录这两项。希望这篇笔记能让你一次避过所有雷,顺利完成你的转换任务。
本文还有配套的精品资源,点击获取