java web请求乱码问题

Posted by hcy on June 19, 2020

java web请求乱码问题

1.get请求路径参数内的中文

发起请求 /test?name=黄 1.因为有汉字,浏览器会将其编码为Url编码,使用哪种字符集浏览器决定,但是多数应该都是utf-8 2.tomcat解析此处的参数为name=%E9%BB%84的形式 3.代码调用request.getParameterMap() 此时会进行Url解码 4.tomcat将name=%E9%BB%84解码,使用utf-8字符集,此字符集是硬编码在代码里的。

请查看类org.apache.tomcat.util.http.Parameters类,此类有个字段queryStringCharset默认就是utf-8,默认情况下使用utf-8解析路径上的参数。

2.post请求,x-www-form-urlencoded

此类型的请求体的参数是由tomcat解析的。 1.如果调用过request.setCharacterEncoding(),设置过字符集,则使用设置的。 2.为空的话,尝试从content-type里获取

​ 3.为空的话,尝试从Context上获取

​ 4.为空的话,返回默认iso8859-1

tomcat内获取字符集源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
    private Charset getCharset() {
        Charset charset = null;
        try {
            //如果设置过的返回设置的,否则尝试从content-type里获取
            charset = coyoteRequest.getCharset();
        } catch (UnsupportedEncodingException e) {
        }
        if (charset != null) {
            return charset;
        }
		//从context里获取
        Context context = getContext();
        if (context != null) {
            String encoding = context.getRequestCharacterEncoding();
            if (encoding != null) {
                try {
                    return B2CConverter.getCharset(encoding);
                } catch (UnsupportedEncodingException e) {
                    // Ignore
                }
            }
        }
		//返回默认iso8859-1
        return org.apache.coyote.Constants.DEFAULT_BODY_CHARSET;
    }


   public Charset getCharset() throws UnsupportedEncodingException {
       //这个charset就是调用request.setCharacterEncoding()设置的
       if (charset == null) {
            getCharacterEncoding();
            if (characterEncoding != null) {
                charset = B2CConverter.getCharset(characterEncoding);
            }
         }

        return charset;
    }
	
    public String getCharacterEncoding() {
        if (characterEncoding == null) {
            //从contentType里获取
            characterEncoding = getCharsetFromContentType(getContentType());
        }

        return characterEncoding;
    }

3.post请求,form-data

​ 获取请求体内参数的过程和x-www-form-urlencoded是一样的, ​ 只是解析的方式不一样,但获取字符集过程是一样的。

4.post请求体内的中文,application/json

Tomcat不会处理此类型的请求体,会当成inputstream的形式放入request里。

Controller内使用@RequestBody接收参数,参数类型为String

Springmvc会帮我们将它映射到参数上,根据参数类型不同,映射方法也不同,如果是Pojo则会使用配置的MappingJackson2HttpMessageConverter 或者FastJsonHttpMessageConverter将流转成Pojo,现在我们讨论转成字符串的形式。

​ 处理参数映射时,从多个MessageConverter中选择一个能处理的Converter,因为参数是字符串,这里选择到的是StringMessageConverter

在下面代码16行处,调用了inputMessage.getHeaders(),这里会解析出所有的请求头,且会将字符集设置到content-type上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters()

@Nullable
	protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

		MediaType contentType;
		boolean noContentType = false;
		try {
       /*
       在这里获取过一次getHeaders
       如果原始的Content-Type:application/json
       则会被修改成 application/json;charset=request.getCharsetEncoding()
       */
			contentType = inputMessage.getHeaders().getContentType();
		}
		catch (InvalidMediaTypeException ex) {
			throw new HttpMediaTypeNotSupportedException(ex.getMessage());
		}
		

		Class<?> contextClass = parameter.getContainingClass();
		Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);


		HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
		Object body = NO_VALUE;

		EmptyBodyCheckingHttpInputMessage message;
		try {
			message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
			
            //获取到StringMessageConverter
			for (HttpMessageConverter<?> converter : this.messageConverters) {
				Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
				GenericHttpMessageConverter<?> genericConverter =
						(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
				if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
						(targetClass != null && converter.canRead(targetClass, contentType))) {
					if (message.hasBody()) {
						HttpInputMessage msgToUse =
								getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
						body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
								((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
						body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
					}
					else {
						body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
					}
					break;
				}
			}
		}
		catch (IOException ex) {

		}

		return body;
	}

这里是获取Heads过程,注释处将获取servletRequest.getCharacterEncoding()拼接在content-type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
	
	@Override
	public HttpHeaders getHeaders() {
		if (this.headers == null) {
			this.headers = new HttpHeaders();
			//获取到所有的请求头
			for (Enumeration<?> names = this.servletRequest.getHeaderNames(); names.hasMoreElements();) {
				String headerName = (String) names.nextElement();
				for (Enumeration<?> headerValues = this.servletRequest.getHeaders(headerName);
						headerValues.hasMoreElements();) {
					String headerValue = (String) headerValues.nextElement();
					this.headers.add(headerName, headerValue);
				}
			}

			try {
                //如果没有字符集,则从request中获取,拼接进去
				if (contentType != null && contentType.getCharset() == null) {
                //获取request上的字符集    
					String requestEncoding = this.servletRequest.getCharacterEncoding();
					if (StringUtils.hasLength(requestEncoding)) {
						Charset charSet = Charset.forName(requestEncoding);
						Map<String, String> params = new LinkedCaseInsensitiveMap<>();
						params.putAll(contentType.getParameters());
						params.put("charset", charSet.toString());
						MediaType mediaType = new MediaType(contentType.getType(), contentType.getSubtype(), params);
						this.headers.setContentType(mediaType);
					}
				}
			}
			catch (InvalidMediaTypeException ex) {
			}

			if (this.headers.getContentLength() < 0) {
				int requestContentLength = this.servletRequest.getContentLength();
				if (requestContentLength != -1) {
					this.headers.setContentLength(requestContentLength);
				}
			}
		}

		return this.headers;
	}

StringmessageConverter读取数据时,会先从content-type中获取,再使用默认的,这里的content-type已经在上面被改写了。所以默认的字符集只有在没有主动调用request.setCharEncoding,也无法从content-type上获取到字符集时才会使用。

1
2
3
4
5
@Override
	protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {
		Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
		return StreamUtils.copyToString(inputMessage.getBody(), charset);
	}

5.总结


1

对于get请求路径上的中文字符,默认是utf-8就可以,一般不会有问题。


2

对于post请求中的form-datax-www-form-urlencoded,参数是由Tomcat解析的,对字符集的优先级是:

request.getEncoding() > Content-Type上的 > context.getRequestCharacterEncoding() > 默认的iso8859-1


3

对于post请求是application/json的,解析操作由Springmvc处理,优先级为:

` Content-Type上的 > request.getEncoding() > StringMessageConverter指定的`


4

Request设置字符集需要在第一次获取参数前设置才可以,所以可以添加一个Filter在最前面,将指定的字符集设置进去。Spring为我们提供了一个CharacterEncodingFilter来做这样的事。


转载请注明出处:https://www.huangchaoyu.com/2020/06/19/java-web请求乱码问题/