1. 概述
简单来说,URL编码 将URL中的特殊字符转换成符合规范的形式,以便网络正确传输和理解。
在这个教程中,我们将重点讲解如何对URL或表单数据进行编码/解码,确保它们符合规范。
2. 分析URL
首先,让我们看看一个基本的统一资源标识符(URI)语法:
scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]
URL编码的第一步是检查其组成部分,并只对相关部分进行编码。
现在看一个URI的例子:
String testUrl =
"http://www.baeldung.com?key1=value+1&key2=value%40%21%242&key3=value%253";
分析URI的一种方法是将字符串表示加载到java.net.URI
类中:
@Test
public void givenURL_whenAnalyze_thenCorrect() throws Exception {
URI uri = new URI(testUrl);
assertThat(uri.getScheme(), is("http"));
assertThat(uri.getHost(), is("www.baeldung.com"));
assertThat(uri.getRawQuery(),
.is("key1=value+1&key2=value%40%21%242&key3=value%253"));
}
URI
类解析URL的字符串表示并通过简单的API(如getXXX
)暴露其部分。
3. 编码URL
在编码URI时,常见的陷阱之一是编码整个URI。通常,我们只需要对URI的查询部分进行编码。
使用URLEncoder
类的encode(data, encodingScheme)
方法来编码数据:
private String encodeValue(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8.toString());
}
@Test
public void givenRequestParam_whenUTF8Scheme_thenEncode() throws Exception {
Map<String, String> requestParams = new HashMap<>();
requestParams.put("key1", "value 1");
requestParams.put("key2", "value@!$2");
requestParams.put("key3", "value%3");
String encodedURL = requestParams.keySet().stream()
.map(key -> key + "=" + encodeValue(requestParams.get(key)))
.collect(joining("&", "http://www.baeldung.com?", ""));
assertThat(testUrl, is(encodedURL));
encode
方法接受两个参数:
data
- 需要转换的字符串encodingScheme
- 字符编码名称
这个encode
方法将字符串转换为application/x-www-form-urlencoded格式。
编码方案会将特殊字符转换成8位的十六进制两位数表示,形式为“%xy”。当我们处理路径参数或动态添加参数时,我们会先对数据进行编码,然后发送到服务器。
注意: 万维网联盟建议使用UTF-8
。如果不这样做可能会引入不兼容性。(参考:https://docs.oracle.com/javase/7/docs/api/java/net/URLEncoder.html)
4. 解码URL
现在使用URLDecoder
的decode
方法来解码先前的URL:
private String decode(String value) {
return URLDecoder.decode(value, StandardCharsets.UTF_8.toString());
}
@Test
public void givenRequestParam_whenUTF8Scheme_thenDecodeRequestParams() {
URI uri = new URI(testUrl);
String scheme = uri.getScheme();
String host = uri.getHost();
String query = uri.getRawQuery();
String decodedQuery = Arrays.stream(query.split("&"))
.map(param -> param.split("=")[0] + "=" + decode(param.split("=")[1]))
.collect(Collectors.joining("&"));
assertEquals(
"http://www.baeldung.com?key1=value 1&key2=value@!$2&key3=value%3",
scheme + "://" + host + "?" + decodedQuery);
}
这里有两个重要点需要注意:
- 在解码之前分析URL
- 编码和解码时使用相同的编码方案
如果先解码再分析,URL的部分可能无法正确解析。如果使用不同的编码方案解码数据,将得到乱码。
5. 编码路径段
我们不能使用URLEncoder
来编码URL的路径段。路径组件指的是代表目录路径的层次结构,或者用于通过“/”分隔的资源定位。
路径段中的保留字符与查询参数值不同。例如,“+”号在路径段中是一个有效的字符,因此不应被编码。
要编码路径段,我们可以使用Spring框架的UriUtils
类。
UriUtils
类提供了encodePath
和encodePathSegment
方法,分别用于编码路径和路径段:
private String encodePath(String path) {
try {
path = UriUtils.encodePath(path, "UTF-8");
} catch (UnsupportedEncodingException e) {
LOGGER.error("Error encoding parameter {}", e.getMessage(), e);
}
return path;
}
@Test
public void givenPathSegment_thenEncodeDecode()
throws UnsupportedEncodingException {
String pathSegment = "/Path 1/Path+2";
String encodedPathSegment = encodePath(pathSegment);
String decodedPathSegment = UriUtils.decode(encodedPathSegment, "UTF-8");
assertEquals("/Path%201/Path+2", encodedPathSegment);
assertEquals("/Path 1/Path+2", decodedPathSegment);
}
在上面的代码片段中,可以看到使用encodePathSegment
方法时,返回了编码后的值,而"+"没有被编码,因为它在路径组件中是一个有效值字符。
让我们在测试URL中添加一个路径变量:
String testUrl
= "/path+1?key1=value+1&key2=value%40%21%242&key3=value%253";
为了组装并验证一个正确编码的URL,我们将第2节的测试稍作修改:
String path = "path+1";
String encodedURL = requestParams.keySet().stream()
.map(k -> k + "=" + encodeValue(requestParams.get(k)))
.collect(joining("&", "/" + encodePath(path) + "?", ""));
assertThat(testUrl, CoreMatchers.is(encodedURL));
6. 总结
在这篇文章中,我们了解了如何正确地编码和解码数据,以便于网络传输和理解。
虽然文章主要关注编码/解码URI查询参数值,但这种方法同样适用于HTML表单参数。
如往常一样,源代码可以在GitHub上找到。