#背景
最近开发Dubbox服务,使用了Http协议对PHP系统暴露了一些Service服务,但是在上传时出现了乱码,google没有发现好的解决方案,只能自己debug,发现是配置中缺少一项。
#解决方案
直接说解决方案:
添加一个filter,filter内容如下:
/**
* Servlet Filter设置编解码<br>
*/
public class CharacterEncodingFilter implements Filter {
private static final String ENCODING_UTF_8 = "UTF-8";
@Override
public void init(FilterConfig config) throws ServletException {
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
request.setCharacterEncoding(ENCODING_UTF_8);
response.setCharacterEncoding(ENCODING_UTF_8);
request.setAttribute(InputPart.DEFAULT_CHARSET_PROPERTY, ENCODING_UTF_8);
chain.doFilter(request, response);
}
}
这里的关键是 InputPart.DEFAULT_CHARSET_PROPERTY
这个 attribute
,设置为 utf-8
,会覆盖掉 dubbox 默认的 us-ascii
编码。
问题定位
这个问题的发生原因是 dubbox 使用的是 RestEasy
框架解析上传数据,RestEasy
框架会判断 request 的 content-type
将 content-type
为multipart/form-data
的 request 交给 resteasy-multipart-provider
包来解析,大致解析过程如下:
- 框架确定 Content-Type 为 multipart/form-data,这个常量定义在 jax 支持包的 MediaType 接口中;
- 框架将请求交给 MultipartFormDataReader ,这个 Reader 注解为 Provider,调用readForm 方法,解析请求;
- MultipartFormDataInputImpl 的 parse 方法解析 body 内容,读取数据,乱码发生在这个方法内部;
MultipartFormDataInputImpl解析
- MultipartFormDataInputImpl 的 parse 方法是继承自父类:MultipartInputImpl 的,解析过程稍微有点复杂,但是最终是通过构造 PartImpl 来表示每一个参数的。
- InputPart 来表示 Form 表单中每一项参数,PartImpl 是 InputPart 的一个实现类,构造的时候传入BodyPart,然后做解析,构造过程如下:
- 框架使用 MultipartFormDataInputImpl 读取 body 内容发生乱码,源代码注释如下。
// 使用注解,框架可以自动发现这个Provider
@Provider
@Consumes("multipart/form-data")
public class MultipartFormDataReader implements MessageBodyReader<MultipartFormDataInput>
{
protected
@Context
Providers workers;
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType)
{
return type.equals(MultipartFormDataInput.class);
}
/**
* 这个方法是关键,RestEasy框架调用这个readFrom方法获取请求信息,
**/
public MultipartFormDataInput readFrom(Class<MultipartFormDataInput> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException
{
// 读取 boundary,Content-Type 中会包含这个boundary,类似如下:
// Content-Type:multipart/form-data;boundary=---------------------------7d33a816d302b6
// 表示的是使用 ---------------------------7d33a816d302b6 作为 http body 内的参数分割符号
String boundary = mediaType.getParameters().get("boundary");
if (boundary == null) throw new IOException(Messages.MESSAGES.unableToGetBoundary());
// 实际上的解析器
MultipartFormDataInputImpl input = new MultipartFormDataInputImpl(mediaType, workers);
input.parse(entityStream);
return input;
}
}
PartImpl
这个类作用是用来表示 body 中每个 form 表单项,比如,你传递了name,sex,image,那么就会有三个 PartImpl 的实例,对应三个参数。
构造过程如下:
public PartImpl(BodyPart bodyPart)
{
this.bodyPart = bodyPart;
//选择ContentType,看Client是否传递了Content-Type,里面有可能包含编码信息;
for (Field field : bodyPart.getHeader())
{
headers.add(field.getName(), field.getBody());
if (field instanceof ContentTypeField)
{
contentType = MediaType.valueOf(field.getBody());
contentTypeFromMessage = true;
}
}
//如果是null,则用默认的Content-Type,一般Client发送的Content-Type不为null,但是不包含编码;
if (contentType == null)
contentType = defaultPartContentType;
//从ContentType中找编码字符,乱码时找不到,进入if内逻辑;
if (getCharset(contentType) == null)
{
if (defaultPartCharset != null) //使用框架全局默认的编码,filter的作用就是设置这个。
{
contentType = getMediaTypeWithDefaultCharset(contentType);
}
else if (contentType.getType().equalsIgnoreCase("text"))//没有默认,使用us-ascii编码,乱码发生。
{
contentType = getMediaTypeWithCharset(contentType, "us-ascii");
}
}
}
MultipartInputImpl : 默认的Conten-Type定义及初始化位置
默认的 Content-Type 在 MultipartInputImpl 中,定义如下,这个是成员初始化的默认值,在构造的时候可以覆盖掉。
// 实际上就是text/plain; charset=us-ascii
protected MediaType defaultPartContentType = MultipartConstants.TEXT_PLAIN_WITH_CHARSET_US_ASCII_TYPE;
默认的defalutPartCharst的初始化时在构造MultipartInputImpl的时候,在构造方法中,这个对象,每次请求都会new一个,所以是和request相关的:
public MultipartInputImpl(MediaType contentType, Providers workers)
{
this.contentType = contentType;
this.workers = workers;
HttpRequest httpRequest = ResteasyProviderFactory
.getContextData(HttpRequest.class);
if (httpRequest != null)
{
// 从 request 的 attribute 中获取默认的编码和 contentType
String defaultContentType = (String) httpRequest
.getAttribute(InputPart.DEFAULT_CONTENT_TYPE_PROPERTY);
if (defaultContentType != null)
this.defaultPartContentType = MediaType
.valueOf(defaultContentType);
this.defaultPartCharset = (String) httpRequest.getAttribute(InputPart.DEFAULT_CHARSET_PROPERTY);
if (defaultPartCharset != null)
{
this.defaultPartContentType = getMediaTypeWithDefaultCharset(this.defaultPartContentType);
}
}
}