利用POI处理office文件注意事项

记录小明一次无奈的修复BUG之旅。

故事纯属虚构,小明不代表任何人立场。

1.不就是处理文件吗?简单!

这一天,风和日丽,刚接手项目的小明接到了一个问题反馈:用户上传文件后,返回文件内容到前端失败。

大家都做过上传文件的功能吧,在java中一般使用MultipartFile类来接收前端传来的文件。对于word文档,只有doc和docx两种。用过POI的朋友都知道,处理doc(即word2003版本)和处理docx(即word2007版本)的方法不同,前者使用HWPFDocument,后者使用XWPFDocument:

1
2
3
4
// doc
HWPFDocument wordDocument = new HWPFDocument(inputStream);
// docx
XWPFDocument document = new XWPFDocument(inputStream);

既然官方都提供了处理的方法,那好办,按道理来说,以下代码逻辑足矣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 主要逻辑代码
@Test
void handleWord(MultipartFile multipartFile) throws Exception {

String docString = "doc";
String docxString = "docx";
String suffix = StringUtils.substringAfterLast(multipartFile.getOriginalFilename(), ".");

if (docString.equalsIgnoreCase(suffix)) {
HandleWord2003(multipartFile.getInputStream());
} else if (docxString.equalsIgnoreCase(suffix)){
HandleWord2007(multipartFile.getInputStream());
} else {
error();
}

}

通过word文档的后缀判断即可,小明思路很清晰,还没开始看旧代码,心里就写完了新代码。

但是看了旧代码后发现,前人思路完全一致,测了几次,一点问题没有。

2.小明:官方代码有问题?官方代码:你才有问题

文档上传失败了,肯定第一件事就是先拿到文件测一下,从前端请求开始检查,发现确实是后端处理发生了错误。

小明有点疑惑了,为什么会错误呢,调试呗。因为除了使用POI处理文档,方法中还干了其他事情,或许是其他逻辑有问题。不知道算不算可庆,HandleWord2003()第一行代码就报错了,也就是官方提供的方法:

1
HWPFDocument wordDocument = new HWPFDocument(inputStream);

官方代码也有问题?这个可能性极小,也有可能是某些依赖包和他产生冲突了。这个先放到后面,因为有一个可能性更大的:文档内容有问题。

于是小明打开了文档。

文档没有中文或其他奇怪的语言,全是标准的26个英文字母,也没有图片、超链接之类奇怪的东西,很普通的一份文档。

那就是官方代码有问题了,小明想。

但是小明不傻,官方发布了这么久的代码,大家都在用,不可能只有自己有问题,那可能就是包太旧?依赖冲突。于是小明直接就创建了一个新项目,只引入了POI的最新依赖包。

然而还是报错。

(为了故事的完整,小明设定为一个只靠自己处理,不依赖看控制台报错的传奇人物,当然,现实情况控制台也仅说明了这里应使用XWPFDocument而不是HWPFDocument,没说为什么)

3.你的doc长得有点像docx

不对啊,自己新建的文档,测试了好几十次了都没问题,就测试人员提供的这份文档有问题?

小明百般无奈,又重头读起了代码:当遇到doc时,使用HWPFDocument;当遇到docx时,使用XWPFDocument……小明灵机一闪,难道这不是doc?

于是,小明又开启了冲浪时间。

从网上的资料上看,doc和docx的存储方式不一样,docx中用了xml的方式存储,两种文件的16进制文件头也不一样。

小明对进制又不太懂的,于是试着使用notepad++直接打开了几个文档。

一打开,吓一跳,好家伙,全是乱码,但细心的小明还是发现了一些东西:

自己的测试文档,doc跟docx差别很大,怎么测试人员给的doc长得跟docx一样呢?

这时候小明心中有数了,但是这么难看谁都不想看,也没什么说服了。于是小明继续冲浪,发现用压缩工具打开会更加直观:

基本破案了,小明直接去追问测试人员,是不是你们的人把docx文档,直接通过改后缀的方式,把它强行变成doc呢,然而测试人员的回答让小明火冒三丈,甚至想直接提桶。测试人员说:

不知道。

没有然后了,小明忙了几个小时,换来了一句话。然而事情还是要进行下去,也不能就放着BUG去提桶了。当然,正常来说,写明系统需要正常操作,不允许直接修改文件后缀就行了。但是小明觉得测试人员的水平不足以理解,或者懒得理解,并且下次还会测这份假的doc文档。

小明也懒得和他们交流。于是继续跻身于知识的海洋冲浪,寻找解决方法。

4.小明怒写代码

小明发现,当把doc和docx转换为16进制后,文件的开头编码会有差别。小明在notepad++中下载了16进制插件,并把文档进行了转换,发现:

  • doc文件第一行:d0 cf 11 e0 a1 b1 1a e1 20 20 20 20 20 20 20 20
  • docx文件第一行:50 4b 03 04 0a 20 20 20 20 20 87 4e e2 40 20 20

并且创建了好几份文档都有这个规律。小明换了换网上搜索的关键词:文件16进制开头。还真有!

