实验项目SpringBoot
版本为2.3.5.RELEASE
如若担心其他版本是否适用本方案,请查看文章 兼容性 章节
一、概述
来到这里的朋友,你一定遇见了中文参数乱码的问题。
你是否有以下症状:
- 项目已设置了
server.servlet.encoding.charset=utf-8
、server.servlet.encoding.force=true
还是会有乱码 - 项目在上条配置的基础上加上了
server.undertow.urlCharset=utf-8
还是会有乱码 - 项目在上述配置下有的接口正常但是有的接口乱码
二、事故现场搭建
我们先来搭建一下现场以便还原事故
pom.xml 配置如下即可:
org.springframework.boot
spring-boot-starter-parent
2.3.5.RELEASE
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-tomcat
org.springframework.boot
spring-boot-starter-undertow
springboot启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class);
}
}
请求controller,注意启动类没有添加@ComponentScan
注解,要把controller放到启动类同目录或者下一级目录下,这个功能是返回给定的姓名
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
public class DemoController {
@RequestMapping("/a")
@CrossOrigin
public String echoName(HttpServletRequest request) {
String name = request.getParameter("name");
System.out.println(name); //打印一下,debug的话可以不用打印也能看到
return name;
}
}
至此搭建完毕,启动项目。
三、事故还原
1、普通get请求
咦,居然是正常的
2、普通post请求
post请求html页面代码
请求结果:
咦,居然也是正常的
3、ajax请求
ajax请求源码
请求结果:
这里是乱码了
四、事故分析之参数传递
可以看到,借用浏览器的普通get\post请求,都能返回正常无乱码结果。
也许你的浏览器返回的是乱码结果,不过这是合理的。
这里说一下为什么能返回正常结果:得益于现在浏览器的智能行为,它对你的参数进行了隐式的URL编码转换。
F12打开浏览器控制台,看get的原报文:
蓝色选中Method上方,是请求的url,可以看到参数 name=张三
已经被隐式地转码了。同理,post也是,上述post请求浏览器控制台最后一行已经很清晰地显示了转码后的参数。
这一点,对于新手或者不熟悉前端知识的后端开发人员来说,很容易让人解决乱码的时候无从下手。
这也就明白了,为什么ajax请求返回的,是乱码,因为它的参数没有得到浏览器的URL编码转换。这是我们期望的,也就是上述说的乱码结果是合理的。
也许有些人会说,那我只要在ajax请求里对参数转码就好了,这里给出一个建议:
尽量对所有参数进行URL转码,除非你很清楚它只有字母和数字
浏览器也是人创造的,万一哪一天,它不再偷偷给你转码了呢?
五、事故分析之参数接收
我们debug去查看应用后台的参数接受情况,参数接受代码这样:
String name = request.getParameter("name");
debug得知,当request中不存在key为name
的参数时候,会从容器中获取字节,然后进行form表单数据的转换。
其中undertow对http请求字节的转换处理,在io.undertow.server.handlers.form.FormEncodedDataDefinition.doParse()
方法中:
private void doParse(final StreamSourceChannel channel) throws IOException {
//省略部分代码
final ByteBuffer buffer = pooled.getBuffer();
//省略部分代码
byte n = buffer.get();
//省略部分代码
builder.append((char) n); //关键操作,对字节直接强转为char,这也就是接受到参数为乱码的原因
//省略部分代码
addPair(name, builder.toString()); //把转换后的参数存储起来,最终会存放到request中
//省略部分代码
}
由上述代码得知,undertow对我们的参数直接进行了强制char型转换,而不是由字节转到字符串,导致request中获取到的参数为强转后乱码的原因。而且undertow官方认为这不是个错误,拒绝修复。泱泱大国,遭受歧视,努力奋斗吧骚年,让我大中华民族在科技界不再遭受忽略、排挤、打击的日子早点到来。
就没有别的办法了吗?
六、解决办法
办法还是有的。可以看到,byte直接转成了char,没有中间操作,不存在高低补位的情况,数据精度并没有丢失,我们再转换回来,即可得到原始的byte字节,然后在转换成字符串,这才是我们想要的。
有如下验证:
public class DecoderTest {
public static void main(String[] args) {
String name = "¥ᄐᅠ¦ᄌノ";
char[] chars = name.toCharArray();
byte[] bytes = new byte[chars.length];
for(int i = 0; i < chars.length; i++){
bytes[i] = (byte) chars[i];
}
System.out.println(new String(bytes));
}
}
控制台输出结果为:张三
想法可行,那么,我们只要添加拦截器,在业务功能获取参数前,反转后存放到request中,就可以了。
添加如下拦截器:
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;
public class UndertowRevertInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Enumeration parameterNames = request.getParameterNames();
while (parameterNames.hasMoreElements()){
String name = parameterNames.nextElement();
String value = request.getParameter(name);
value = revert(value);
//注意不要和原有key重复,我不是教你写bug,只是提供一种思路。
request.setAttribute(name,value);
}
return true;
}
private String revert(String s){
char[] chars = s.toCharArray();
byte[] bytes = new byte[chars.length];
for(int i = 0; i < chars.length; i++){
bytes[i] = (byte) chars[i];
}
//如系统非使用UTF-8编码,请替换为带有编码格式的构造函数
return new String(bytes);
}
}
注意是放到了 attribute
里面,request
不提供setParameter
方法,想想也是合理的,http单次请求本来就是单向发送到后端的,setParameter
做什么?
把拦截器注入到Spring当中:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注意把参数转换拦截器放到第一位,如果有多个拦截器,在其下面添加
registry.addInterceptor(new UndertowRevertInterceptor());
}
}
controller里面获取参数相应替换成:
String name = (String) request.getAttribute("name");
重启应用,得到正确的值,博主还拿了锟斤拷
去测试:
七、其他办法
当然除了拦截器,还可以有如下方法:
- 添加参数解析器
HandlerMethodArgumentResolver
- 添加过滤器 ,可以考虑继承
OncePerRequestFilter
,参考CharacterEncodingFilter
的实现。 - 添加
aop
,拦截点可以有很多,太麻烦,不建议。
大家有什么精巧的办法和别的想法,欢迎留言。
八、兼容性
博主因时间原因,并没有充分测试,只要undertow在参数转换的时候依然是由byte
强转为char
,本方法就会生效。