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方法接受两个参数:

  1. data - 需要转换的字符串
  2. 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

现在使用URLDecoderdecode方法来解码先前的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类提供了encodePathencodePathSegment方法,分别用于编码路径和路径段:

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上找到。