小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
Spring的RestTemplate大家都不陌生,不熟悉的同学可参考《Springboot -- 用更优雅的方式发HTTP请求(RestTemplate详解)》了解更多RestTemplate的知识。RestTemplate使用起来非常简单方便,但要注意几个关于URLEncode的问题
一、请求的url(字符串类型)会自动URLEncode
测试代码:
RestTemplate restTemplate = new RestTemplate();
restTemplate.getForEntity("https://www.baidu.com?r=编码", String.class);
控制台日志:
源码分析:
通过以下这段代码,将url字符串进行URLEncode
URI expanded = this.getUriTemplateHandler().expand(url, uriVariables);
具体流程:
- 通过url生成DefaultUriBuilderFactory.DefaultUriBuilder对象
new DefaultUriBuilderFactory.DefaultUriBuilder(uriTemplate)
-
调用DefaultUriBuilderFactory.DefaultUriBuilder的build方法URI
2.1 通过UriComponents的实现类HierarchicalUriComponents已分层的形式解析生成HierarchicalUriComponents对象
this.expandInternal(new UriComponents.VarArgsTemplateVariables(uriVariableValues));
protected HierarchicalUriComponents expandInternal(UriTemplateVariables uriVariables) {
Assert.state(!this.encodeState.equals(HierarchicalUriComponents.EncodeState.FULLY_ENCODED), "URI components already encoded, and could not possibly contain '{' or '}'.");
String schemeTo = expandUriComponent(this.getScheme(), uriVariables, this.variableEncoder);
String userInfoTo = expandUriComponent(this.userInfo, uriVariables, this.variableEncoder);
String hostTo = expandUriComponent(this.host, uriVariables, this.variableEncoder);
String portTo = expandUriComponent(this.port, uriVariables, this.variableEncoder);
HierarchicalUriComponents.PathComponent pathTo = this.path.expand(uriVariables, this.variableEncoder);
MultiValueMap<String, String> queryParamsTo = this.expandQueryParams(uriVariables);
String fragmentTo = expandUriComponent(this.getFragment(), uriVariables, this.variableEncoder);
return new HierarchicalUriComponents(schemeTo, fragmentTo, userInfoTo, hostTo, portTo, pathTo, queryParamsTo, this.encodeState, this.variableEncoder);
}
2.2 对已封装好的Uri组件进行编码生成URI对象
this.createUri(uric);
private URI createUri(UriComponents uric) {
if (DefaultUriBuilderFactory.this.encodingMode.equals(DefaultUriBuilderFactory.EncodingMode.URI_COMPONENT)) {
uric = uric.encode();
}
return URI.create(uric.toString());
}
2.3 对Uri各部分进行编码
public HierarchicalUriComponents encode(Charset charset) {
if (this.encodeState.isEncoded()) {
return this;
} else {
String scheme = this.getScheme();
String fragment = this.getFragment();
String schemeTo = scheme != null ? encodeUriComponent(scheme, charset, HierarchicalUriComponents.Type.SCHEME) : null;
String fragmentTo = fragment != null ? encodeUriComponent(fragment, charset, HierarchicalUriComponents.Type.FRAGMENT) : null;
String userInfoTo = this.userInfo != null ? encodeUriComponent(this.userInfo, charset, HierarchicalUriComponents.Type.USER_INFO) : null;
String hostTo = this.host != null ? encodeUriComponent(this.host, charset, this.getHostType()) : null;
BiFunction<String, HierarchicalUriComponents.Type, String> encoder = (s, type) -> {
return encodeUriComponent(s, charset, type);
};
HierarchicalUriComponents.PathComponent pathTo = this.path.encode(encoder);
MultiValueMap<String, String> queryParamsTo = this.encodeQueryParams(encoder);
return new HierarchicalUriComponents(schemeTo, fragmentTo, userInfoTo, hostTo, this.port, pathTo, queryParamsTo, HierarchicalUriComponents.EncodeState.FULLY_ENCODED, (UnaryOperator)null);
}
}
具体编码(核心逻辑:判断字符是否需要编码,如果需要就进行URLEncode):
static String encodeUriComponent(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);
boolean original = true;
byte[] var5 = bytes;
int var6 = bytes.length;
int var7;
for(var7 = 0; var7 < var6; ++var7) {
byte b = var5[var7];
if (!type.isAllowed(b)) {
original = false;
break;
}
}
if (original) {
return source;
} else {
ByteArrayOutputStream baos = new ByteArrayOutputStream(bytes.length);
byte[] var13 = bytes;
var7 = bytes.length;
for(int var14 = 0; var14 < var7; ++var14) {
byte b = var13[var14];
if (type.isAllowed(b)) {
baos.write(b);
} else {
baos.write(37);
char hex1 = Character.toUpperCase(Character.forDigit(b >> 4 & 15, 16));
char hex2 = Character.toUpperCase(Character.forDigit(b & 15, 16));
baos.write(hex1);
baos.write(hex2);
}
}
return StreamUtils.copyToString(baos, charset);
}
}
}
二、RestTemplate的Encode与java.net.URLEncode不完全一致
Spring的RestTemplate会对url进行encode,但它的encode与jdk自带的java.net.URLEncode.encode方法并不完全一致,区别在于:判断字符是否需要encode的逻辑不一样。
示例:
RestTemplate restTemplate = new RestTemplate();
restTemplate.getForEntity("https://www.baidu.com?r=编,码", String.class);
System.out.println(URLEncoder.encode("编,码", "utf-8"));
控制台日志:
16:12:09.548 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET https://www.baidu.com?r=%E7%BC%96,%E7%A0%81
16:12:09.559 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[text/plain, application/json, application/*+json, */*]
16:12:09.745 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
16:12:09.746 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "text/html"
%E7%BC%96%2C%E7%A0%81
对于"编,码"
字符串
Spring的encode编码结果:%E7%BC%96,%E7%A0%81
java.net.URLEncode.encode:%E7%BC%96%2C%E7%A0%81
区别:Spring不会对,
encode;URLEncode会对,
编码(编码结果%2C
)
Spring不编码的字符(对应ASCII码):
// 参考:org.springframework.web.util.HierarchicalUriComponents.Type#QUERY_PARAM
c >= 97 && c <= 122 || c >= 65 && c <= 90 || 33 == c || 36 == c || 39 == c || 40 == c || 41 == c || 42 == c || 43 == c || 44 == c || 59 == c || 58 == c || 64 == c
URLEncode不编码的字符(对应ASCII码):
// 参考:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/src.zip!/java/net/URLEncoder.java:87
for (i = 'a'; i <= 'z'; i++) {
dontNeedEncoding.set(i);
}
for (i = 'A'; i <= 'Z'; i++) {
dontNeedEncoding.set(i);
}
for (i = '0'; i <= '9'; i++) {
dontNeedEncoding.set(i);
}
dontNeedEncoding.set(' '); /* encoding a space to a + is done
* in the encode() method */
dontNeedEncoding.set('-');
dontNeedEncoding.set('_');
dontNeedEncoding.set('.');
dontNeedEncoding.set('*');
Spring的Encode与java.net.URLEncode不完全一致,大部分使用场景没有影响,因为即使未转码,URLDecode也可以解码。但涉及需要签名计算时,就要特别注意了!最近在项目中就踩到这样的坑:
百度开放平台,验签需要对参数进行URLEncode编码再加密生成签名。如果使用RestTemplate发起请求时,参数带有,
字符时,就会验签失败。因为url上的参数URLEncode与生成签名时的URLEncode结果不一致
三、execute方法中的String url
和URI url
的区别
源码:
@Nullable
public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor, Map<String, ?> uriVariables) throws RestClientException {
URI expanded = this.getUriTemplateHandler().expand(url, uriVariables);
return this.doExecute(expanded, method, requestCallback, responseExtractor);
}
@Nullable
public <T> T execute(URI url, HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
return this.doExecute(url, method, requestCallback, responseExtractor);
}
从源码很容易得知,如果参数为String url
,其为对url处理,比如URLEncode等。