当相对URI包含空路径时,Java的URI.resolve是否与RFC 3986不兼容?

我相信Java的URI.resolve方法的定义和实现与RFC 3986第5.2.2节不兼容。 我知道Java API定义了该方法的工作方式,如果它现在被更改,它会破坏现有应用程序,但我的问题是: 任何人都可以确认我的理解这个方法与RFC 3986不兼容吗?

我正在使用这个问题中的示例: java.net.URI仅针对查询字符串进行解析 ,我将在此处复制:


我正在尝试使用JDK java.net.URI构建URI。 我想附加一个绝对URI对象,一个查询(在String中)。 例如:

URI base = new URI("http://example.com/something/more/long"); String queryString = "query=http://local:282/rand&action=aaaa"; URI query = new URI(null, null, null, queryString, null); URI result = base.resolve(query); 

理论(或我认为)是决心应该回归:

 http://example.com/something/more/long?query=http://local:282/rand&action=aaaa 

但我得到的是:

 http://example.com/something/more/?query=http://local:282/rand&action=aaaa 

我对RFC 3986第5.2.2节的理解是,如果相对URI的路径为空,那么将使用基URI的整个路径:

  if (R.path == "") then T.path = Base.path; if defined(R.query) then T.query = R.query; else T.query = Base.query; endif; 

并且仅当指定了路径时才是要与基本路径合并的相对路径:

  else if (R.path starts-with "/") then T.path = remove_dot_segments(R.path); else T.path = merge(Base.path, R.path); T.path = remove_dot_segments(T.path); endif; T.query = R.query; endif; 

