概述
兼容项目的jdk1.7,只能使用resteasy3.0.19.Final版本,之后的版本需要1.8支持
使用此版本进行post提交时,使用"multipart/form-data"进行表单提交,对于中文文件名会出现乱码,出现乱码的本质就是字符转码失败.
相对于web访问中对字符进行不同编码之间的转换应用场景,基本就是读取流中自己数据,字符而言,流中存储的是字节,服务器处理时按照你解码的方式进行编码获取正确的字符,然后服务器根据字符进行相应的处理.出现乱码的原因主要是服务器浏览器编码方式不一致,比如浏览器用uft-8进行编码,而服务器用us_asii就会出现乱码(本次乱码原因就是在此)
demo
开始测试时以为是前端解析参数时出现的问题,后来才发现问题其实出现现在resteasy自己的参数解析
jsp测试
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<form method="post" action="xxx" enctype="multipart/form-data">
选择一个文件:
<input type="file" name="uploadFile" />
<br/><br/>
<input type="submit" value="上传" />
</form>
</body>
</html>
起初以为是前端页面解析的问题,在form 表单里添加accept-charset=''utf-8''也没用,
其实accept-charset默认值是 "unknown",表示表单的字符集与包含表单的文档的字符集相同.
使用jetty8 默认 处理编码utf-8
查看post的请求体payload
------WebKitFormBoundaryRAPt6XbF1drWs6Kx
Content-Disposition: form-data; name="uploadFile"; filename="我是中文.xlsx"
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
------WebKitFormBoundaryRAPt6XbF1drWs6Kx--
这个请求体,然后在contentType中加入charset=utf-8,其实这个参数怎么起作用,作用于什么内容,是跟框架的解析有关系,一般的框架处理解析的是其内容
查看MultipartFormDataInput是如何解析的
数据以字节流的形式InputStream传输到后台,经过web一系列的参数解析(有时间再搞出来),最终需要绑定到MultipartFormDataInput的实现类MultipartFormDataInputImpl,绑定过程有一个解析过程(预处理->设置载体(Message)->设置解析器MimeStreamParser->设置内容处理器ContentHandler),比如MultipartFormDataInputImpl的父类设置载体(载体中设置解析器,解析器中设置内容处理):
public void parse(InputStream is) throws IOException
{
mimeMessage = new BinaryMessage(addHeaderToHeadlessStream(is));
extractParts();
}
到达这里时,InputStream 中文件名存储的是utf-8的自己数组
最后回调到MimeStreamParser
public void parse(InputStream is) throws MimeException, IOException {
mimeTokenStream.parse(is);
OUTER: for (;;) {
int state = mimeTokenStream.getState();
switch (state) {
case MimeTokenStream.T_BODY:
BodyDescriptor desc = mimeTokenStream.getBodyDescriptor();
InputStream bodyContent;
if (contentDecoding) {
bodyContent = mimeTokenStream.getDecodedInputStream();
} else {
bodyContent = mimeTokenStream.getInputStream();
}
handler.body(desc, bodyContent);
break;
case MimeTokenStream.T_END_BODYPART:
handler.endBodyPart();
break;
case MimeTokenStream.T_END_HEADER:
handler.endHeader();
break;
case MimeTokenStream.T_END_MESSAGE:
handler.endMessage();
break;
case MimeTokenStream.T_END_MULTIPART:
handler.endMultipart();
break;
case MimeTokenStream.T_END_OF_STREAM:
break OUTER;
case MimeTokenStream.T_EPILOGUE:
handler.epilogue(mimeTokenStream.getInputStream());
break;
case MimeTokenStream.T_FIELD:
handler.field(mimeTokenStream.getField());
break;
case MimeTokenStream.T_PREAMBLE:
handler.preamble(mimeTokenStream.getInputStream());
break;
case MimeTokenStream.T_RAW_ENTITY:
handler.raw(mimeTokenStream.getInputStream());
break;
case MimeTokenStream.T_START_BODYPART:
handler.startBodyPart();
break;
case MimeTokenStream.T_START_HEADER:
handler.startHeader();
break;
case MimeTokenStream.T_START_MESSAGE:
handler.startMessage();
break;
case MimeTokenStream.T_START_MULTIPART:
handler.startMultipart(mimeTokenStream.getBodyDescriptor());
break;
default:
throw new IllegalStateException("Invalid state: " + state);
}
state = mimeTokenStream.next();
}
}
这里使用到java的新进后出的Stack与反射,对post payload的这种field进行解析,这里主要讲对header的处理
一个完整的header完整的处理流程
- startHeader()->field()->endHeader();
- startHeader()-往stack中push一个new Header())
- endHeader()-stack peek一个出来(新进后出的原则,也就是把你刚刚放进去的header拿出来)
重点在field()中
public void field(Field field) throws MimeException {
expect(Header.class);
Field parsedField = AbstractField.parse(field.getRaw());
((Header) stack.peek()).addField(parsedField);
}
public static ParsedField parse(final ByteSequence raw) throws MimeException {
String rawStr = ContentUtil.decode(raw);
return parse(raw, rawStr);
}
public static String decode(ByteSequence byteSequence) {
return decode(CharsetUtil.US_ASCII, byteSequence, 0, byteSequence
.length());
}
看到以上就明白了,最后以US_ASCII编码,而且是写死的,没什么可配置修改,至此乱码原因找到了.因为基本没什么配置,所以一些解析器都是使用的默认,要修复这个bug大体上有三个个思路
- 第一种:filename与文件分开.分开有两种方法,第一种客户端另外传递一个filename参数(不推荐),第二种服务器单独把filename取出来,独自解析
- 第二种:使用指定或者自定义的解析器
- 第三种:改源码,最简单,最暴力第一种尝试失败了
我要获取content-disposition,必须要读inputstream,但是我用jetty使用的是httpinput,这个inputstream不允许重复读(不支持reset,大概是保护数据只被读取一次)
第二种,自定义一个MultipartFormDataReader,自己对内容进行处理,这么做相当于对自定义了一个MultipartFormDataInput,复杂
第三种,改源码
resteasy 还有一种@MultipartForm,这种底层原理就是MultipartFormDataInput(二者默认的formdatareader不一样,一个是MultipartFormDataReader,一个是MultipartFormAnnotationReader),最后将内容解析单独拿出来封装到你制定的Entity中,但是这种需要你额外在entity指定file与filename的key(@FormParam("selectedFile"),也就意味着客户端必须与服务器的key值一样,太挫)
或者直接使用原生的requeset的common uploadfile
以下代码仅供测试用例:
ServletFileUpload upload = new ServletFileUpload();
FileItemIterator fileIterator = upload.getItemIterator(request);
String parent="C:\\Users\\admin\\Desktop\\temp\\";
while (fileIterator.hasNext()) {
FileItemStream item = fileIterator.next();
String filename=item.getName();
File file = new File(parent,filename);
FileUtils.writeByteArrayToFile(file, IOUtils.toByteArray(item.openStream()));
}
return "Done";
附录编码笔记
任何编码,都有特定的机器语言与之对应,最终是以字节的形式存储,不同的编码其存储的字节不同.简单的讲,一个符号对应编码表中一个数字,不同的编码格式决定了这个数字以什么样的方式存储.以"中"字为例
"中"字在utf-8与unicode中各自对应的数字(可以理解为唯一标识)不一样
再来看看字节
utf-8存储是三个字节,而unicode存储的是四个字节.存储多少个自己根据的就是编码规则来定.而unicode与utf等其他的区别,主要是前者是code point,后者是UCS Transformation Format
获取虚拟机编码Charset.defaultCharset(),也就是工作运行的编码(比如存储class文件的编码(可以通过文件的properties属性查看)),但是在java内存中,String是以unicode编码的形式存在的(这个就真不知道怎么去测试了),我们用String.getBytes(charset),其底层就是讲内存中unicode编码的字符串转为指定编码的字节(因为unicode基本可以向任何一种编码格式转换).