【原创】http请求中加号被替换为空格?源码背后的秘密
这是why技术的第**20**篇原创文章
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550eb82e10eff?w=900&h=383&f=png&s=707278)
本周本来是没有时间写技术文章的,为了周更不断,想着去把之前发布在其他平台的一篇原创文章搬过来就行。结果发现,当年我写的那篇文章,离**真相还差着十万八千里。**
而去搜索这个问题时,我的文章是检索结果的第一个。
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550eb864428d5?w=549&h=216&f=png&s=45137)
原文《http请求参数中加号被替换为空格及请求参数被URLDeCode的记录》链接如下:
*https://www.jianshu.com/p/1a30b585c39e*
所以为了避免继续误导读者,就算周末”爆肝”,也得输出此文,不得不发。
**这是我作为程序员的自我修养。**
# 加号变空格
之前写那篇文章的原因是碰到了两个有趣的问题,如下:
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550eb8a3ffe56?w=717&h=345&f=png&s=75662)
首先,我们进行场景复现,搭建项目的过程就不说了,用idea+springboot搭建一个简单的web项目还不是信手拈来的事?
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550eb8d880ca0?w=710&h=691&f=png&s=109240)
正如上面的现象所示:我的入参是**jay+love**,但是后台接收到的是**jay love**,加号变空格了。为什么呢?
# 源码之下无秘密
**本文分析的Tomcat源码版本为:9.0.29.**
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550eb9009740d?w=452&h=64&f=png&s=8113)
通过Debug可以找到**两处关键**的代码:
第一处:
*org.apache.tomcat.util.http.Parameters#processParameters(byte[], int, int, java.nio.charset.Charset) 下图中的290行*
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550eb93801419?w=1190&h=923&f=png&s=627080)
在这个地方**因为有’+’,所以把decodeValue参数设置为true**,表示需要对请求中的value进行decode操作。
decode的具体的源码位置如下,也就是第二处关键代码:
*org.apache.tomcat.util.buf.UDecoder#convert(org.apache.tomcat.util.buf.ByteChunk, boolean)*
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ebb7866c8e?w=1082&h=974&f=png&s=705343)![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ebbaf7a8b9?w=376&h=86&f=png&s=7623)
可以看到,在源码里面有一段代码,是把’+’替换了为了空格,是特意做了这样的特殊处理。
整个方法的解读如下:
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ebba7cd59b?w=925&h=922&f=png&s=613854)
所以我的入参是**jay+love**,但是后台接收到的是**jay love**,加号变空格了。为什么呢?
原因很简单,在源码中有一段代码把’+’替换成了空格,刻意为之。
# 为什么这样做呢?
之前的文章里面我写的是:
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ebc1d69db6?w=699&h=171&f=png&s=36265)
**由于历史原因,那到底是什么历史原因呢?**
我在网上查了一圈,没有找到具体的历史原因,我看到的所有的关于这个问题的文章,要么只是给了解决方案,要么就是上面这一句历史原因,一带而过,含糊其辞。
这里,我就明明白白的告诉你为啥。
经过我长时间的摸排,我找到了很多蛛丝马迹,整理之后,我决定**从JDK的一个”BUG”讲起。**
*对应链接:http://bugs.sun.com/view_bug.do?bug_id=4616184*
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ebd427ebb4?w=1018&h=308&f=png&s=60145)
从提交时间上可以看出,**该问题早在2001年,距今18年前就有人指出来了**,并给JDK上报了BUG,他的描述如下:
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ebd577a2f6?w=943&h=919&f=png&s=135592)
首先,我们先把他的测试代码拿出来跑一下:
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ebe19c7d7e?w=908&h=352&f=png&s=70779)
他为什么说空格encode之后应该是%20呢?
因为他在BUG里面提到了**RFC2396**标准。(RFC就不解释了,你只要知道是业界认证的权威标准就行):
*地址:http://www.ietf.org/rfc/rfc2396.txt*
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ebf15fdf9e?w=525&h=335&f=png&s=24116)
**在RFC2396的第2.4.1节,明确的说了:”%20″是US-ASCII空格字符的转义编码。**
去查询标准的ASCII码你也可以发现确实是这样的:
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ebebffb719?w=785&h=86&f=png&s=6794)
用代码实践一下,证明以上结论:
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ebf1549630?w=809&h=585&f=png&s=109039)
看java.net.URLEncoder#encode(java.lang.String, java.lang.String)的源码也可以直观的看到,**源码里面做了特殊处理:**
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ec02fc2ca0?w=710&h=718&f=png&s=112814)
再看java.net.URLDecoder#decode(java.lang.String, java.lang.String)的源码:
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ec0a3f7130?w=930&h=511&f=png&s=76310)
这里就和前面的**呼应上了**,这处理方式,一模一样呀。所以为什么这样处理,两处地方属于**同宗同源**啊!
而提BUG的那个哥们为什么觉得这是一个BUG呢?
虽然经过试验,’+’和’%20’经过decode都能转化为空格,但是他认为,根据RFC2396来讲,这里只能是’%20′,怎么能变成’+’呢?所以他觉得这是一个BUG。
那我们看看**JDK官方是怎么回复这个问题的呢?**
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ec0b991a2e?w=480&h=258&f=png&s=30366)
官方回复:
*这不是BUG啊,朋友!这个类就是遵循了HTML规范中的规定:如何对 HTML表单中的URLs进行encode。它不打算用于其他用途。
而这样做的原因,是因为包括HTML 4.01第17.13.4节和RFC 1866(已经被W3C HTML推荐标准取代)都是这样规定的。*
对于第一段话,官方的意思我理解是:这个类就是拿来对url进行encode的,不做其他用途。因为你调用了encode编码,那就需要decode解码,我只要保证你解码之后的数据和你encode之前的数据是一样的就行了。你要拿去搞其他事情,我就管不了了。
而为什么这样做呢?是因为规定就是这样的呀,类似于国家标准就是这样的,类似于产品经理提出的需求就是这样的呀。这里官方提出了两个标准,**一个是HTML 4.01,一个是RFC1866**(这个已经被其他的标准取代了,那我们就只看HTML 4.01)。
*HTML4.01是1999年12月24日发布的,在HTML4.0基础上进行微小改进,W3C推荐标准 。
在w3c上找到该标准,地址如下
https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1*
下图圈起来的地方很关键,可以点开放大查看:
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ec14b48acb?w=1873&h=355&f=png&s=458724)
找到**HTML 4.01第17.13.4节**,其中明确指出:当content-type为application/x-www-form-urlencoded时,对names和vaules进行转义,空格用’+’代替。
HTML 4.01第17.13.4节原文如下:
*Control names and values are escaped. Space characters are replaced by `+’*
官方举的虽然是HTML 4.01的例子,但是我翻译了历史文献,发现其实在更早的HTML 3.2规范中就规定了,**HTML 3.2规范在1996年就成为了W3C推荐标准**,其中相关内容如下:
*链接地址:https://www.w3.org/TR/2018/SPSD-html32-20180315/*
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ec1909c784?w=1905&h=481&f=png&s=1531598)
**而application/x-www-form-urlencoded是浏览器默认的content-type。**
在BUG里面提到的**RFC2396标准是1998年8月提出来的**:
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ec245f90e5?w=536&h=186&f=png&s=23639)
而**HTML 3.2规范在1996年就成为了W3C推荐标准。**
所以,我觉得**这就是历史原因!**
**再说一次,在HTML 4.01规范中就明确规定了:当content-type为application/x-www-form-urlencoded时,对names和vaules进行转义,空格用’+’代替。**
没有原因,就是规定!我在查询的过程中发现,其他的编程语言也有这样的问题,因为他们都遵从同样的标准,就有了同样的”历史原因”。
回到前面的这个地方:
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ec401a0e61?w=1082&h=974&f=png&s=705343)
这里解码的时候为什么把’+’转化为空格呢?因为”历史原因”,如果URLs中出现了空格,需要用’+’替换,所以这里解码的时候把’+’转化回了空格。先有了编码的操作,所以才会有解码的操作。
**很多的文章都在说这是’+’的原因,甚至有的文章说’+’的编码应该改为%20。但是其实上面分析过了,有问题的是空格,而不是’+’。**
那为什么我们在做表单提交的时候,也经常写’+’号呀,为什么没有问题呢?
**因为当Html的表单被提交时, 每个表单域都会被Url编码之后才在被发送,**下面的小例子可以佐证:
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ec41b06be1?w=1176&h=764&f=png&s=595664)
# 解决方案
解决方案网上一大堆了,我这里罗列一下吧:
**方案一:修改客户端,将客户端带’+’的参数中的’+’全部替换为’%2B’,如下:**
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ec4340f3b1?w=1176&h=764&f=png&s=595664)
**方案二:修改服务器端,将空格替换为’+’,这种方式只适用于参数中’+’没有空格的情况。如下:**
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ec49217c48?w=818&h=679&f=png&s=103666)
**方案三:修改服务器端,将获取参数的方法由reuqest.getParameter改为request.getQueryString(),然后对得到的字符串进行解析。**
![在这里插入图片描述](https://user-gold-cdn.xitu.io/2019/12/30/16f550ec570a131a?w=519&h=322&f=png&s=57909)
# 最后说一句
正如我文章最开始说的,就算是熬夜爆肝,我也必须得输出这篇文章,因为我最开始的文章不仅写的表面,而且还有一些问题,我得对其进行纠正。
让我突然想起了之前和朋友的一次对话,他问我说:你作为程序员,时刻待命,只要系统一出问题你就立马会响应。你不觉得累吗?
我回答道:说真的,当系统出问题,需要我排查问题的时候,我不觉得累。因为这个系统是我负责的,代码是我自己一行行的写出来的。出现了问题,我得证明我的系统是没有问题的,是不是别人的打开方式不对。但是如果真的是我的代码导致的问题,我会心有愧疚,我也得立即响应,对其负责。
**这是我作为一个程序员的自我修养。**
这篇文章的风格和《这道面试题我真不知道面试官想要的回答是什么》有点相似,全文描述的都是很小的知识点,甚至可以说是冷知识。一句话就能说出表面上的为什么,提炼出一个知识点。
但是我觉得提炼出来的,是一个干瘪瘪的知识点,它不够丰富,没有探索的过程。
而我所展示的是我去寻找这个问题的答案的过程。通过JDK的”BUG”把几个协议串联起来,而且是全世界共同遵循的协议,极具权威性。
才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。
感谢您的阅读,感谢您的关注。
以上。