resteasy 上传表单文件名乱码

概述

       兼容项目的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基本可以向任何一种编码格式转换).

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值