关于SpringBoot项目使用undertow容器的中文参数乱码问题的真正解决方案

实验项目 SpringBoot 版本为 2.3.5.RELEASE

如若担忧其余版本是否适用本方案,请查看文章 兼容性 章节html

1、概述

来到这里的朋友,你必定碰见了中文参数乱码的问题。前端

你是否有如下症状:java

  • 项目已设置了 server.servlet.encoding.charset=utf-8server.servlet.encoding.force=true 仍是会有乱码
  • 项目在上条配置的基础上加上了 server.undertow.urlCharset=utf-8 仍是会有乱码
  • 项目在上述配置下有的接口正常可是有的接口乱码

2、事故现场搭建

咱们先来搭建一下现场以便还原事故web

pom.xml 配置以下便可:ajax

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <!-- 省略部分项目属性 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.5.RELEASE</version>
    </parent>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>
    </dependencies>

</project>

springboot启动类:spring

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放到启动类同目录或者下一级目录下,这个功能是返回给定的姓名apache

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;
    }
}

至此搭建完毕,启动项目。后端

3、事故还原

一、普通get请求

地址栏普通get请求
咦,竟然是正常的浏览器

二、普通post请求

post请求html页面代码tomcat

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
        <form method="post" action="http://127.0.0.1:8080/a">
            <input type="text" name="name" value="张三" />
            <input type="submit" />
        </form>
    </body>
</html>

请求结果:
post请求正常返回
咦,竟然也是正常的

三、ajax请求

ajax请求源码

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
        <script>
            var xhr = new XMLHttpRequest();
             xhr.open('post', 'http://127.0.0.1:8080/a' );
             xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
            //发送请求参数
            xhr.send('name=张三');
            xhr.onreadystatechange = function () {
                if (xhr.readyState == 4 && xhr.status == 200) {
                    alert(xhr.responseText);
                  } 
            };
        </script>
    </body>
</html>

请求结果:
ajax请求乱码
这里是乱码了

4、事故分析之参数传递

能够看到,借用浏览器的普通get\post请求,都能返回正常无乱码结果。

也许你的浏览器返回的是乱码结果,不过这是合理的。

这里说一下为何能返回正常结果:得益于如今浏览器的智能行为,它对你的参数进行了隐式的URL编码转换。

F12打开浏览器控制台,看get的原报文:
浏览器控制台get请求原始报文
蓝色选中Method上方,是请求的url,能够看到参数 name=张三已经被隐式地转码了。同理,post也是,上述post请求浏览器控制台最后一行已经很清晰地显示了转码后的参数。

这一点,对于新手或者不熟悉前端知识的后端开发人员来讲,很容易让人解决乱码的时候无从下手。

这也就明白了,为何ajax请求返回的,是乱码,由于它的参数没有获得浏览器的URL编码转换。这是咱们指望的,也就是上述说的乱码结果是合理的。

也许有些人会说,那我只要在ajax请求里对参数转码就行了,这里给出一个建议:

尽可能对全部参数进行URL转码,除非你很清楚它只有字母和数字

浏览器也是人创造的,万一哪一天,它再也不偷偷给你转码了呢?

5、事故分析之参数接收

咱们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官方认为这不是个错误,拒绝修复。泱泱大国,遭受歧视,努力奋斗吧骚年,让我大中华民族在科技界再也不遭受忽略、排挤、打击的日子早点到来。

就没有别的办法了吗?

6、解决办法

办法仍是有的。能够看到,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<String> 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");

重启应用,获得正确的值,博主还拿了锟斤拷去测试:

锟斤拷被正确输出

7、其余办法

固然除了拦截器,还能够有以下方法:

  • 添加参数解析器 HandlerMethodArgumentResolver
  • 添加过滤器 ,能够考虑继承 OncePerRequestFilter ,参考 CharacterEncodingFilter 的实现。
  • 添加 aop,拦截点能够有不少,太麻烦,不建议。

你们有什么精巧的办法和别的想法,欢迎留言。

8、兼容性

博主因时间缘由,并无充分测试,只要undertow在参数转换的时候依然是由byte强转为char,本方法就会生效。

相关文章
相关标签/搜索