记录小明一次无奈的修复BUG之旅。
故事纯属虚构,小明不代表任何人立场。
1.不就是处理文件吗?简单!
这一天,风和日丽,刚接手项目的小明接到了一个问题反馈:用户上传文件后,返回文件内容到前端失败。
大家都做过上传文件的功能吧,在java中一般使用MultipartFile类来接收前端传来的文件。对于word文档,只有doc和docx两种。用过POI的朋友都知道,处理doc(即word2003版本)和处理docx(即word2007版本)的方法不同,前者使用HWPFDocument,后者使用XWPFDocument:
1 2 3 4
| HWPFDocument wordDocument = new HWPFDocument(inputStream);
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
|
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"); }
public static String getFileType(InputStream inputStream) { return M_FILE_TYPES.get(getFileHeader(inputStream)); }
public static String getFileHeader(InputStream inputStream) {
InputStream is = null; String value = null; try {
is = inputStream; byte[] b = new byte[4];
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; }
private static String bytesToHexString(byte[] src) { StringBuilder builder = new StringBuilder(); if (src == null || src.length <= 0) { return null; } String hv; for (byte aSrc : src) { 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(CheckFileFormatUtil.getFileType(multipartFile.getInputStream()))){ HandleWord2003(multipartFile.getInputStream()); } else if (docxString.equalsIgnoreCase(CheckFileFormatUtil.getFileType(multipartFile.getInputStream()))){ HandleWord2007(multipartFile.getInputStream()); } else { error(); } }
|
写完后,小明测试了几次,都没有问题,算是解决了。其实,小明怕的不是BUG,只是有时碰到某些不负责任的行为,会浪费很多时间,但是面对打工这件事,谁还没点无奈呢,这只是小明这几天遇到的无奈的事之其一。
完。
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"); 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");
|