Spring RestTemplate发送请求时 自动对参数进行urlencode的问题
我们用Java开发项目时,发送请求都是用的RestTemplate。最近和其他部门合作时,我们需要请求他们的一个http接口。两边协议都确定好后,发现联调不通。后来发现是我们这边发出的请求,到达对方那边时,他们接收到的是经过了urlencode后的结果,通过wireshark抓包也看到确实发出的请求是被urlencode的。我们这边的进程,并没有显式调用urlencode相关的方法,因此猜测是Re
我们用Java开发项目时,发送请求都是用的RestTemplate。最近和其他部门合作时,我们需要请求他们的一个http接口。两边协议都确定好后,发现联调不通。后来发现是我们这边发出的请求,到达对方那边时,他们接收到的是经过了urlencode后的结果,通过wireshark抓包也看到确实发出的请求是被urlencode的。
我们这边的进程,并没有显式调用urlencode相关的方法,因此猜测是RestTemplate自动给我们进行了urlencode。网上查找资料,发现这两篇文章讲得很详细:
https://blog.csdn.net/Petershusheng/article/details/54236816
RestTemplate确实在底层自动给我们进行了urlencode,不过我们也可以通过UriComponentsBuilder来构建URI对象,手动选择不进行urlencode,具体操作方式可以参考这个文章:
https://blog.csdn.net/blueheart20/article/details/80916517
riComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl("http://xxx.com/image-checker/train_mean.txt").queryParam("Expires", "3678172563").queryParam("Signature", "2FqOFfzePCjESlKMqiGc9V8C9Es%3D");
URI uri = builder.build(true).toUri();
不过一般接口调用,都应该进行urlencode,避免一些特殊字符在传递过程中出现问题。因此回到我们这个应用,最终还是联系接口提供方,在接收请求时,对所有参数都进行了urldecode。
RestTemplate中encode请求参数的原理
我的请求参数里面,有个sign参数,其值含有一些特殊符号,比如斜杠(/)和等号(=),经过RestTemplate处理并发送请求后,我抓包发现请求中的斜杠没有被urlencode,但是等号却被urlencode了,这是为什么呢?RestTemplate的urlencode难道不是通用的,而是自定义的么?
下午花了2个小时,通过IDE断点跟踪调试源码,终于弄清楚原因了。
原因
在HierarchicalUriComponents的大约234行,有个encodeUriComponent方法,这个方法就是RestTemplate用来对url中的参数进行encode处理的逻辑:
static String (String source, Charset charset, HierarchicalUriComponents.Type type) {
if (!StringUtils.hasLength(source)) {
return source;
} else {
Assert.notNull(charset, "Charset must not be null");
Assert.notNull(type, "Type must not be null");
byte[] bytes = source.getBytes(charset);
ByteArrayOutputStream bos = new ByteArrayOutputStream(bytes.length);
boolean changed = false;
byte[] var6 = bytes;
int var7 = bytes.length;
for(int var8 = 0; var8 < var7; ++var8) {
byte b = var6[var8];
if (b < 0) {
b = (byte)(b + 256);
}
/*
* 这个type规定了哪些字符需要被encode转义
*/
if (type.isAllowed(b)) {
bos.write(b);
} else {
bos.write(37);
char hex1 = Character.toUpperCase(Character.forDigit(b >> 4 & 15, 16));
char hex2 = Character.toUpperCase(Character.forDigit(b & 15, 16));
bos.write(hex1);
bos.write(hex2);
changed = true;
}
}
return changed ? new String(bos.toByteArray(), charset) : source;
}
}
从代码可以看到,encode的过程,是先将参数转为byte数组,然后逐个byte进行检查,如果发现某个byte不在url参数允许的范围内,则对其进行encode操作。
然后我们找到上面的HierarchicalUriComponents.Type.QUERY_PARAM的代码,大约在HierarchicalUriComponents的811行:
QUERY_PARAM {
public boolean isAllowed(int c) {
// 61是=,31是&
if (61 != c && 38 != c) {
// 47是/,63是?
return this.isPchar(c) || 47 == c || 63 == c;
} else {
return false;
}
}
}
从上面的逻辑可以看到,如果字符是斜杠(ASCII码等于47),那么进程会判断这个字符是在url参数中被允许的,则不进行encode了;而等号(ASCII码为61)被其判断为是一个不被允许的url字符,因此就会被encode。
至此,原因终于找到了。
解决方案
解决方案也比较简单,因为我们可以自行选择要不要让RestTemplate对我们的参数进行urlencode,所以我们可以先自己手动将url进行encode,然后发送请求时,选择不让RestTemplate自动做encode即可。
使用UriComponentBuilder
public UriComponents build()
Build a
UriComponents
instance from the various components contained in this builder.Returns:
the URI components
public UriComponents build(boolean encoded)
Variant of
build()
to create aUriComponents
instance when components are already fully encoded. This is useful for example if the builder was created viafromUri(URI)
.Parameters:
encoded
- whether the components in this builder are already encodedReturns:
the URI components
这是处理参数的方法:
/**
* 将Key-Value格式的Map参数,转换为url参数,类似key1=value1&key2=value2这样
* 做了2个特殊的操作:
* 1.对参数名进行了排序
* 2.对value进行urlencode处理
* @param mapper
* @return
*/
public static String mapToUrlString(Map<String, Object> mapper, boolean encode) {
String urlString = "";
TreeMap<String,Object> map =new TreeMap<>();
map.putAll(mapper);
for (Map.Entry<String,Object> entity : map.entrySet()) {
String value = "";
try {
value = encode ? URLEncoder.encode((String)entity.getValue(), "UTF-8") : (String)entity.getValue();
} catch (UnsupportedEncodingException e) {
logger.error("获取参数[" + entity.getKey() + "]失败:" + e.getMessage());
}
urlString += entity.getKey() + "=" + value + "&";
}
return urlString.substring(0, urlString.length() - 1);
}
这是发送请求的代码:
String requestUrl = "http://www.10jqka.com.cn/"
Map<String, Object> params = new HashMap<>(){
{
put("name", "zhangsan");
put("nickname", "可 / 口 / 可 / 乐");
}
};
HttpMethod httpMethod = this.request.getHttpMethod();
RestTemplate client = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
HttpEntity<String> entity = new HttpEntity<>(headers);
// 将Map格式的参数转为字符串,并且做urlencode
String urlParams = mapToUrlString(params, true);
requestUrl += "?" + urlParams;
// 通过UriComponentsBuilder创建URI对象,这样RestTemplate不会自动进行urlencode
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(requestUrl);
URI uri = uriComponentsBuilder.build(true).toUri();
String resultContent = client.exchange(uri, HttpMethod.GET, entity, String.class).getBody();
参考 https://stackoverflow.com/questions/28182836/resttemplate-to-not-escape-url
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)