一、遇到问题
今天要写一个接口,收到请求后,给第三方接口发送请求,第三方接口会创建一个聊天室,然后返回报文。
碰到一个问题:
使用restTemplate.getForObject()发送请求时,获取的响应报文显示:{"message":"url请求非法!"},无法获取正确的响应报文。
代码如下:
//样例url
String url = "http://10.111.222.333/live";
String cid = "USER_NAME_EXAMPLE";
String signCode = "LIKEPASS123ABC456";
String roomId = "@ABC#1A2B3C";
String tail = "/chatRoom/create_room?cid="+cid+"×tamp="+System.currentTimeMillis()+"&roomId="+URLEncoder.encode(roomId);
String sign = "&sign="+signCode;
String s = MD5Util.computeMD5(tail+sign);
//获得最终的url
url = url + tail + "&md5=" + s;
RestTemplate restTemplate = new RestTemplate();
//发送请求并用String格式获取响应报文
String backStr = restTemplate.getForObject(url, String.class);
//将响应报文转为JSONObject格式
JSONObject backJson = JSONObject.fromObject(backStr);
//打印下请求地址,url
System.out.println(url);
//打印下响应报文,backStr
System.out.println(backStr);
调用接口后,收到的错误的响应报文:
{"message":"url请求非法!","code":"-1"}
二、分析问题
1.首先,第三方的接口没有问题。(虽然不知道什么情况下会返回这种信息)
2.将控制台打印出来的url复制,用Chrome打开是没有问题的,页面也可以看到"创建聊天室成功"的json报文。(本质上就是一个普通的get请求,也不是请求头设置错误)
3.同样的url,在程序中就无法获取正确的响应报文。
4.准备抓包分析。
(1)首先打开抓包工具Fiddler,用chrome访问url,获得正确的包;
(2)在代码中增加配置,使用代理:
System.setProperty("http.proxyHost", "127.0.0.1");
System.setProperty("https.proxyHost", "127.0.0.1");
System.setProperty("http.proxyPort", "8888");
System.setProperty("https.proxyPort", "8888");
(3)打开Fiddler,Tools->Fiddler Options...->Connections,确认代理端口是否为8888(与代码中的要匹配)
(4)调试代码,抓包,与正确的包比较,发现了不同点:
//正确的url
http://10.111.222.333/live/chatRoom/create_room?cid=USER_NAME_EXAMPLE×tamp=1602737280156&roomId=%40ABC%231A2B3C&md5=b0f35all1k3j241l
//错误的url
http://10.111.222.333/live/chatRoom/create_room?cid=USER_NAME_EXAMPLE×tamp=1602737280156&roomId=%2540ABC%25231A2B3C&md5=b0f35all1k3j241l
(5)不同点是roomId的值。
三、明确问题
1.代码中,为了获取md5码,需要先使用URLEncoder.encode(roomId)对roomId转码,然后拼接成url,这本来是没有问题的。
2.然而在使用restTemplate.getForObject(url, String.class)发送请求时,它对url中的【%】又进行了一次encode转码,导致【%】被转为了【%25】,进一步导致实际请求的url错误,第三方接口也就返回了“url请求非法!”的错误信息。
四、解决方法
1.首先想到的解决方法是,既然restTemplate在发送请求时会对url进行encode转码,那么在拼接url时自己先不转码,就不会有问题了;然而这是一个坑:
//代码同上,省略
......
//使用tail2,不encode,进行url拼接
String tail2 = "/chatRoom/create_room?cid="+cid+"×tamp="+System.currentTimeMillis()+"&roomId="+roomId;
//获得最终的url
url = url + tail2 + "&md5=" + s;
通过抓包发现,实际请求的url还是有问题:
//控制台打印的url,roomId没有encode
http://10.111.222.333/live/chatRoom/create_room?cid=USER_NAME_EXAMPLE×tamp=1602737280156&roomId=@ABC#1A2B3C&md5=b0f35all1k3j241l
//我们认为的正确url,restTemplate发送请求时会对url进行encode,将@和#转码了
http://10.111.222.333/live/chatRoom/create_room?cid=USER_NAME_EXAMPLE×tamp=1602737280156&roomId=%40ABC%231A2B3C&md5=b0f35all1k3j241l
//实际抓包得到的url,还是请求了一个错误的路径
http://10.111.222.333/live/chatRoom/create_room?cid=USER_NAME_EXAMPLE×tamp=1602737280156&roomId=@ABC
可以看到,如果我们自己不对url进行encode的话,restTemplate进行处理时会有问题,丢失了【#】之后的字段,并且没有对【@】进行encode。
经过多次测试,restTemplate会将url中的【%】转为【%25】,其它特殊字符如【!@#】等,则不会转码。
如果我们自己对url进行了encode,此时特殊符号被转为了【%+数字或字母】的形式;然而,restTemplate进行处理时还会将【%】转为【%25】,实际请求的url还是错误的。
2.踩过坑之后,下面是正确的解决方式:
//上方代码同上,可以执行自己的encode
......
URI uriObj = URI.create(url);
RestTemplate restTemplate = new RestTemplate();
//之前是传入String类型的url
//现在改为URI类型的uriObj,这样restTemplate就不会进行encode了。
String backStr = restTemplate.getForObject(uriObj, String.class);
五、总结
使用restTemplate发送请求时,如果传入String类型的url,则这个url中的【%】会被转为【%25】;
如果url中不存在特殊符号,则没有问题;
如果url存在特殊符号且自己没有encode,那么restTemplate执行后,实际请求的url可能会有问题(会丢失#后面的内容等,url机制);
如果url中存在特殊符号且自己执行过encode,那么restTemplate执行后,url会被再次encode,导致实际请求的url出错(%被转码)。
因此,如果url中存在特殊符号,可以在自己执行encode后,转为URI对象,再使用restTemplate发送请求,就避免了【%】被转码的问题。
<补充:URLEncoder.encode()是将字符转为URL可以使用的编码的方法,一般来说,URL只能使用英文字母、阿拉伯数字和某些标点符号,不能使用其他文字和符号。>