但是Java实现总是进行合并,即使路径是空的:

  String cp = (child.path == null) ? "" : child.path; if ((cp.length() > 0) && (cp.charAt(0) == '/')) { // 5.2 (5): Child path is absolute ru.path = child.path; } else { // 5.2 (6): Resolve relative path ru.path = resolvePath(base.path, cp, base.isAbsolute()); } 

如果我的读数是正确的,要从RFC伪代码中获取此行为,您可以在查询字符串之前在相对URI中放置一个点作为路径,根据我的经验,使用相对URI作为网页中的链接是我期望的:

 transform(Base="http://example.com/something/more/long", R=".?query") => T="http://example.com/something/more/?query" 

但我希望,在网页中,“ http://example.com/something/more/long ”到“?query”页面上的链接会转到“ http://example.com/something/更多/更长?查询 “,而不是” http://example.com/something/more/?query “ – 换句话说,与RFC一致,但不与Java实现一致。

我对RFC的解读是否正确,Java方法是否与之不一致,或者我错过了什么?

是的,我同意URI.resolve(URI)方法与RFC 3986不兼容 。原始问题本身提供了大量的研究,有助于得出这个结论。 首先,让我们澄清任何困惑。

正如Raedwald所解释的那样(在一个现在已删除的答案中),以/结尾或不结束的基本路径之间存在区别:

  • fizz相对于/foo/bar是: /foo/fizz
  • fizz相对于/foo/bar/是: /foo/bar/fizz

虽然正确,但这不是一个完整的答案,因为最初的问题不是询问路径 (即上面的“嘶嘶声”)。 相反,该问题涉及相对URI引用的单独查询组件 。 示例代码中使用的URI类构造函数接受五个不同的String参数,而除queryString参数之外的所有参数都作为null传递。 (请注意,Java接受一个空字符串作为路径参数,这在逻辑上会导致“空”路径组件,因为“ 路径组件永远不会被定义 ”,尽管它“ 可能是空的(零长度) ”。)这在以后会很重要。

在之前的评论中 ,Sajan Chandran指出java.net.URI类被记录为实现RFC 2396而不是问题的主题RFC 3986 。 前者在2005年被后者淘汰 .URL类Javadoc没有提到新的RFC可能被解释为其不兼容性的更多证据。 让我们再说一些:

  • JDK-6791060是一个开放性问题,表明此类“应针对RFC 3986进行更新”。 那里的评论警告说“RFC3986并不完全向后兼容2396”。

  • 之前的尝试是更新部分URI类以符合RFC 3986,例如JDK-6348622 ,但随后又回滚以破坏向后兼容性。 (另请参阅JDK邮件列表中的讨论 。)

  • 虽然路径“合并”逻辑听起来类似,如SubOptimal所述,新RFC中指定的伪代码与实际实现不匹配。 在伪代码中,当相对URI的路径为空时 ,生成的目标路径将按原样从基URI复制 。 在这些条件下不执行“合并”逻辑。 与该规范相反,Java的URI实现修剪了最后一个/字符之后的基本路径 ,如问题中所示。

如果您需要RFC 3986行为,则可以使用URI类的替代方法。 Java EE 6实现提供了javax.ws.rs.core.UriBuilder ,它(在Jersey 1.18中)似乎表现得像你期望的那样(见下文)。 就编码不同的URI组件而言,它至少声称对RFC的认识。

在J2EE之外,Spring 3.0引入了UriUtils ,专门针对“基于RFC 3986的编码和解码”进行了记录。 Spring 3.1弃用了部分function并引入了UriComponentsBuilder ,但遗憾的是它没有记录对任何特定RFC的遵守情况。


测试程序,展示不同的行为:

 import java.net.*; import java.util.*; import java.util.function.*; import javax.ws.rs.core.UriBuilder; // using Jersey 1.18 public class StackOverflow22203111 { private URI withResolveURI(URI base, String targetQuery) { URI reference = queryOnlyURI(targetQuery); return base.resolve(reference); } private URI withUriBuilderReplaceQuery(URI base, String targetQuery) { UriBuilder builder = UriBuilder.fromUri(base); return builder.replaceQuery(targetQuery).build(); } private URI withUriBuilderMergeURI(URI base, String targetQuery) { URI reference = queryOnlyURI(targetQuery); UriBuilder builder = UriBuilder.fromUri(base); return builder.uri(reference).build(); } public static void main(String... args) throws Exception { final URI base = new URI("http://example.com/something/more/long"); final String queryString = "query=http://local:282/rand&action=aaaa"; final String expected = "http://example.com/something/more/long?query=http://local:282/rand&action=aaaa"; StackOverflow22203111 test = new StackOverflow22203111(); Map> strategies = new LinkedHashMap<>(); strategies.put("URI.resolve(URI)", test::withResolveURI); strategies.put("UriBuilder.replaceQuery(String)", test::withUriBuilderReplaceQuery); strategies.put("UriBuilder.uri(URI)", test::withUriBuilderMergeURI); strategies.forEach((name, method) -> { System.out.println(name); URI result = method.apply(base, queryString); if (expected.equals(result.toString())) { System.out.println(" MATCHES: " + result); } else { System.out.println(" EXPECTED: " + expected); System.out.println(" but WAS: " + result); } }); } private URI queryOnlyURI(String queryString) { try { String scheme = null; String authority = null; String path = null; String fragment = null; return new URI(scheme, authority, path, queryString, fragment); } catch (URISyntaxException syntaxError) { throw new IllegalStateException("unexpected", syntaxError); } } } 

输出:

 URI.resolve(URI) EXPECTED: http://example.com/something/more/long?query=http://local:282/rand&action=aaaa but WAS: http://example.com/something/more/?query=http://local:282/rand&action=aaaa UriBuilder.replaceQuery(String) MATCHES: http://example.com/something/more/long?query=http://local:282/rand&action=aaaa UriBuilder.uri(URI) MATCHES: http://example.com/something/more/long?query=http://local:282/rand&action=aaaa 

对我来说没有任何差异。 随着Java的行为。

在RFC2396 5.2.6a中

除了基URI的路径组件的最后一段之外的所有段都被复制到缓冲区。 换句话说, 排除最后(最右侧)斜杠字符后面的任何字符(如果有)。

在RFC3986中5.2.3

返回一个字符串,该字符串由引用的路径组件组成,该路径组件附加到除基本URI路径的最后一段之外的所有路径(即, 排除基本URI路径中最右侧/“之后的任何字符”,如果是,则排除整个基本URI路径不包含任何“/”字符)。