小明总结了几份资料并参考了其他人的代码,写下了以下工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
* 由于某些测试人才喜欢直接修改文件后缀,把doc和docx后缀相互转换,导致后续文件解析失败
* 于是有了这个工具类,用于判断文件的真实类型
*
* @author xiaoming
* @date 2020/11/25
*/
public class CheckFileFormatUtil {
/**
* 文件头信息
*/
private static final HashMap<String, String> M_FILE_TYPES = new HashMap<>();
static {
M_FILE_TYPES.put("504B0304", "docx");
M_FILE_TYPES.put("D0CF11E0", "doc");
// 文末附其他文件类型文件头
}

/**
* @param inputStream 输入流
* @return 文件头信息
*/
public static String getFileType(InputStream inputStream) {
return M_FILE_TYPES.get(getFileHeader(inputStream));
}

/**
* @param inputStream 输入流
* @return 文件头信息
*/
public static String getFileHeader(InputStream inputStream) {
// FileInputStream is = null;
InputStream is = null;
String value = null;
try {
// is = new FileInputStream(filePath);
is = inputStream;
byte[] b = new byte[4];
/*
* int read() 从此输入流中读取一个数据字节。int read(byte[] b) 从此输入流中将最多 b.length
* 个字节的数据读入一个 byte 数组中。 int read(byte[] b, int off, int len)
* 从此输入流中将最多 len 个字节的数据读入一个 byte 数组中。
*/
is.read(b, 0, b.length);
value = bytesToHexString(b);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return value;
}

/**
* @param src 要读取文件头信息的文件的byte数组
* @return 文件头信息
*/
private static String bytesToHexString(byte[] src) {
StringBuilder builder = new StringBuilder();
if (src == null || src.length <= 0) {
return null;
}
String hv;
for (byte aSrc : src) {
// 以十六进制(基数 16)无符号整数形式返回一个整数参数的字符串表示形式,并转换为大写
hv = Integer.toHexString(aSrc & 0xFF).toUpperCase();
if (hv.length() < 2) {
builder.append(0);
}
builder.append(hv);
}
return builder.toString();
}
}

于是最后代码改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    @Test
void handleWord(MultipartFile multipartFile) throws Exception {

String docString = "doc";
String docxString = "docx";
String suffix = StringUtils.substringAfterLast(multipartFile.getOriginalFilename(), ".");

// if (docString.equalsIgnoreCase(suffix)) {
// HandleWord2003(multipartFile.getInputStream());
// } else if (docxString.equalsIgnoreCase(suffix)){
// HandleWord2007(multipartFile.getInputStream());
// } else {
// error();
// }
if (docString.equalsIgnoreCase(CheckFileFormatUtil.getFileType(multipartFile.getInputStream()))){
HandleWord2003(multipartFile.getInputStream());
} else if (docxString.equalsIgnoreCase(CheckFileFormatUtil.getFileType(multipartFile.getInputStream()))){
HandleWord2007(multipartFile.getInputStream());
} else {
error();
}
}

写完后,小明测试了几次,都没有问题,算是解决了。其实,小明怕的不是BUG,只是有时碰到某些不负责任的行为,会浪费很多时间,但是面对打工这件事,谁还没点无奈呢,这只是小明这几天遇到的无奈的事之其一。

完。

  • 附其他文件类型16进制文件头:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
M_FILE_TYPES.put("FFD8FF", "jpg");
M_FILE_TYPES.put("89504E47", "png");
M_FILE_TYPES.put("47494638", "gif");
M_FILE_TYPES.put("49492A00", "tif");
M_FILE_TYPES.put("424D", "bmp");
M_FILE_TYPES.put("41433130", "dwg"); // CAD
M_FILE_TYPES.put("38425053", "psd");
M_FILE_TYPES.put("7B5C727466", "rtf"); // 日记本
M_FILE_TYPES.put("3C3F786D6C", "xml");
M_FILE_TYPES.put("68746D6C3E", "html");
M_FILE_TYPES.put("44656C69766572792D646174653A", "eml"); // 邮件
M_FILE_TYPES.put("D0CF11E0", "doc");
M_FILE_TYPES.put("D0CF11E0", "xls");
M_FILE_TYPES.put("5374616E64617264204A", "mdb");
M_FILE_TYPES.put("252150532D41646F6265", "ps");
M_FILE_TYPES.put("255044462D312E", "pdf");
M_FILE_TYPES.put("504B0304", "docx");
M_FILE_TYPES.put("504B0304", "xlsx");
M_FILE_TYPES.put("52617221", "rar");
M_FILE_TYPES.put("57415645", "wav");
M_FILE_TYPES.put("41564920", "avi");
M_FILE_TYPES.put("2E524D46", "rm");
M_FILE_TYPES.put("000001BA", "mpg");
M_FILE_TYPES.put("000001B3", "mpg");
M_FILE_TYPES.put("6D6F6F76", "mov");
M_FILE_TYPES.put("3026B2758E66CF11", "asf");
M_FILE_TYPES.put("4D546864", "mid");
M_FILE_TYPES.put("1F8B08", "gz");