跟进过程与结果在评论中
0. 起因
正常情况下 url 只会出现英文字母、数字和标点符号,特殊字符会在请求前进行 encode 操作,转化成合法的 url。 例如我们用浏览器在百度上搜索 +=
时,浏览器实际上访问的是 https://www.baidu.com/s?wd=%2B%3D
。
encode 操作其实是将需要转码的字符转为 16 进制,然后从右到左,取 4 位(不足 4 位直接处理),每 2 位做一位,前面加上 % ,编码成 %XY 格式。
常见特殊字符及编码后值如下:
字符 | 编码 |
---|---|
! | %21 |
# | %23 |
$ | %24 |
% | %25 |
+ | %2B |
@ | %40 |
: | %3A |
= | %3D |
? | %3F |
1. 经过
在最近的开发中恰好用到了 HTTP 的 GET 方式请求,并且参数中涉及到特殊字符。
在通信过程中发现,第三方服务收到的数据,与我发出的不一致。例如我发出的数据是 a+b=
,第三方收到的却是 a b=
,这就变得有意思了。因为 ‘+’ 和 ‘=’ 都是特殊字符,为什么一个可以正常收到,另一个却不行。
刚开始我们怀疑是日志打印的问题,可是在我开发环境上也能重现这个问题。然后我们开始怀疑是我发出去的请求没有进行 UrlEncode 处理,可是 ‘=’ 却可以被正常接收和处理,经过猜测和跟代码,最后我们终于找到了问题的原因。
找问题的过程如下:
因为我们使用了 Spring 的 RestTemplate 作为 http 的 client ,所以从 RestTemplate 入手。
具体的跟踪思路和源码如下:
- RestTemplate 中会有一个 uriTemplateHandler 来处理 uri。
private UriTemplateHandler uriTemplateHandler = new DefaultUriBuilderFactory();
@Override
@Nullable
public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback,
@Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {
URI expanded = getUriTemplateHandler().expand(url, uriVariables);
return doExecute(expanded, method, requestCallback, responseExtractor);
}
- DefaultUriBuilderFactory 会使用 UriComponentsBuilder 来实例化自己。也就是说
/**
* Default constructor without a base URI.
* <p>The target address must be specified on each UriBuilder.
*/
public DefaultUriBuilderFactory() {
this(UriComponentsBuilder.newInstance());
}
/**
* Variant of {@link #DefaultUriBuilderFactory(String)} with a
* {@code UriComponentsBuilder}.
*/
public DefaultUriBuilderFactory(UriComponentsBuilder baseUri) {
Assert.notNull(baseUri, "'baseUri' is required");
this.baseUri = baseUri;
}
- 而 UriComponentsBuilder 的 encode 处理默认会通过 HierarchicalUriComponents 完成
/**
* Encode all URI components using their specific encoding rules and return
* the result as a new {@code UriComponents} instance.
* @param charset the encoding of the values
* @return the encoded URI components
*/
@Override
public HierarchicalUriComponents encode(Charset charset) {
if (this.encoded) {
return this;
}
String scheme = getScheme();
String fragment = getFragment();
String schemeTo = (scheme != null ? encodeUriComponent(scheme, charset, Type.SCHEME)