一、概述
技术点选择与解析版:合并Pdf、excel、图片、word为单个Pdf文件的工具类(技术点的选择与深度解析)2.1 流的重置机制:选择 ge - 掘金
基于三个核心Java工具类,实现多格式文件合并为PDF。这三个类分别是:
- FileTypeDetector - 文件类型检测工具类
- MergeFilesToPDFUtil - 文件合并为PDF工具类
- Word2PdfUtils - Word转PDF工具类
1.1 类功能简介
FileTypeDetector(文件类型检测工具)
FileTypeDetector 是一个基于文件头字节(Magic Number)的文件类型识别工具。它通过读取文件的前几个字节来判断文件类型,而不是依赖文件扩展名,这种方式更加可靠和安全。
核心方法:
java
public static String detectFileType(FileInputStream inputStream) throws IOException
使用示例:
java
FileInputStream fis = new FileInputStream("document.docx");
String fileType = FileTypeDetector.detectFileType(fis);
// 返回: "docx"
MergeFilesToPDFUtil(文件合并工具)
MergeFilesToPDFUtil 提供了将多种格式文件(PDF、Word、Excel、图片等)合并为单个PDF文档的功能。支持两种使用方式:
- 基于文件路径列表合并
- 基于文件输入流列表合并
核心方法:
java
// 基于文件路径
public static ByteArrayOutputStream generatePdf(List<String> fileNames, String outputFile)
// 基于文件流
public static ByteArrayOutputStream generatePdfFromFileStream(
List<FileInputStream> fileInputStreams, String outputFile)
使用示例:
java
List<String> files = Arrays.asList("doc1.docx", "excel1.xlsx", "image1.jpg");
ByteArrayOutputStream pdfStream = MergeFilesToPDFUtil.generatePdf(files, null);
// 返回合并后的PDF字节流
Word2PdfUtils(Word转PDF工具)
Word2PdfUtils 专门负责将Word文档(.doc、.docx)转换为PDF格式,并具备自动删除空白页的功能。
核心方法:
java
public static void wordToPdfStream(InputStream inputStream, OutputStream outputStream)
使用示例:
java
FileInputStream wordFile = new FileInputStream("document.docx");
FileOutputStream pdfFile = new FileOutputStream("output.pdf");
Word2PdfUtils.wordToPdfStream(wordFile, pdfFile);
特殊说明:实现word转pdf和excel转pdf使用了商用aspose-words、aspose-cells,必须下载相关jar包,实现无水印转pdf 需要修改jar包,需要的话参考文章:blog.csdn.net/qq_40965901...
二、类源码
word2PdfUtils
java
import com.aspose.words.Document;
import com.aspose.words.License;
import com.aspose.words.SaveFormat;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import lombok.SneakyThrows;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Word转pdf工具类
* @Version 1.0
*/
public class Word2PdfUtils {
/**
* 加载license 用于破解 不生成水印
*/
@SneakyThrows
private static void getLicense() {
try (InputStream is = Word2PdfUtils.class.getClassLoader().getResourceAsStream("License.xml")) {
License license = new License();
license.setLicense(is);
}
}
/**
* word转pdf
*
* @param inputStream word文件保存的路径
* @param response 转换后pdf文件保存的路径
*/
@SneakyThrows
public static void wordToPdf(InputStream inputStream, HttpServletResponse response) {
getLicense();
// 先转换为PDF到内存中
ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream();
Document doc = new Document(inputStream);
doc.updatePageLayout();
doc.save(pdfOutputStream, SaveFormat.PDF);
// 删除PDF中的空白页
byte[] pdfBytes = removeEmptyPagesFromPdf(pdfOutputStream.toByteArray());
// 输出到response
try (OutputStream outputStream = response.getOutputStream()) {
outputStream.write(pdfBytes);
outputStream.flush();
}
}
@SneakyThrows
public static void wordToPdfStream(InputStream inputStream, OutputStream outputStream) {
getLicense();
// 先转换为PDF到内存中
ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream();
Document doc = new Document(inputStream);
doc.updatePageLayout();
doc.save(pdfOutputStream, SaveFormat.PDF);
// 删除PDF中的空白页
byte[] pdfBytes = removeEmptyPagesFromPdf(pdfOutputStream.toByteArray());
// 输出到目标流
outputStream.write(pdfBytes);
outputStream.flush();
}
/**
* 从PDF中删除空白页
*
* @param pdfBytes PDF文件的字节数组
* @return 删除空白页后的PDF字节数组
*/
@SneakyThrows
private static byte[] removeEmptyPagesFromPdf(byte[] pdfBytes) {
ByteArrayInputStream inputStream = new ByteArrayInputStream(pdfBytes);
PdfReader reader = new PdfReader(inputStream);
// 检查哪些页面是空白页
java.util.List<Integer> pagesToKeep = new java.util.ArrayList<>();
for (int i = 1; i <= reader.getNumberOfPages(); i++) {
if (!isPageEmpty(reader, i)) {
pagesToKeep.add(i);
}
}
// 如果没有页面需要保留,返回空PDF
if (pagesToKeep.isEmpty()) {
reader.close();
return new byte[0];
}
// 创建新的PDF,只包含非空白页
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 创建新的PdfReader,只包含要保留的页面
PdfReader newReader = new PdfReader(pdfBytes);
newReader.selectPages(pagesToKeep);
PdfStamper stamper = new PdfStamper(newReader, outputStream);
stamper.close();
newReader.close();
reader.close();
return outputStream.toByteArray();
}
/**
* 判断PDF页面是否为空
*
* @param reader PDF读取器
* @param pageNumber 页面编号
* @return 是否为空页面
*/
@SneakyThrows
private static boolean isPageEmpty(PdfReader reader, int pageNumber) {
// 获取页面内容
String pageContent = com.itextpdf.text.pdf.parser.PdfTextExtractor.getTextFromPage(reader, pageNumber);
// 如果页面内容为空或只包含空白字符,则认为是空白页
return pageContent == null || pageContent.trim().isEmpty();
}
}
MergeFilesToPDFUtil
java
import com.aspose.cells.License;
import com.aspose.cells.SaveFormat;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import jdk.internal.util.xml.impl.Input;
import lombok.SneakyThrows;
import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.io.IOUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName MergeFilesToPDFUtil
* @Description
**/
public class MergeFilesToPDFUtil {
private static final String url = "D:\\testMeta\\tempFile\\tempp";
public static ByteArrayOutputStream generatePdf(List<String> fileNames) throws IOException{
return generatePdf(fileNames,null);
}
/**
*
* @param fileNames 需要转换的文件列表(需带文件路径)
* @param outputFile 输出文件路径 如:/test/output.pdf
*/
public static ByteArrayOutputStream generatePdf(List<String> fileNames, String outputFile) throws IOException{
//合成过程的temp pdfDocument
List<PDDocument> pdfdocuments = new ArrayList<>();
String num = System.currentTimeMillis()+"";
String realFileName = num + ".pdf";
String tempPath = url+"/"+num;
PDDocument document = null;
//生成临时文件的路径
//在输出pdf存在的时候应该删除,防止旧的数据污染(待定)
try {
if(outputFile!=null){
File file = new File(outputFile+"/"+realFileName);
if (file.exists()) {
// 删除已存在的文件
file.delete();
} else {
// 如果文件不存在,确保父目录存在
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
}
// 创建新文件
file.createNewFile();
}
// 创建一个空的pdf文档
document = new PDDocument();
// 依次读取要合并的各个文件,并将其内容添加到pdf文档中
for (String fileName : fileNames) {
String fileExt = fileName.substring(fileName.lastIndexOf(".") + 1);
FileInputStream fis = new FileInputStream(fileName);
//读取jpg、jpeg、png格式的文件
if (fileExt.equalsIgnoreCase("jpg") || fileExt.equalsIgnoreCase("jpeg")
|| fileExt.equalsIgnoreCase("png")) {
imageToPdf(fis, document);
fis.close();
} else if (fileExt.equalsIgnoreCase("pdf")) {
// PDDocument pdf = PDDocument.load(new File(fileName));
PDDocument pdf = PDDocument.load(fis);
for (PDPage page : pdf.getPages()) {
document.addPage(page);
}
pdfdocuments.add(pdf);
fis.close();
} else if (fileExt.equalsIgnoreCase("docx") || fileExt.equalsIgnoreCase("doc")) {
wordToPdf(fis,tempPath, document, pdfdocuments);
fis.close();
} else if (fileExt.equalsIgnoreCase("xlsx") || fileExt.equalsIgnoreCase("xls")
|| fileExt.equalsIgnoreCase("xml")) {
excelToPdf(fis,tempPath, document, pdfdocuments);
fis.close();
}
}
// 将pdf文档保存到本地
if(outputFile!=null){
document.save(outputFile+"/"+realFileName);
}
ByteArrayOutputStream pdfMemoryStream = new ByteArrayOutputStream();
document.save(pdfMemoryStream);
return pdfMemoryStream;
} catch (FileNotFoundException e) {
throw new RuntimeException("文件不存在!",e);
} catch (IOException e) {
throw new RuntimeException("系统异常,请联系管理员!",e);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if(document!=null){
document.close();
}
closeIo( pdfdocuments);
FileUtils.forceDelete(new File(tempPath));
}
}
}
/**
* 合并多个文件为pdf文件
* @param fileInputStreams
* @return PDF字节流
* @throws IOException
*/
public static ByteArrayOutputStream generatePdfFromFileStream(List<FileInputStream> fileInputStreams) throws IOException{
return generatePdfFromFileStream(fileInputStreams,null);
}
/**
* 合并多个文件为pdf文件
* @param fileInputStreams pdf文件输入流列表
* @param outputFile 合并后的pdf文件夹路径
* @return ByteArrayOutputStream PDf字节流
* @throws IOException
*/
public static ByteArrayOutputStream generatePdfFromFileStream(List<FileInputStream> fileInputStreams, String outputFile) throws IOException{
if(fileInputStreams == null ||fileInputStreams.isEmpty()){
throw new RuntimeException("文件流列表不能为空!");
}
PDDocument document = null;
List<PDDocument> pdfdocuments = new ArrayList<>();
String num = System.currentTimeMillis()+ "";
String realFileName = num + ".pdf";
String tempPath = url+"/"+num;
//String url = "D:\\testMeta\\tempFile";
//在输出pdf存在的时候应该删除,防止旧的数据污染(待定)
try {
if(outputFile != null){
File file = new File(outputFile+"/"+realFileName);
if (file.exists()) {
// 删除已存在的文件
file.delete();
} else {
// 如果文件不存在,确保父目录存在
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
}
// 创建新文件
file.createNewFile();
}
// 创建一个空的pdf文档
document = new PDDocument();
// 依次读取要合并的各个文件,并将其内容添加到pdf文档中
for (FileInputStream fis : fileInputStreams) {
//通过文件流获取文件类型
String fileExt = FileTypeDetector.detectFileType(fis);
if(fileExt == null){
fis.close();
continue;
}
if (fileExt.equalsIgnoreCase("jpg") || fileExt.equalsIgnoreCase("jpeg")
|| fileExt.equalsIgnoreCase("png")) {
imageToPdf(fis, document);
fis.close();
} else if (fileExt.equalsIgnoreCase("pdf")) {
pdfToPdf(fis,document, pdfdocuments);
fis.close();
} else if (fileExt.equalsIgnoreCase("docx") || fileExt.equalsIgnoreCase("doc")) {
wordToPdf(fis,tempPath, document, pdfdocuments);
fis.close();
} else if (fileExt.equalsIgnoreCase("xlsx") || fileExt.equalsIgnoreCase("xls")
|| fileExt.equalsIgnoreCase("xml")) {
excelToPdf(fis,tempPath, document, pdfdocuments);
fis.close();
}
}
// 将pdf文档保存到传入位置
if(outputFile != null){
document.save(outputFile+"/"+realFileName);
}
ByteArrayOutputStream pdfMemoryStream = new ByteArrayOutputStream();
document.save(pdfMemoryStream);
return pdfMemoryStream;
} catch (FileNotFoundException e) {
throw new RuntimeException("文件不存在!",e);
} catch (IOException e) {
throw new RuntimeException("系统异常,请联系管理员!",e);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if(document != null){
document.close();
}
//关闭pdf合并过程中产生的临时pdDocuments
closeIo(pdfdocuments);
//删除临时目录
FileUtils.forceDelete(new File(tempPath));
}
}
/**
* 关闭所有的io流
* @param pdfdocuments
*/
public static void closeIo(List<PDDocument> pdfdocuments){
try {
//延迟处理等待文件关闭
for (PDDocument pdfdocument : pdfdocuments) {
pdfdocument.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 通过pdf文件流将pdf加入输出pdf文档
* @param fis 文件流
* @param document 输出pdf文档
* @param pdfdocuments 临时pdf文档list
*/
public static void pdfToPdf(FileInputStream fis ,PDDocument document, List<PDDocument> pdfdocuments ) throws IOException {
PDDocument pdf = PDDocument.load(fis);
for (PDPage page : pdf.getPages()) {
document.addPage(page);
}
pdfdocuments.add(pdf);
}
/**
* 根据图片文件地址转pdf
* @param fileName
* @param document
*/
private static void imageToPdf(String fileName, PDDocument document){
try {
FileInputStream fis = new FileInputStream(fileName);
imageToPdf(fis, document);
}catch (IOException e){
throw new RuntimeException(e);
}
}
/**
* 图片文件流转pdf
* @param is 图片文件流
* @param document 输出pdf文档
*/
private static void imageToPdf(FileInputStream is, PDDocument document ) throws IOException {
// 1. 将 FileInputStream 转换为字节数组(PDImageXObject 支持从字节数组创建图片)
byte[] imageBytes = streamToByteArray(is);
// 2. 从字节数组创建 PDImageXObject(第二个参数为图片名称,可自定义)
PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "image");
// 3. 创建与图片尺寸一致的 PDF 页面(避免图片拉伸/压缩)
PDPage page = new PDPage(new PDRectangle(image.getWidth(), image.getHeight()));
document.addPage(page);
// 4. 将图片绘制到 PDF 页面(坐标 (0,0) 表示左上角对齐)
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
contentStream.drawImage(image, 0, 0); // 图片尺寸与页面一致,直接填充
}
}
/**
* doc、docx文件转pdf
* @param is doc、docx文件输入流
* @param tempPath 临时文件保存路径
* @param document 输出pdf的文档
* @param pdDocuments 待关闭流list
*/
private static void wordToPdf(FileInputStream is,String tempPath, PDDocument document, List<PDDocument> pdDocuments) throws IOException {
String pdfFile = System.currentTimeMillis() + ".pdf";
String tempPdfUrl = tempPath + "/" + pdfFile;
// 确保临时目录存在
File tempDir = new File(tempPath);
if (!tempDir.exists()) {
tempDir.mkdirs();
}
FileOutputStream os = new FileOutputStream(tempPdfUrl);
Word2PdfUtils.wordToPdfStream(is,os);
os.close();
//这里是将转完的pdf添加到新的pdf页面之后
PDDocument load = PDDocument.load(new File(tempPdfUrl));
for (PDPage page : load.getPages()) {
document.addPage(page);
}
//将生成的pdf临时文件删除掉
//FileUtils.forceDelete(new File(tempPdfUrl));
pdDocuments.add(load);
}
/**
* xlsx、xls、xml文件转pdf
* @param is 文件流
* @param tempPath 临时文件保存路径
* @param document 输出pdf的文档
* @param pdDocuments 待关闭流list
*/
private static void excelToPdf(FileInputStream is,String tempPath, PDDocument document, List<PDDocument> pdDocuments){
String pdfFile = System.currentTimeMillis() + ".pdf";
String tempPdfUrl = tempPath + "/" + pdfFile;
// 确保临时目录存在
File tempDir = new File(tempPath);
if (!tempDir.exists()) {
tempDir.mkdirs();
}
//转换Excel文件为pdfAspose
try {
convertExcelToPdfByAspose(is, tempPdfUrl);
PDDocument load = PDDocument.load(new File(tempPdfUrl));
for (PDPage page : load.getPages()) {
document.addPage(page);
}
//将生成的pdf临时文件删除掉
//FileUtils.forceDelete(new File(tempPdfUrl));
pdDocuments.add(load);
} catch (Exception e) {
throw new RuntimeException(e);
}
//这里是将转完的pdf添加到新的pdf页面之后
}
/**
* FileInputStream 转换为字节数组
* @param stream 输入流
* @return 字节数组
* @throws IOException 流读取异常
*/
private static byte[] streamToByteArray(FileInputStream stream) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024]; // 缓冲区大小,可根据图片大小调整(如 4096)
int len;
// 循环读取流数据到缓冲区,再写入字节输出流
while ((len = stream.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
bos.flush(); // 确保所有数据写入
return bos.toByteArray();
}
private static void convertExcelToPdfByAspose(FileInputStream is, String pdfFilePath) throws Exception {
FileOutputStream os = null;
InputStream resourceAsStream = null;
com.aspose.cells.Workbook workbook = null;
try{
resourceAsStream = MergeFilesToPDFUtil.class.getClassLoader().getResourceAsStream("License.xml");
com.aspose.cells.License license = new com.aspose.cells.License();
license.setLicense(resourceAsStream);
os = new FileOutputStream(pdfFilePath);
workbook = new com.aspose.cells.Workbook(is);
workbook.save(os, SaveFormat.PDF);//设置转换文件类型并转换
}catch (Exception e){
throw new RuntimeException("Excel转换为PDF失败: " + e.getMessage(), e);
}finally {
if(os != null){
os.close();
}
if(resourceAsStream != null){
resourceAsStream.close();
}
if(workbook != null){
//特有的关闭非托管资源
workbook.dispose();
}
}
}
FileTypeDetector
java
import cn.hutool.core.io.FileTypeUtil;
import com.baomidou.mybatisplus.generator.config.rules.FileType;
import lombok.Getter;
import org.apache.poi.poifs.filesystem.DirectoryEntry;
import org.apache.poi.poifs.filesystem.Entry;
import org.apache.poi.poifs.filesystem.FileMagic;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import java.io.*;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* 文件类型判断工具(基于文件头字节)
* @ClassName MergeFilesToPDFUtil
* @Description
* **/
public class FileTypeDetector {
// 常见文件的"文件头字节"和对应类型(扩展可添加更多格式)
private static final List<FileType> FILE_TYPES = Arrays.asList(
// PDF: 前4字节是 %PDF (十六进制:25 50 44 46)
new FileType("pdf", new byte[]{0x25, 0x50, 0x44, 0x46}),
// Word (docx): 同ZIP格式,前4字节 PK
new FileType("docx", new byte[]{0x50, 0x4B, 0x03, 0x04}),
// Word (doc): 前2字节是 D0 CF (OLE格式)
new FileType("doc", new byte[]{(byte) 0xD0, (byte) 0xCF}),
// Excel (xlsx/xlsm): 前4字节是 PK (ZIP格式,因为xlsx是压缩包),十六进制:50 4B 03 04
new FileType("xlsx", new byte[]{0x50, 0x4B, 0x03, 0x04}),
// Excel (xls): 前8字节是 58 4C 53 48 (BIFF8格式)
new FileType("xls", new byte[]{0x58, 0x4C, 0x53, 0x48, 0x00, 0x00, 0x00, 0x00}),
// Excel (xls): 前4字节是 D0 CF (OLE格式)
new FileType("xls", new byte[]{(byte) 0xD0, (byte) 0xCF}),
// 图片 (PNG): 前8字节是 89 50 4E 47 0D 0A 1A 0A
new FileType("png", new byte[]{(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}),
// 图片 (JPG): 前2字节是 FF D8
new FileType("jpg", new byte[]{(byte) 0xFF, (byte) 0xD8}),
// 图片 (GIF): 前3字节是 47 49 46
new FileType("gif", new byte[]{0x47, 0x49, 0x46}),
// XML (带UTF-8 BOM): 前3字节 EF BB BF + 第4字节 3C (<)
new FileType("xml", new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF, 0x3C}),
// XML (无BOM): 前1字节 3C (<),覆盖大多数纯XML文件(如<?xml version="1.0"?>)
new FileType("xml", new byte[]{0x3C}),
// 图片 (JPEG): 前2字节是 FF D8(与JPG核心头一致,单独标识格式)
new FileType("jpeg", new byte[]{(byte) 0xFF, (byte) 0xD8}),
// 图片 (JPEG EXIF版): 前4字节是 FF D8 FF E1(带EXIF信息的JPEG,如相机照片,提升识别精准度)
new FileType("jpeg", new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xE1})
);
// 读取流的前N字节(用于判断文件头,避免读取整个流)
private static final int READ_BYTES_LENGTH = 20; // 足够覆盖常见文件头长度
// 读取 ZIP 条目的最大字节限制(防止恶意文件,仅读前 10KB)
private static final int ZIP_SCAN_LIMIT = 1024 * 10;
private static final String XLSX_FEATURE = "xl/workbook.xml";
private static final String DOCX_FEATURE = "word/document.xml";
private static final String XLS_FEATURE = "Workbook";
private static final String DOC_FEATURE = "WordDocument";
/**
* 通过输入流判断文件类型
* @param inputStream 输入流(不会关闭流,需调用方自行处理)
* @return 文件类型枚举(如PDF、EXCEL_XLSX),未知类型返回 UNKNOWN
* @throws IOException 流读取异常
*/
public static String detectFileType(FileInputStream inputStream) throws IOException {
if (inputStream == null) {
return null;
}
try {
//读取前20个字节,用于判断文件类型
byte[] fileHeader = new byte[READ_BYTES_LENGTH];
int readLen = inputStream.read(fileHeader);
if (readLen <= 0) {
return null;
}
// 匹配文件头字节,返回对应的文件类型
for (FileType fileType : FILE_TYPES) {
byte[] magicNumber = fileType.magicNumber;
if (readLen >= magicNumber.length) {
byte[] actualHeader = Arrays.copyOfRange(fileHeader, 0, magicNumber.length);
if (Arrays.equals(actualHeader, magicNumber)) {
String typeName = fileType.getTypeName();
boolean check = true;
//xlsx、docx文件头都是OOXML格式,xls、doc文件头都是OLE格式,需要进一步判断
if(typeName.equals("xlsx") ){
check = isZipFeatureExists(inputStream,XLSX_FEATURE);
}else if(typeName.equals("docx")){
check = isZipFeatureExists(inputStream,DOCX_FEATURE);
}else if(typeName.equals("xls")){
check = isOleFeatureExists(inputStream,XLS_FEATURE);
}else if(typeName.equals("doc")){
check = isOleFeatureExists(inputStream,DOC_FEATURE);
}
if(check){
return typeName;
}
}
}
}
return null;
}catch (IOException e){
throw new IOException("系统异常,请联系管理员!",e);
}
finally {
inputStream.getChannel().position(0);
}
}
private static boolean isZipFeatureExists(FileInputStream fis,String feature) throws IOException {
// 重置流到起始位置(确保 ZipInputStream 从开头读取)
fis.getChannel().position(0);
ZipInputStream zipIs = null;
try {
//匿名包装流:重写 close() 为空,避免关闭包装流的同时关闭底层流
InputStream noCloseFis = new FilterInputStream(fis) {
@Override
public void close() throws IOException {}
};
zipIs = new ZipInputStream(noCloseFis);
ZipEntry entry;
// 遍历 ZIP 条目,查找特征文件(找到即返回,性能极高)
while ((entry = zipIs.getNextEntry()) != null) {
// 统一路径分隔符(避免 Windows/Linux 差异)
String entryPath = entry.getName().replace("\\", "/");
// System.out.println("辨别:"+feature+"---"+entryPath);
// 关闭当前条目,释放资源(不关闭会导致内存泄漏)
zipIs.closeEntry();
// 忽略大小写(部分特殊文件可能大小写不一致)
if (entryPath.equalsIgnoreCase(feature)) {
return true;
}
// 超过扫描限制,直接退出(防止恶意文件无限遍历)
// if (zipIs.available() <= 0) {
// break;
// }
}
return false;
} catch (IOException e) {
// 解析 ZIP 失败(非 ZIP 格式),返回异常
throw new IOException("系统异常,请联系管理员!",e);
}finally {
if(zipIs != null){
zipIs.close();
}
}
// return false;
}
/**
* 验证 OLE 容器内是否存在指定流(区分 xls/doc)
*/
private static boolean isOleFeatureExists(FileInputStream is, String featureStream) throws IOException {
//is.reset();
is.getChannel().position(0);
POIFSFileSystem poifs = null;
try {
// 使用不关闭底层流的包装
InputStream nonClosingStream = new FilterInputStream(is) {
@Override
public void close() throws IOException {
// 不执行任何操作,防止流被关闭
}
};
poifs = new POIFSFileSystem(nonClosingStream);
DirectoryEntry root = poifs.getRoot();
// 遍历 OLE 根目录下的流,查找特征流名称
for (Entry entry : root) {
if (entry.getName().equals(featureStream)) {
return true;
}
}
return false;
} catch (IOException e) {
// 解析 OLE 失败(非 OLE 格式)
throw new IOException("系统异常,请联系管理员!",e);
}finally {
if(poifs != null){
poifs.close();
}
}
}
// 内部静态类:存储文件类型名称和对应的文件头字节
@Getter
public static class FileType {
String typeName; // 类型名称(需与 FileTypeEnum 枚举值一致)
byte[] magicNumber; // 文件头字节(Magic Number)
FileType(String typeName, byte[] magicNumber) {
this.typeName = typeName;
this.magicNumber = magicNumber;
}
}
}