
1. 项目概述与核心价值最近在做一个文档管理系统客户提了个挺实际的需求他们有一批历史文档有些是加了密的但时间一长密码记不清了或者交接时没传下来。他们希望系统能自动扫描这些文档识别出哪些是受密码保护的最好还能做个简单的分类。这个需求听起来简单但真做起来发现市面上现成的、能同时处理Word、Excel、PPT、PDF这四大主流格式的Java库还真不好找。总不能为了这个功能让用户装一堆不同的软件吧所以我们就决定自己动手基于Apache POI和PDFBox这些成熟的Java库封装一个轻量级的文档密码检测工具。这个工具的核心目标很明确给定一个文档文件路径快速、准确地判断它是否被密码保护。它不负责破解密码那是另一个完全不同的、且涉及法律和伦理的复杂领域只做“检测”这一件事。这在很多场景下非常有用比如企业文档资产盘点、数据迁移前的合规性检查、或者像我们项目里这样作为文档管理流程中的一个前置环节。实现起来技术点主要在于熟悉不同文档格式的底层结构以及对应的Java库如何打开并感知密码保护状态。下面我就把这次实战中积累的经验、踩过的坑以及最终的实现方案详细拆解一遍。2. 技术选型与依赖库解析要实现跨格式的文档密码检测选对工具库是第一步。我们不能用一个库打天下必须针对不同格式选用最专业、最稳定的解决方案。2.1 办公文档三剑客Apache POI对于微软系的Word.doc, .docx、Excel.xls, .xlsx、PowerPoint.ppt, .pptxApache POI是Java生态中毋庸置疑的事实标准。它提供了完整的API来读写这些OLE2和Office Open XML格式的文件。POI-HSLF用于处理.ppt格式PowerPoint 97-2003。POI-XSLF用于处理.pptx格式PowerPoint 2007。POI-HSSF用于处理.xls格式Excel 97-2003。POI-XSSF用于处理.xlsx格式Excel 2007。POI-HWPF用于处理.doc格式Word 97-2003。注意这个模块对复杂.doc文件的支持有限但用于检测密码足够了。POI-XWPF用于处理.docx格式Word 2007。在Maven项目中我们通常引入poi-ooxml和poi-scratchpad来覆盖所有新旧格式dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.3/version !-- 请使用最新稳定版 -- /dependency dependency groupIdorg.apache.poi/groupId artifactIdpoi-scratchpad/artifactId version5.2.3/version /dependency注意POI的不同模块版本号必须严格保持一致否则可能引发难以排查的兼容性问题。建议使用poi-ooxml-full来避免依赖冲突但会引入较多依赖。2.2 PDF文档专家Apache PDFBoxPDF格式自成一体POI无能为力。这里我们选择Apache PDFBox它是一个功能强大且活跃的开源Java库专门用于处理PDF文档。核心能力PDFBox可以创建、渲染、打印、拆分、合并PDF当然也包括我们需要的“尝试用空密码或特定密码加载文档以判断是否加密”的功能。Maven依赖dependency groupIdorg.apache.pdfbox/groupId artifactIdpdfbox/artifactId version2.0.27/version !-- 请使用最新稳定版 -- /dependency2.3 其他格式的考量除了上述四大件有时还会遇到其他格式比如WPS的.et、.dps或者苹果的.pages、.numbers。对于这些非主流或私有格式通用检测方法往往不奏效。一个务实的做法是明确需求边界与项目干系人确认是否必须支持这些格式。很多时候支持主流格式已覆盖90%以上的场景。寻找专用库或转换方案如果必须支持需要寻找特定的SDK如WPS可能提供或者考虑先将文档转换为标准格式如PDF再进行检测但这会引入转换失败、信息丢失等新风险。兜底策略对于无法识别的格式工具应明确返回“格式不支持”或“未知状态”而不是抛出令人困惑的异常。在我们的项目中优先保障了Word、Excel、PPT、PDF这四种格式的稳定检测。3. 核心检测原理与实现方案不同格式的文档其加密机制和POI/PDFBox提供的检测接口各不相同。理解其原理才能写出健壮的代码。3.1 Word文档.doc/.docx密码检测Word文档的密码保护分为两种“打开密码”和“修改密码”。我们通常说的密码检测指的是“打开密码”。.docx格式基于Open XML格式其加密信息存储在/encryption包中。POI的XWPFDocument在尝试用空密码或错误密码打开一个加密文档时会抛出InvalidFormatException或EncryptedDocumentException。.doc格式基于二进制的OLE2复合文档格式加密信息在文件头。HWPFDocument在打开加密文档时同样会抛出异常。实现思路尝试用空密码或一个肯定错误的密码去加载文档。如果加载成功说明无密码或密码已知但已知密码不符合我们场景如果捕获到特定的异常如EncryptedDocumentException则说明文档受密码保护。public boolean isWordEncrypted(File file) { String fileName file.getName().toLowerCase(); try { if (fileName.endsWith(.docx)) { try (FileInputStream fis new FileInputStream(file); XWPFDocument doc new XWPFDocument(fis)) { // 如果能成功创建XWPFDocument对象说明没有打开密码或者密码为空某些情况 return false; } } else if (fileName.endsWith(.doc)) { try (FileInputStream fis new FileInputStream(file); HWPFDocument doc new HWPFDocument(fis)) { return false; } } } catch (EncryptedDocumentException e) { // POI抛出的明确加密异常 return true; } catch (InvalidFormatException e) { // 对于.docxInvalidFormatException也可能是加密导致的 if (e.getMessage() ! null e.getMessage().contains(Encrypted)) { return true; } // 其他格式错误按非加密处理或抛出 return false; } catch (Exception e) { // 其他IOException等可能是文件损坏这里我们保守地返回false或记录错误 // 在实际项目中最好将异常信息日志记录下来 return false; } return false; // 非Word格式或未处理情况 }实操心得InvalidFormatException的异常信息可能因POI版本不同而略有差异直接判断字符串包含“Encrypted”可能不够健壮。更稳妥的做法是在捕获到InvalidFormatException时可以尝试读取文件魔数或进行更深入的二进制分析但考虑到我们只是检测用异常信息判断在大多数情况下是可行的折中方案。3.2 Excel文档.xls/.xlsx密码检测Excel的密码保护更复杂一些有“工作簿打开密码”、“工作表保护密码”、“工作簿结构保护密码”等。我们这里检测的也是“工作簿打开密码”。.xlsx格式与.docx类似加密信息在XML包中。XSSFWorkbook在打开加密文件时会抛出InvalidPasswordExceptionPOI 4.0或EncryptedDocumentException。.xls格式HSSFWorkbook的行为类似。实现思路与Word检测高度相似尝试用默认空密码打开。public boolean isExcelEncrypted(File file) { String fileName file.getName().toLowerCase(); try { if (fileName.endsWith(.xlsx)) { try (FileInputStream fis new FileInputStream(file); XSSFWorkbook workbook new XSSFWorkbook(fis)) { return false; } } else if (fileName.endsWith(.xls)) { try (FileInputStream fis new FileInputStream(file); HSSFWorkbook workbook new HSSFWorkbook(fis)) { return false; } } } catch (InvalidPasswordException e) { // 这是最明确的“需要密码”异常 return true; } catch (EncryptedDocumentException e) { return true; } catch (Exception e) { // 处理其他异常 return false; } return false; }注意事项这里检测的是“打开密码”。即使返回false文档内部的工作表仍可能受“保护密码”锁定防止编辑。这属于另一种粒度的保护通常需要不同的API如sheet.getProtect()来检测不在本文“打开密码检测”范畴内但你的系统如果需求更细可以后续扩展。3.3 PowerPoint文档.ppt/.pptx密码检测PPT的密码保护机制与Word/Excel类似。.pptx格式使用XMLSlideShow。.ppt格式使用HSLFSlideShow。实现思路完全遵循相同的“尝试加载-捕获异常”模式。public boolean isPptEncrypted(File file) { String fileName file.getName().toLowerCase(); try { if (fileName.endsWith(.pptx)) { try (FileInputStream fis new FileInputStream(file); XMLSlideShow ppt new XMLSlideShow(fis)) { return false; } } else if (fileName.endsWith(.ppt)) { try (FileInputStream fis new FileInputStream(file); HSLFSlideShow ppt new HSLFSlideShow(fis)) { return false; } } } catch (EncryptedDocumentException | InvalidPasswordException e) { return true; } catch (Exception e) { return false; } return false; }3.4 PDF文档密码检测PDF的加密检测逻辑与Office文档略有不同。PDFBox提供了更直接的API。核心类PDDocument。检测方法使用PDDocument.load()方法加载文件时可以传入一个DecryptionMaterial对象来尝试解密。如果文档加密了而你传入的密码不对或为空加载过程会抛出InvalidPasswordException。关键点即使密码错误PDDocument对象仍然会被创建但文档处于“不可用”的加密状态。我们需要通过document.isEncrypted()方法来判断并且要妥善处理异常和关闭文档。实现思路尝试用空密码加载文档。如果抛出InvalidPasswordException说明文档有密码且密码不是空。此时捕获异常并返回true。如果没有抛出异常成功加载则检查document.isEncrypted()。如果为true说明文档有密码但密码恰好是空这种情况很少见但存在。这通常也视为受密码保护。如果为false说明文档无密码。public boolean isPdfEncrypted(File file) { try (FileInputStream fis new FileInputStream(file)) { // 尝试用空密码加载 PDDocument document PDDocument.load(fis, (String)null); boolean encrypted document.isEncrypted(); document.close(); // 务必关闭 return encrypted; } catch (InvalidPasswordException e) { // 捕获到无效密码异常说明文档有密码且不是空密码 return true; } catch (Exception e) { // 文件损坏或其他IO异常 return false; } }重要提示PDFBox的PDDocument必须显式调用close()方法释放资源否则可能导致内存泄漏或文件锁未被释放。务必使用try-with-resources语句或在finally块中确保关闭。4. 工程化封装与性能优化将上述分散的检测方法封装成一个统一、易用、健壮的工具类是项目落地的关键。4.1 统一接口设计我们设计一个DocumentPasswordDetector类提供静态方法或实例方法。public class DocumentPasswordDetector { public enum EncryptionStatus { NOT_ENCRYPTED, // 未加密 ENCRYPTED, // 已加密 UNSUPPORTED_FORMAT, // 不支持的文件格式 ERROR // 读取文件时发生错误如文件损坏 } /** * 检测单个文件的密码保护状态 * param filePath 文件完整路径 * return EncryptionStatus 枚举表示状态 */ public static EncryptionStatus detect(String filePath) { File file new File(filePath); if (!file.exists() || !file.isFile()) { return EncryptionStatus.ERROR; } String fileName file.getName().toLowerCase(); try { if (fileName.endsWith(.docx) || fileName.endsWith(.doc)) { return isWordEncrypted(file) ? EncryptionStatus.ENCRYPTED : EncryptionStatus.NOT_ENCRYPTED; } else if (fileName.endsWith(.xlsx) || fileName.endsWith(.xls)) { return isExcelEncrypted(file) ? EncryptionStatus.ENCRYPTED : EncryptionStatus.NOT_ENCRYPTED; } else if (fileName.endsWith(.pptx) || fileName.endsWith(.ppt)) { return isPptEncrypted(file) ? EncryptionStatus.ENCRYPTED : EncryptionStatus.NOT_ENCRYPTED; } else if (fileName.endsWith(.pdf)) { return isPdfEncrypted(file) ? EncryptionStatus.ENCRYPTED : EncryptionStatus.NOT_ENCRYPTED; } else { return EncryptionStatus.UNSUPPORTED_FORMAT; } } catch (Exception e) { // 记录日志 e.printStackTrace(); 或使用log框架 return EncryptionStatus.ERROR; } } // 将之前实现的 isWordEncrypted, isExcelEncrypted 等方法改为private放在这里 // ... }4.2 批量处理与并发优化在实际场景中往往是扫描整个目录。我们需要考虑性能和资源管理。串行扫描简单但慢适用于文件数量少100的情况。public MapFile, EncryptionStatus batchDetectSerial(ListFile files) { MapFile, EncryptionStatus result new HashMap(); for (File file : files) { result.put(file, detect(file.getAbsolutePath())); } return result; }并行流优化利用Java 8的并行流可以轻松利用多核CPU加速I/O密集型小文件或CPU密集型大文件解析任务。public MapFile, EncryptionStatus batchDetectParallel(ListFile files) { return files.parallelStream() .collect(Collectors.toMap( file - file, file - detect(file.getAbsolutePath()) )); }踩坑记录并行流虽然方便但要注意线程安全。我们上面的detect方法是无状态的只依赖输入参数因此是线程安全的。但如果方法内部使用了共享的可变静态资源就必须加锁或使用ThreadLocal否则会出大问题。线程池控制对于超大批量如上万个文件或者需要更精细控制并发度、队列和拒绝策略的场景应使用自定义的ThreadPoolExecutor。public MapFile, EncryptionStatus batchDetectWithExecutor(ListFile files, int threadPoolSize) throws InterruptedException { ExecutorService executor Executors.newFixedThreadPool(threadPoolSize); MapFile, EncryptionStatus resultMap new ConcurrentHashMap(); ListFuture? futures new ArrayList(); for (File file : files) { futures.add(executor.submit(() - { EncryptionStatus status detect(file.getAbsolutePath()); resultMap.put(file, status); })); } // 等待所有任务完成 for (Future? future : futures) { future.get(); // 可以添加超时参数 } executor.shutdown(); return resultMap; }线程池大小经验值通常设置为CPU核心数 * 2。但文档检测是混合型任务I/O等待和CPU计算可以适当调大比如CPU核心数 * 4并通过实际压测找到最优值。切忌盲目设置过大以免线程上下文切换开销抵消并发收益。4.3 资源管理与内存泄漏防范这是最容易出问题的地方。POI和PDFBox在加载文档时可能会在内存中持有文件流或大量DOM对象。必须使用Try-With-Resources确保每一个打开的Workbook、Document、SlideShow、PDDocument以及底层的FileInputStream都在try-with-resources语句中或者在有异常时在finally块中明确关闭。PDFBox的特别提醒PDDocument.close()至关重要。即使检测到加密并抛出InvalidPasswordException如果之前成功创建了PDDocument对象也必须关闭它。大文件处理对于超大的Excel或PDF文件加载整个文档到内存可能引发OOM。POI和PDFBox都提供了部分读取如SAX模式解析Excel或设置内存阈值的方法。在纯检测场景下如果只是判断加密理论上不需要解析全部内容但库的加载接口通常还是会读入大部分数据。如果遇到特大文件频繁OOM需要考虑以下策略使用POI的org.apache.poi.xssf.eventusermodel.XSSFReader用于.xlsx进行基于事件的流式解析但代码复杂度激增。为检测服务单独分配一个JVM实例并设置较大的堆内存如-Xmx2g。作为妥协可以设定一个文件大小阈值如100MB超过此大小的文件直接标记为ERROR或跳过并记录日志在需求层面进行沟通。5. 常见问题排查与实战技巧在实际开发和测试过程中我遇到了不少典型问题这里总结一下排查思路和解决方案。5.1 问题速查表问题现象可能原因排查步骤与解决方案检测Word/.docx文件始终返回“未加密”1. 文件扩展名伪装实为.txt。2. 文件已损坏。3. 使用的是“修改密码”而非“打开密码”。1. 使用Files.probeContentType()或检查文件魔数验证真实类型。2. 尝试用Office软件手动打开确认文件完好。3. 明确需求确认要检测的是“打开密码”。检测PDF抛出InvalidPasswordException但isEncrypted()返回false逻辑顺序错误。在捕获异常后没有正确处理可能已部分加载的PDDocument对象。确保在catch块中如果PDDocument对象已创建必须调用close()。最好将加载和关闭逻辑封装好。批量处理时内存溢出OOM1. 同时加载了过多大文档到内存。2. 未正确关闭文档资源导致内存泄漏。3. JVM堆内存设置过小。1. 控制并发度使用有界队列的线程池。2.绝对确保每个文档处理完后相关资源InputStream, Document对象被关闭。3. 增加JVM堆内存。考虑使用-XX:HeapDumpOnOutOfMemoryError参数生成堆转储文件分析。某些.xls文件检测很慢文件可能包含大量复杂的公式或格式POI的HSSF解析器需要全部处理。考虑为检测操作设置超时时间。使用Future.get(timeout, TimeUnit)控制单个文件的检测任务。超时则标记为ERROR。返回UNSUPPORTED_FORMAT但文件确实是.docx文件扩展名大小写问题或包含多个点如报告.v1.docx。在提取扩展名时使用fileName.toLowerCase()统一处理。使用String的substring和lastIndexOf(.)来准确获取扩展名而不是简单的endsWith。在Linux服务器上运行检测部分Office文档报错服务器可能缺少字体库影响POI对某些文档样式的处理虽然不影响加密判断但可能触发其他异常。在服务器安装基础字体包如yum install fontconfig或apt-get install fontconfig。5.2 性能监控与日志一个健壮的生产级工具离不开监控和日志。日志记录使用SLF4J Logback记录关键事件。INFO级别记录开始批量检测、检测完成总结如共处理X个加密Y个。WARN级别记录不支持的文件格式、跳过的超大文件。ERROR级别记录文件读取异常、解析异常附带文件路径和异常信息方便事后排查。import org.slf4j.Logger; import org.slf4j.LoggerFactory; private static final Logger logger LoggerFactory.getLogger(DocumentPasswordDetector.class); // 在detect方法中 catch (Exception e) { logger.error(检测文件加密状态时发生异常文件: {}, filePath, e); return EncryptionStatus.ERROR; }性能统计可以简单记录每个文件的检测耗时用于发现性能瓶颈。long startTime System.currentTimeMillis(); EncryptionStatus status detect(filePath); long cost System.currentTimeMillis() - startTime; if (cost 5000) { // 超过5秒视为慢操作 logger.warn(文件检测耗时过长: {}ms, 文件: {}, cost, filePath); }5.3 扩展性思考这个基础检测工具可以很容易地扩展集成到Spring Boot应用将DocumentPasswordDetector声明为Component通过REST API或消息队列接收检测任务。添加结果持久化将检测结果文件路径、状态、检测时间存入数据库如MySQL或搜索引擎如Elasticsearch便于查询和生成报表。支持压缩包结合Apache Commons Compress库先解压ZIP/RAR文件再对其中的文档进行递归检测。更细粒度的保护检测如前所述可以扩展接口不仅检测“打开密码”还能检测Excel的“工作表保护”、Word的“限制编辑”等。分布式检测对于海量文件如对象存储OSS/S3中的数百万文档可以将文件列表和检测逻辑封装成任务提交到分布式计算框架如Spark进行处理。6. 完整示例代码与使用指南最后给出一个整合了上述所有要点的、可直接运行的示例类。import org.apache.poi.EncryptedDocumentException; import org.apache.poi.hslf.usermodel.HSLFSlideShow; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.hwpf.HWPFDocument; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.poifs.filesystem.NotOLE2FileException; import org.apache.poi.xslf.usermodel.XMLSlideShow; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.*; import java.util.concurrent.*; import java.util.stream.Collectors; public class DocumentPasswordDetector { private static final Logger log LoggerFactory.getLogger(DocumentPasswordDetector.class); public enum EncryptionStatus { NOT_ENCRYPTED, // 文档未加密 ENCRYPTED, // 文档有打开密码 UNSUPPORTED_FORMAT, // 不支持的文件格式 ERROR // 文件读取/解析错误 } /** * 检测单个文档的加密状态 */ public static EncryptionStatus detect(String filePath) { File file new File(filePath); if (!file.exists() || !file.isFile()) { log.warn(文件不存在或不是普通文件: {}, filePath); return EncryptionStatus.ERROR; } String fileName file.getName().toLowerCase(); String extension getFileExtension(fileName); try { switch (extension) { case docx: case doc: return isWordEncrypted(file) ? EncryptionStatus.ENCRYPTED : EncryptionStatus.NOT_ENCRYPTED; case xlsx: case xls: return isExcelEncrypted(file) ? EncryptionStatus.ENCRYPTED : EncryptionStatus.NOT_ENCRYPTED; case pptx: case ppt: return isPptEncrypted(file) ? EncryptionStatus.ENCRYPTED : EncryptionStatus.NOT_ENCRYPTED; case pdf: return isPdfEncrypted(file) ? EncryptionStatus.ENCRYPTED : EncryptionStatus.NOT_ENCRYPTED; default: log.info(不支持的文件格式: {}, 文件: {}, extension, filePath); return EncryptionStatus.UNSUPPORTED_FORMAT; } } catch (Exception e) { log.error(处理文件时发生异常: {}, filePath, e); return EncryptionStatus.ERROR; } } // --- 各格式具体检测方法 (private) --- private static boolean isWordEncrypted(File file) throws IOException { String name file.getName().toLowerCase(); try { if (name.endsWith(.docx)) { try (FileInputStream fis new FileInputStream(file); XWPFDocument ignored new XWPFDocument(fis)) { return false; } } else if (name.endsWith(.doc)) { try (FileInputStream fis new FileInputStream(file); HWPFDocument ignored new HWPFDocument(fis)) { return false; } } } catch (EncryptedDocumentException e) { return true; } catch (InvalidFormatException e) { // 对.docxInvalidFormatException可能意味着加密 if (e.getMessage() ! null e.getMessage().toLowerCase().contains(encrypted)) { return true; } throw e; // 重新抛出其他格式错误 } return false; } private static boolean isExcelEncrypted(File file) throws IOException { String name file.getName().toLowerCase(); try { if (name.endsWith(.xlsx)) { try (FileInputStream fis new FileInputStream(file); XSSFWorkbook ignored new XSSFWorkbook(fis)) { return false; } } else if (name.endsWith(.xls)) { try (FileInputStream fis new FileInputStream(file); HSSFWorkbook ignored new HSSFWorkbook(fis)) { return false; } } } catch (org.apache.poi.EncryptedDocumentException | org.apache.poi.InvalidPasswordException e) { return true; } return false; } private static boolean isPptEncrypted(File file) throws IOException { String name file.getName().toLowerCase(); try { if (name.endsWith(.pptx)) { try (FileInputStream fis new FileInputStream(file); XMLSlideShow ignored new XMLSlideShow(fis)) { return false; } } else if (name.endsWith(.ppt)) { try (FileInputStream fis new FileInputStream(file); HSLFSlideShow ignored new HSLFSlideShow(fis)) { return false; } } } catch (EncryptedDocumentException | org.apache.poi.InvalidPasswordException e) { return true; } return false; } private static boolean isPdfEncrypted(File file) throws IOException { try (FileInputStream fis new FileInputStream(file)) { // 尝试用空密码加载 PDDocument document PDDocument.load(fis, (String) null); boolean encrypted document.isEncrypted(); document.close(); return encrypted; } catch (InvalidPasswordException e) { // 捕获此异常说明有密码且密码错误 return true; } } // --- 批量处理方法 --- /** * 并行流批量检测 (适合中等数量文件) */ public static MapFile, EncryptionStatus batchDetectParallel(ListFile files) { return files.parallelStream() .collect(Collectors.toConcurrentMap( file - file, file - detect(file.getAbsolutePath()) )); } /** * 使用固定线程池批量检测 (适合大批量可控制并发度) */ public static MapFile, EncryptionStatus batchDetectWithExecutor(ListFile files, int nThreads) throws InterruptedException, ExecutionException { ExecutorService executor Executors.newFixedThreadPool(nThreads); ConcurrentHashMapFile, EncryptionStatus resultMap new ConcurrentHashMap(); ListFuture? futures new ArrayList(); for (File file : files) { futures.add(executor.submit(() - { EncryptionStatus status detect(file.getAbsolutePath()); resultMap.put(file, status); })); } // 等待所有任务完成 for (Future? future : futures) { try { future.get(30, TimeUnit.SECONDS); // 设置单个任务超时时间 } catch (TimeoutException e) { log.warn(文件检测任务超时: {}, e.getMessage()); future.cancel(true); } } executor.shutdown(); if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { executor.shutdownNow(); } return resultMap; } // --- 工具方法 --- private static String getFileExtension(String fileName) { int lastDotIndex fileName.lastIndexOf(.); if (lastDotIndex 0 lastDotIndex fileName.length() - 1) { return fileName.substring(lastDotIndex 1); } return ; } // --- 主方法示例用法 --- public static void main(String[] args) { // 示例检测单个文件 String testFile /path/to/your/document.docx; EncryptionStatus status detect(testFile); System.out.println(文件状态: status); // 示例批量检测一个目录 File dir new File(/path/to/your/documents); if (dir.isDirectory()) { ListFile fileList Arrays.asList(Objects.requireNonNull(dir.listFiles((d, name) - name.toLowerCase().matches(.*\\.(docx?|xlsx?|pptx?|pdf)$)))); System.out.println(开始批量检测文件数: fileList.size()); long start System.currentTimeMillis(); MapFile, EncryptionStatus results batchDetectParallel(fileList); long end System.currentTimeMillis(); // 统计结果 MapEncryptionStatus, Long summary results.values().stream() .collect(Collectors.groupingBy(s - s, Collectors.counting())); System.out.println(检测完成耗时: (end - start) ms); System.out.println(结果统计: summary); // 输出加密文件列表 results.entrySet().stream() .filter(entry - entry.getValue() EncryptionStatus.ENCRYPTED) .forEach(entry - System.out.println(加密文件: entry.getKey().getName())); } } }使用指南环境准备确保项目pom.xml中正确引入了poi-ooxml,poi-scratchpad,pdfbox以及日志框架如logback-classic的依赖。运行可以直接运行main方法测试或将该类集成到你的Spring Boot服务、命令行工具或定时任务中。配置根据实际文件数量和服务器资源调整批量检测方法中的线程池大小nThreads和超时时间。日志配置好logback.xml将日志输出到文件和控制台便于监控和排查问题。这个工具类已经过我们生产环境的检验稳定处理了数十万个各类文档。记住它的职责是“检测”而非“破解”清晰的功能边界是项目成功的前提。希望这份详细的拆解和代码能帮助你顺利实现类似的需求。