记一次文件因content-type问题无法打开的经历
- 引
- 场景
- 方案
- Jsoup的Content-Type
- simplemagic
- file can't open
- 流不可重复消费问题
- Tika
- 总结
引
在Http请求头和响应头都有这个attribute,来声明请求和响应报文的资源类型。
Content-Type(MediaType),即是Internet Media Type,互联网媒体类型,也叫做MIME类型。在互联网中有成百上千中不同的数据类型,HTTP在传输数据对象时会为他们打上称为MIME的数据格式标签,用于区分数据类型。最初MIME是用于电子邮件系统的,后来HTTP也采用了这一方案。
在HTTP协议消息头中,使用Content-Type来表示请求和响应中的媒体类型信息。它用来告诉服务端如何处理请求的数据,以及告诉客户端(一般是浏览器)如何解析响应的数据,比如显示图片,解析并展示html等等。
场景
我这边遇到的情况是,源文件的Content-Type全都显示为application/octet-stream,如PDF,PNG等,都显示这个,接着我们需要将这个文件上传到下游系统中,下游系统有另外的retrieve接口,来查看和下载这些文件。按照原逻辑,上传到下游系统的文件的Content-Type依旧会显示为application/octet-stream,部分终端使用这种格式下载下来是没什么问题的,能正确获取文件名,也能正常打开。问题出在某些终端对文件扩展名的解析方式不一样,通过mimeType去解析文件时,遇到application/octet-stream就会有问题,文件名可能就变成了xxx.bin。当然,这并不是我们所期望的。
方案
从源文件到下游系统,中间由我们系统集成,所以很理所当然想到在上传到下游系统之前,能否把这个Content-Type给它弄对。
Jsoup的Content-Type
查看了源代码,我们系统在上传文件到下游系统时,使用的是Jsoup三方依赖。这个接口需要同时上传文件和json文本,请求方式为POST。
文本参数用典型的kv参数方法
/**
* Add a request data parameter. Request parameters are sent in the request query string for GETs, and in the
* request body for POSTs. A request may have multiple values of the same name.
* @param key data key
* @param value data value
* @return this Connection, for chaining
*/
Connection data(String key, String value);
针对文件类型的参数,Jsoup提供了以下方法,可以设定文件名和文件输入流
/**
* Add an input stream as a request data parameter. For GETs, has no effect, but for POSTS this will upload the
* input stream.
* @param key data key (form item name)
* @param filename the name of the file to present to the remove server. Typically just the name, not path,
* component.
* @param inputStream the input stream to upload, that you probably obtained from a {@link java.io.FileInputStream}.
* You must close the InputStream in a {@code finally} block.
* @return this Connections, for chaining
* @see #data(String, String, InputStream, String) if you want to set the uploaded file's mimetype.
*/
Connection data(String key, String filename, InputStream inputStream);
同时,我注意到Jsoup还提供了另外一个上传文件的方法,该方法支持设置文件的Content-Type
/**
* Add an input stream as a request data parameter. For GETs, has no effect, but for POSTS this will upload the
* input stream.
* @param key data key (form item name)
* @param filename the name of the file to present to the remove server. Typically just the name, not path,
* component.
* @param inputStream the input stream to upload, that you probably obtained from a {@link java.io.FileInputStream}.
* @param contentType the Content Type (aka mimetype) to specify for this file.
* You must close the InputStream in a {@code finally} block.
* @return this Connections, for chaining
*/
Connection data(String key, String filename, InputStream inputStream, String contentType);
simplemagic
看到这里,我喜出望外,认为只需要根据源文件的inputStream来获取文件的Content-Type,设置到这个参数里就行了。在获取inputStream的Content-Type信息时,有很多选择,有原生的,也可以选择三方依赖,我这里选择的是simplemagic。
<dependency>
<groupId>com.j256.simplemagic</groupId>
<artifactId>simplemagic</artifactId>
<version>1.17</version>
</dependency>
通过以下代码即可获取文件的Content-Type对象
ContentType contentType = new ContentInfoUtil().findMatch(file).getContentType();
该框架对输入提供了良好的支持,可以是文件路径,文件对象,文件流,也可以是byte数组。
按照这个思路,很快就完成了修改,本地postman测试,很快通过。
file can’t open
然而,按照上面描述的改动发版后,从下游系统下载的文件却无法打开。
通过postman调用下载接口,返回的内容可以预览(pdf格式,不得不说postman很强大),但是save response to file过后,文件却无法打开。这里有个小妙招,也是在工作中遇到的,有些打不开的pdf文件,如果使用类如edge等浏览器直接打开,打不开的话就直接报错打不开,如果用adobe尝试打开失败的话,会提示一些打不开的原因,如文件加密,文件格式损坏,文件破损等。这里我通过adobe打开发现是文件的file type有问题。
到这里,我怀疑过是我们下载接口的问题,但测试过直接从下游系统下载文件,文件依旧无法打开,那么问题只能出在我们将文件上传到下游系统的过程了。
我怀疑过Jsoup的传参方式是否正确,尝试过在文件头信息中加入Content-Type(因为我在debug的时候,看到入参的文件流是有头信息的,如postman上传,客户端上传等,都有),构建临时文件来转换等方法,均无一成功。首先,明确的是,通过simplemagic获取到的mimeType是对的,传到下游系统再反查出来回写到响应头里面的也是对的。
流不可重复消费问题
在处理文件打不开的过程,其实花费了许多时间,尝试的方式也很多,在上面没有一一列举。从下游系统下载的文件,通过simplemagic查看其Content-Type信息,为unkonwn,但文件后缀名是对的。这里需要说明一下,文件的mimeType判断方式有多种,扩展名是最常见也是不一定靠谱的一种。比如你创建一个text文件,强行将其扩展名修改为.pdf,再打开就会报错了,而且报的错和我这次遇到的报错一模一样。
在历经层层磨难后,终于发现了问题所在。
一个inputStream对象,我为了传获取它的Content-Type,其实已经消费了一次,在通过Jsoup上传到下游系统时,还是使用的是原来的那个流。再消费了一次后,再通过simplemagic去解析相同流的Content-Type时,你会发现结果就是unkonwn(第二次消费时,并没有报文件已关闭之类的异常)。至此,文件打不开的问题,就转换为处理流重复消费的问题了。直接跳转到针对流只能消费一次的处理方案就可以了,之前已经记录过,不再赘述。
通过这里的修改,文件便可以正常的上传,下载,预览,打开了。
Tika
和simplemagic类似,tika也是一个文件Content-Type解析的三方依赖,在处理问题的过程中,我也用过。经过测试,simplemagic对大部分文件支持良好,但csv文件却不行,而tika可以解析csv,但对office相关文件显得有些无能为力。所以最终使用二者结合来支持文件mimeType的判断。
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.8.0</version>
</dependency>
总结
简单总结一下,在你我的开发生涯中,总会遇到一个又一个麻烦的问题(对当时的你会是个麻烦),可能你花了一些时间能够处理,并在过程中有所收获,也有可能你直接百度或csdn一搜就有了结果。领导可能在意的是处理了问题的结果,而对你个人成长,更重要的是处理问题的过程。
在刚出来工作的第一家公司,我们的一个服务总是会内存溢出,可用性极差。真正处理要处理这个问题可能需要花很多时间去排查测试(因为已经排查过业务代码的逻辑),领导建议做服务自动重启的工作,这样在一定程度能够缓解。对要求不算高的甲方,可能就糊弄过去了。但作为一个有洁癖的程序员,肯定不能接受这种是是而非的处理方式。后来通过层次排查,发现问题出来了三方依赖里面,问题才得到了真正的处理。
类似这样的问题还有很多,我也在CSDN的问答板块看到过许许多多折磨这程序员的问题,有些我已经经历过并掌握了,会觉得很简单,有一些是我还没曾在学习过程或项目中遇到过的,当我遇到了,也会头疼。所以,不断总结就显得非常重要了。
另外相对自己告诫的一点,查看处理问题时眼光不能太局限,定位问题比处理问题更重要。