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-data
和x-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
来做这样的事。