URLEncoder无法转换空格字符

245

我期望

System.out.println(java.net.URLEncoder.encode("Hello World", "UTF-8"));

输出应该是:

Hello%20World

(20是空格的ASCII十六进制码)

然而我得到的是:

Hello+World

我使用的方法有误吗?应该使用什么正确的方法?


3
这个类的名称确实很令人困惑,许多人错误地使用它。然而他们没有注意到这一点,因为应用URLDecoder时,原始值会被恢复,所以对他们来说,+或%20并不重要。 - irreputable
19个回答

258

这个方法表现正常。 URLEncoder 实现了用于在 HTML 表单中编码 URL 的 HTML 规范。

来自javadocs:

该类包含将字符串转换为应用程序/x-www-form-urlencoded MIME 格式的静态方法。

以及来自HTML 规范:

application/x-www-form-urlencoded

使用此内容类型提交的表单必须进行如下编码:

  1. 控件名称和值需要进行转义。空格字符被替换为 `+'。

您需要进行替换,例如:

System.out.println(java.net.URLEncoder.encode("Hello World", "UTF-8").replace("+", "%20"));

23
确实,这是一个回答,而不是替代方案。有没有一个Java库或函数可以执行这个任务? - 0x12
28
@congliu,那是不正确的 - 你可能在想replaceAll(),它使用正则表达式 - replace() 是简单的字符序列替换。 - CupawnTae
15
是的@congliu,正确的方法是:URLEncoder.encode("Myurl", "utf-8").replaceAll("\+", "%20"); - eento
10
因为这种短视的解决方案是危险的,所以被踩了。这不仅仅涉及到空格字符,还要参考有关URL编码的RFC 3986 - pyb
18
@ClintEastwood 这个回答鼓励使用java.net.URLEncoder,但它并不能完成最初的要求。因此,这个回答建议在其基础上使用replace()进行修补。为什么不呢?因为这种解决方案容易出现漏洞,并可能导致20个其他类似但字符不同的问题。这就是为什么我说这是短视的原因。 - pyb
显示剩余11条评论

76
在URL中,空格被编码为%20,在表单提交的数据中(内容类型为application/x-www-form-urlencoded),则被编码为+。你需要使用前者。
使用Guava
dependencies {
     compile 'com.google.guava:guava:23.0'
     // or, for Android:
     compile 'com.google.guava:guava:23.0-android'
}
您可以使用UrlEscapers
String encodedString = UrlEscapers.urlFragmentEscaper().escape(inputString);

不要使用String.replace,这只会对空格进行编码。相反,请使用一个库。


它也适用于Android,com.google.guava:guava:22.0-rc1-android。 - Bevor
1
@Bevor rc1 表示第一候选版本,即尚未获得正式发布批准的版本。如果可以的话,请选择一个没有快照、alpha、beta、rc等标识的版本,因为它们已知会包含错误。 - pyb
1
@pyb 谢谢,但我会在项目完成后更新库。这意味着我不会在没有最终版本的情况下进入生产环境。而且这还需要很多周的时间,所以我猜到时候应该会有最终版本了。 - Bevor
1
不幸的是,Guava没有提供解码器,不像Apache的URLCodec - Benny Bottema

28

这个类执行的是application/x-www-form-urlencoded类型的编码,而不是百分比编码,因此用+替换是一种正确的行为。

根据 javadoc:

在对字符串进行编码时,请遵循以下规则:

  • 字母数字字符 "a" 到 "z","A" 到 "Z" 和 "0" 到 "9" 保持不变。
  • 特殊字符 "."、"-"、"*" 和 "_" 保持不变。
  • 空格字符 " " 转换为加号 "+"。
  • 所有其他字符都不安全,并且首先使用某些编码方案将其转换为一个或多个字节。然后,每个字节由三个字符的字符串 "%xy" 表示,其中 xy 是字节的两位十六进制表示法。建议使用 UTF-8 编码方案。但是,出于兼容性原因,如果没有指定编码,则使用平台的默认编码。

@axtavt 很好的解释。但我还有一些问题。在 url 中,空格应该被解释为 %20。所以我们需要做 url.replaceAll("\\+", "%20") 吗?如果是 JavaScript,我们不应该使用 escape 函数。而是应该使用 encodeURIencodeURIComponent。这就是我想的。 - Alston
2
@Stallman 这是Java,不是JavaScript。这两种语言完全不同。 - Charles Wood

25

编码查询参数

org.apache.commons.httpclient.util.URIUtil
    URIUtil.encodeQuery(input);

或者,如果您想在URI中转义字符

public static String escapeURIPathParam(String input) {
  StringBuilder resultStr = new StringBuilder();
  for (char ch : input.toCharArray()) {
   if (isUnsafe(ch)) {
    resultStr.append('%');
    resultStr.append(toHex(ch / 16));
    resultStr.append(toHex(ch % 16));
   } else{
    resultStr.append(ch);
   }
  }
  return resultStr.toString();
 }

 private static char toHex(int ch) {
  return (char) (ch < 10 ? '0' + ch : 'A' + ch - 10);
 }

 private static boolean isUnsafe(char ch) {
  if (ch > 128 || ch < 0)
   return true;
  return " %$&+,/:;=?@<>#%".indexOf(ch) >= 0;
 }

5
使用 org.apache.commons.httpclient.util.URIUtil 似乎是解决该问题最有效的方法! - Stéphane Ammar
1
URIUtil在当前版本中似乎已经消失了,有什么替代品吗? - wutzebaer

11

Hello+World 是浏览器在进行 GET 请求时对表单数据 (application/x-www-form-urlencoded) 进行编码的方式,这也是 URI 查询部分的普遍接受形式。

http://host/path/?message=Hello+World

如果您将此请求发送给Java servlet,则servlet会正确解码参数值。通常情况下,只有在编码不匹配时才会出现问题。
严格来说,在HTTP或URI规范中,并没有要求查询部分使用application/x-www-form-urlencoded键值对进行编码;查询部分只需要以Web服务器接受的形式呈现即可。实际上,这很少会成为问题。
通常情况下,使用此编码方式来编码URI的其他部分(例如路径)是不正确的。在这种情况下,您应该使用RFC 3986中描述的编码方案。
http://host/Hello%20World

更多这里


那么,人们如何正确地编码字符串?我在SO的这篇完整帖子中找不到任何有效的解决方案... - Zordid

7

如果您想对URI路径组件进行编码,也可以使用标准JDK函数,例如:

public static String encodeURLPathComponent(String path) {
    try {
        return new URI(null, null, path, null).toASCIIString();
    } catch (URISyntaxException e) {
        // do some error handling
    }
    return "";
}

URI类也可以用于编码URI的不同部分或整个URI。

更新:我刚意识到,如果路径中有冒号和斜杠之前的部分不是有效的URI方案,则此方法无法正常工作。


2
谢谢!这对我有用。是的,有点绕,但比为了这一个功能将Guava引入我的小项目要好。 - Jonathan Fuerth
同意。谢谢你提供这个。 - Michael K

6

5

虽然不是一行代码,但你可以使用以下代码:

URL url = new URL("https://some-host.net/dav/files/selling_Rosetta Stone Case Study.png.aes");
URI uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef());
System.out.println(uri.toString());

这将会给你一个输出结果:
https://some-host.net/dav/files/selling_Rosetta%20Stone%20Case%20Study.png.aes

这个可以。而且我能够从URI对象中获取URL对象,因为我需要从它获取输入流。 我是这样做的:uri.toURL().openStream(); - Oliver

4

虽然很老,但是回答很快:

Spring提供了UriUtils - 使用它可以指定如何对URI进行编码以及与之相关的部分,例如:

encodePathSegment
encodePort
encodeFragment
encodeUriVariables
....

我使用它们是因为我们已经在使用Spring,即不需要额外的库!


1
在Spring中还有其他进行URL编码的方法吗?我问这个问题是因为当我使用getForObjectRestTemplate的一部分)进行测试请求时,它输出的URL会保留未编码的逗号,但是UriUtils.encode(...)会对逗号进行编码,这意味着如果我使用UriUtils.encode的输出,则我的MockRestServiceServer无法匹配路径。 - IpsRich
我认为这个回答解决了我的问题:https://dev59.com/ZmEi5IYBdhLWcg3w2fYm#20885702 - IpsRich

4
其他答案要么提供手动字符串替换,URLEncoder 实际上编码了 HTML 格式,Apache 的 abandoned URIUtil,或使用 Guava 的 UrlEscapers。最后一个很好,但它不提供解码器。
Apache Commons Lang 提供了 URLCodec,它根据 URL 格式 rfc3986 进行编码和解码。
String encoded = new URLCodec().encode(str);
String decoded = new URLCodec().decode(str);

如果您已经在使用Spring,您也可以选择使用UriUtils类。

9
URLCodec 不是一个好的解决方案,因为它会将空格编码为加号,但问题要求将空格编码为 %20。 - davidwebster48
2
Spring的UriUtil.encodeQuery(用于查询字符串)对我很有用。 - Raf
这不是正确的答案。它再次将空格编码为+! - Zordid

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接