一、背景介绍
java后端使用Spring MVC框架,在需求测试过程中遇到,服务端接收到Android请求时出现中文乱码问题,但接收到IOS请求时中文正常显示。
二、问题排查步骤一
由于在web.xml中配置了编码转换器,代码如下所示:
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
在CharacterEncodingFilter中对 request.setCharacterEncoding(encoding) 进行了赋值,代码如下所示:
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String encoding = getEncoding();
if (encoding != null) {
if (isForceRequestEncoding() || request.getCharacterEncoding() == null) {
request.setCharacterEncoding(encoding);
}
if (isForceResponseEncoding()) {
response.setCharacterEncoding(encoding);
}
}
filterChain.doFilter(request, response);
}
对以上代码中的request.getCharacterEncoding()方法引起了怀疑,查看源码发现会调用到父类org.apache.catalina.connector.Request>getCharacterEncoding 向上逐级调用到org.apache.coyote.Request>getCharacterEncoding()>getCharsetFromContentType() 方法,代码如下所示:
/**
* Parse the character encoding from the specified content type header.
* If the content type is null, or there is no explicit character encoding,
* <code>null</code> is returned.
*
* @param contentType a content type header
*/
private static String getCharsetFromContentType(String contentType) {
if (contentType == null) {
return (null);
}
int start = contentType.indexOf("charset=");
if (start < 0) {
return (null);
}
String encoding = contentType.substring(start + 8);
int end = encoding.indexOf(';');
if (end >= 0) {
encoding = encoding.substring(0, end);
}
encoding = encoding.trim();
if ((encoding.length() > 2) && (encoding.startsWith("\""))
&& (encoding.endsWith("\""))) {
encoding = encoding.substring(1, encoding.length() - 1);
}
return (encoding.trim());
}
通过以上代码可以得知,即使在服务端配置了CharacterEncodingFilter转码器,调用者配置的contentType>charset= 依旧能起到关键性作用。
*问题排查到这里与移动端沟通后发现IOS再请求服务端接口时显示的在contentType中配置了charset=UTF-8,但Android在请求时并未配置charset=UTF-8,与Android沟通后在请求的contentType中显示配置charset=UTF-8,中文乱码问题正常解决。
三、问题排查步骤二
问题并未完,因为本次需求是在原有老项目中进行的功能需求迭代,并且Android端本次并未做任何改造,因此表示是由于本次的改动导致Android端请求出现中文乱码。
没办法只能进行排除法来排查查找问题,最终在web.xml中发现可疑点,发现本次新增了一个Filter 并且配置到了CharacterEncodingFilter转码器前面,先于CharacterEncodingFilter转码器执行,逐行排查发现以下代码非常可疑:
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
.....
if(httpServletRequest.getParameterMap().containsKey("xxxxx")){
}
...
}
通过对httpServletRequest.getParameterMap()向上追踪发现org.apache.catalina.connector.Request>getParameterMap()>getParameterNames()>parseParameters()方法,详细代码如下所示:
/**
* Hash map used in the getParametersMap method.
*/
protected ParameterMap<String, String[]> parameterMap = new ParameterMap<>();
/**
* Returns a <code>Map</code> of the parameters of this request.
* Request parameters are extra information sent with the request.
* For HTTP servlets, parameters are contained in the query string
* or posted form data.
*
* @return A <code>Map</code> containing parameter names as keys
* and parameter values as map values.
*/
@Override
public Map<String, String[]> getParameterMap() {
if (parameterMap.isLocked()) {
return parameterMap;
}
Enumeration<String> enumeration = getParameterNames();
while (enumeration.hasMoreElements()) {
String name = enumeration.nextElement();
String[] values = getParameterValues(name);
parameterMap.put(name, values);
}
parameterMap.setLocked(true);
return parameterMap;
}
/**
* Return the names of all defined request parameters for this request.
*/
@Override
public Enumeration<String> getParameterNames() {
if (!parametersParsed) {
parseParameters();
}
return coyoteRequest.getParameters().getParameterNames();
}
/**
* Parse request parameters.
*/
protected void parseParameters() {
parametersParsed = true;
Parameters parameters = coyoteRequest.getParameters();
boolean success = false;
try {
// Set this every time in case limit has been changed via JMX
parameters.setLimit(getConnector().getMaxParameterCount());
// getCharacterEncoding() may have been overridden to search for
// hidden form field containing request encoding
String enc = getCharacterEncoding();
boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
if (enc != null) {
parameters.setEncoding(enc);
if (useBodyEncodingForURI) {
parameters.setQueryStringEncoding(enc);
}
} else {
//DEFAULT_CHARACTER_ENCODING = ISO-8859-1 默认值
parameters.setEncoding
(org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);
if (useBodyEncodingForURI) {
parameters.setQueryStringEncoding
(org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);
}
}
parameters.handleQueryParameters();
if (usingInputStream || usingReader) {
success = true;
return;
}
if( !getConnector().isParseBodyMethod(getMethod()) ) {
success = true;
return;
}
String contentType = getContentType();
if (contentType == null) {
contentType = "";
}
int semicolon = contentType.indexOf(';');
if (semicolon >= 0) {
contentType = contentType.substring(0, semicolon).trim();
} else {
contentType = contentType.trim();
}
if ("multipart/form-data".equals(contentType)) {
parseParts(false);
success = true;
return;
}
if (!("application/x-www-form-urlencoded".equals(contentType))) {
success = true;
return;
}
int len = getContentLength();
if (len > 0) {
int maxPostSize = connector.getMaxPostSize();
if ((maxPostSize >= 0) && (len > maxPostSize)) {
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.postTooLarge"));
}
checkSwallowInput();
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
return;
}
byte[] formData = null;
if (len < CACHED_POST_LEN) {
if (postData == null) {
postData = new byte[CACHED_POST_LEN];
}
formData = postData;
} else {
formData = new byte[len];
}
try {
if (readPostBody(formData, len) != len) {
parameters.setParseFailedReason(FailReason.REQUEST_BODY_INCOMPLETE);
return;
}
} catch (IOException e) {
// Client disconnect
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.parseParameters"),
e);
}
parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
return;
}
parameters.processParameters(formData, 0, len);
} else if ("chunked".equalsIgnoreCase(
coyoteRequest.getHeader("transfer-encoding"))) {
byte[] formData = null;
try {
formData = readChunkedPostBody();
} catch (IllegalStateException ise) {
// chunkedPostTooLarge error
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.parseParameters"),
ise);
}
return;
} catch (IOException e) {
// Client disconnect
parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.parseParameters"),
e);
}
return;
}
if (formData != null) {
parameters.processParameters(formData, 0, formData.length);
}
}
success = true;
} finally {
if (!success) {
parameters.setParseFailedReason(FailReason.UNKNOWN);
}
}
}
发现parameterMap 是LinkedHashMap 的子类,通过上面代码可以得出一个结论,那就是Request.parameterMap 再初次请求时会对进行初始化赋值,并且一旦赋值不会二次进行修改。
在调用parseParameters()方法进行赋值时,会调用parameters.setQueryStringEncoding()方法配置参数编码格式,并且编码格式优先来源请求中contentType配置的charset编码,如果请求未配置会默认配置org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING="ISO-8859-1"。
四、问题结论
1、由于在CharacterEncodingFilter转码器之前调用了request.getParameterMap(),导致Request的参数优先于配置request.setCharacterEncoding(encoding)编码格式提前初始化。
2、由于Android移动端请求中未显示对contentType配置charset编码,导致调用request.getParameterMap()方法时,进行Request的参数初始化时,使用的默认编码格式DEFAULT_CHARACTER_ENCODING="ISO-8859-1",从而将Android移动端的中文强转为了ISO-8859-1导致乱码。
3、最终结果出现同一套服务端接口代码,应对不同移动端请求,出现处理结果不一致问题。