最近遇到一个很蛋疼的问题,机器发来http请求,信息都放在body Data里用gb2312编码,然后后台用@RequestBody来接受,这时问题来了,机器发来的请求没有设置content-type,于是默认就是content-type:application/x-www-form-urlencoded,然后spring容器就默认设置CharacterEncoding为utf-8,来解码。更奇怪的是,spring中途还对body使用urlencode。

比方说http body内容是“温度设定值”,最后接受到的是

%ef%bf%bd%c2%b6%ef%bf%bd%ef%bf%bd%e8%b6%a8%d6%b5,用URLDecode.decode(data,"gb2312")解码后是“锟铰讹拷锟借定值”,经过我多次试验,实际过程是这样的,

	public static void main(String[] args) {
		try {
			String data = "温度设定值";
			System.out.println(data);
			data = new String(data.getBytes("gb2312"),"utf-8");
			System.out.println(data);
			data = URLEncoder.encode(data,"utf-8");
			System.out.println(data);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
温度设定值
�¶��趨ֵ
%EF%BF%BD%C2%B6%EF%BF%BD%EF%BF%BD%E8%B6%A8%D6%B5

多次debug后终于发现了spring自作主张对内容进行urlencode,在ServletServerHttpRequest类的

getBodyFromServletRequestParameters方法中

	/**
	 * Use {@link javax.servlet.ServletRequest#getParameterMap()} to reconstruct the
	 * body of a form 'POST' providing a predictable outcome as opposed to reading
	 * from the body, which can fail if any other code has used the ServletRequest
	 * to access a parameter, thus causing the input stream to be "consumed".
	 */
	private static InputStream getBodyFromServletRequestParameters(HttpServletRequest request) throws IOException {
		ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
		Writer writer = new OutputStreamWriter(bos, FORM_CHARSET);

		Map<String, String[]> form = request.getParameterMap();
		for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
			String name = nameIterator.next();
			List<String> values = Arrays.asList(form.get(name));
			for (Iterator<String> valueIterator = values.iterator(); valueIterator.hasNext();) {
				String value = valueIterator.next();
				writer.write(URLEncoder.encode(name, FORM_CHARSET));
				if (value != null) {
					writer.write('=');
					writer.write(URLEncoder.encode(value, FORM_CHARSET));
					if (valueIterator.hasNext()) {
						writer.write('&');
					}
				}
			}
			if (nameIterator.hasNext()) {
				writer.append('&');
			}
		}
		writer.flush();

		return new ByteArrayInputStream(bos.toByteArray());
	}
	protected static final String FORM_CHARSET = "UTF-8";

FORM_CHARSET是静态常量,也就是说是固定的。spring为什么要将@RequestBody接受的数据进行urlencode编码我不得而知,总之我们知道了不管怎样最后都应该用UrlDecode.decode(data,"utf-8")进行解码,然后问题就变成了如何解决spring用utf-8默认解码的问题。

 

查了很多资料,也试验了很多,最快捷的方法自然是机器传来的信息加上消息头 content-type charset=GB2312,这样tomcat容器就会使用gb2312进行解码了。但是我的提议没有被接受。那么只有强制在这个消息路径里使用

request.setCharacterEncoding("gbk2312")了,但是servlet规定只有在调用request.getParameters()之前设置

characterEncoding才会生效,而spring容器早就不知道在之前做过了多少事情了。我试图在过滤器中设置request编码,但很可惜并不生效,说明spring在过滤器之前就调用了getParameters方法。

@WebFilter(filterName = "encodingFilter", urlPatterns = "/*")
public class MutiCharacterEncodingFilter extends OncePerRequestFilter

在这个问题上我花费了大量时间,几乎绝望了,网上的信息都是说要在getParameters之前设置request编码,这我已经充分了解了,但是你告诉我加在哪儿啊?怎么在spring做出处理之前设置编码。最绝望的就是,你知道解决问题的方法,但却不知道怎么实现。最后我幸运地找到了资料,很可惜并不是我自己独立完成的。

 

Spring boot 字符集编码

作者跟踪源码,发现CharacterEncodingFilter会调用request.setCharacterEncoding("UTF-8"),于是他写了一个类继承

CharacterEncodingFilter,并在Application中注入它

    @Bean()
    @ConfigurationProperties(prefix = "spring.http.encoding")
    @ConfigurationPropertiesBinding
    public MutiCharacterEncodingFilter mutiCharacterEncodingFilter(){
        MutiCharacterEncodingFilter encodingFilter = new MutiCharacterEncodingFilter();
        encodingFilter.setEncoding(charset);
        encodingFilter.setForceRequestEncoding(forceRequest);
        encodingFilter.setForceResponseEncoding(forceResponse);
        return encodingFilter;
    }

这样自定义的过滤器就会取代CharacterEncodingFilter。request.setCharacterEncoding也就生效了。

这让我对springboot的了解更加得深入,之前都是copy别人的代码,发现有很多用@Bean的方式配置变量,一直只知其然不知其所以然。看来springboot会把@Bean标注的变量替换掉他默认的变量,只要这个变量继承了那个默认变量。而我用

@WebFilter加入的过滤器只是加在这些默认过滤器的后面,而不是替换这些默认过滤器。

至此,乱码问题终于解决

 

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