關於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,本方法就會生效。

相關文章
相關標籤/搜索