URL 无处不在,不过似乎开发人员并没有真正地理解它们,因为在 Stack Overflow 上经常看到有人在问如何正确的创建一个 URL 。想知道 URL 语法是如何工作的,可以看下兄弟连教育( www.lampbrother.net )总结的这篇文章,非常不错。
本文不会深入介绍 URL 的全部语法,这是我们发布的一个用于正确地创建 URL 的 Java 库。
问题 1 : Java 的 URLEncoder 这个类不仅名字取的很差,而且它的文档上来第一句话就不太对头。
Utility class for HTML form encoding. 你可能正纳闷为什么叫 URLEncoder 呢,看到这行就彻底无语了。
如果你读过兄弟连教育( www.lampbrother.net )的那篇博文,现在你应该明白了,你没法通过这个类将一个 URL 串奇迹般地转化成一个安全,正确编码的 URL 对象,当然如果你没做足功课的话,这里有个小例子可以帮助你理解下。
假设你有个 HTTP 的服务端点,它接受一个查询参数 p , p 的值就是要查找的字符串。如果你搜索"You & I"这个串的话,你第一次创建的搜索的 URL 可能是这样:。这个当然没法工作,因为&是分隔查询参数 name/value 对的分隔符。如果你拿到这个错乱的 URL 串的话,你对它简直束手无策,因为首先你就没法正确的解析它。
那好,我们来使用下 URLEncoder 。 URLEncoder.encode("You & I", "UTF-8")是结果是 You+%26+I 。这个%26 解码之后就是&,而+号在查询串中代表的就是空格,因此这个 URL 是能正常工作的。
现在假设你想使用你的查询串来拼接 URL 路径,而不是放到 URL 参数里面。很明显,是错误的。不幸的是, URLEncoder.encode()的结果也是错的。解码后会得到 /search/You+&+I ,因为+号在 URL 路径中是不会解析成空格的。
URLEncoder 或许能满足你的一些场景。但不幸的是,它这个过于通用的名字使得开发人员很容易误用它。因此最好的方法就是不要使用它,免得后面别的开发人员在你的基础上又使用了别的功能时犯错(除非,你真的是在进行"HTML 表单编码")。
问题 2 : Groovy HttpBuilder 以及 Java 的 URI HTTP Builder 是 Groovy 的一个 HTTP 客户端库。
创建一个普通的 GET 请求非常简单:
new HTTPBuilder.request(Method.GET) { uri.path = "/foo" } 这段代码会发送 GET /foo HTTP/1.1 到服务端(你可以运行 nc -l -p 18080 之后再执行这段代码验证下)。
我们来试一下包含空格的 URL 。
new HTTPBuilder.request(Method.GET) { uri.path = "/foo bar" } 这个发送的是 GET /foo%20bar HTTP/1.1 ,看起来还不错。
现在假设我们的路径中有一段就叫做 foo/bar 。这可不能简单地发送 foo/bar 就完了,因为这会被认为成路径中包含两段, foo 和 bar ,那我们试下 foo%2Fbar 吧(把 /替换成对应的编码)。
new HTTPBuilder.request(Method.GET) { uri.path = '/foo%2Fbar' } 这个发送的则是 GET /foo%252Fbar HTTP/1.1 。这可不太妙。%2F 中的%被重复编码了,这样解码后拿到的路径是 foo%2Fbar 而不是 foo/bar 。这里其实真正要怪的是 java.net.URI ,因为这个 HTTPBuilder 里的 URIBuilder 类用的就是它。
上述代码中的配置闭包中暴露的 uri 属性的类型是 URIBuilder 。如果你通过 uri.path = ...来更新 uri 的 path 属性的话,它最终会调用 URI 的一个构造方法,这个方法对于传入的 path 属性是这么描述的:
如果提供了 path 参数,则将它追加到 URL 后面。 path 里面的字符,只要不是非保留,标点,转义及其它分类(译注:这几个分类在 RFC 2396 中有详细说明)的字符,同时又不是 /或者 @号的,都会进行编码。 这个做法意义不大,因为如果未编码前的文本包含特殊字符的话,它就无法生成一个正确编码的路径分段。换句话说,“我会对这个字符串进行编码,而编码之后它就是正确的”,这当然是个谬论,而 URI 正好是这个谬论的牺牲品。如果字符串已经正确编码了,那就没什么问题,如果不是的话,那就完蛋了,因为这个串没法解析。事实上,文档里说的不会对 /号转义的意思是,它假设 path 串已经正确地编码了(就是说正确地使用 /来分隔路径),同时又还没有正确地编码(除了 /外的其它部分仍然需要进行编码)。
如果 HTTPBuilder 不使用 URI 类的这个存在缺陷的功能就好了,当然了,如果 URI 自己本身没问题的话就更好了。
正确的做法 我们写了这个 url-builder ,它能帮助开发人员方便的拼接各种类型的 URL 。它遵循了篇首那几个参考资料中的编码规范,同时它还提供了流式的 API 。下面这个使用示例几乎可以涵盖所有的使用场景了:
UrlBuilder.forHost("http", "foo.com") .pathSegment("with spaces") .pathSegments("path", "with", "varArgs") .pathSegment("&=?/") .queryParam("fancy + name", "fancy?=value") .matrixParam("matrix", "param?") .fragment("#?=") .toUrlString()
这个例子演示了 URL 各个部分的不同的编码规则,比如说在路径中未编码的&=是允许的,而?/则是需要编码的,但在查询参数中=是需要编码的,但?号则不需要,因为这里已经是查询串的部分了。