實驗項目SpringBoot
版本爲2.3.5.RELEASE
如若擔憂其餘版本是否適用本方案,請查看文章 兼容性 章節html
來到這裏的朋友,你必定碰見了中文參數亂碼的問題。前端
你是否有如下症狀:java
server.servlet.encoding.charset=utf-8
、server.servlet.encoding.force=true
仍是會有亂碼server.undertow.urlCharset=utf-8
仍是會有亂碼咱們先來搭建一下現場以便還原事故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; } }
至此搭建完畢,啓動項目。後端
咦,竟然是正常的瀏覽器
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>
請求結果:
咦,竟然也是正常的
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>
請求結果:
這裏是亂碼了
能夠看到,借用瀏覽器的普通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<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");
重啓應用,獲得正確的值,博主還拿了錕斤拷
去測試:
固然除了攔截器,還能夠有以下方法:
HandlerMethodArgumentResolver
OncePerRequestFilter
,參考 CharacterEncodingFilter
的實現。aop
,攔截點能夠有不少,太麻煩,不建議。你們有什麼精巧的辦法和別的想法,歡迎留言。
博主因時間緣由,並無充分測試,只要undertow在參數轉換的時候依然是由byte
強轉爲char
,本方法就會生效。