spring MVC 请求中文乱码问题排查详解

一、背景介绍
        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、最终结果出现同一套服务端接口代码,应对不同移动端请求,出现处理结果不一致问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值