Java的URI.resolve方法在相对路径为空时是否与RFC 3986不兼容?

15
我相信Java的URI.resolve方法的定义和实现与RFC 3986第5.2.2节不兼容。我知道Java API定义了该方法的工作原理,如果现在更改它将会破坏现有的应用程序,但我的问题是: 有人能确认我的理解,这个方法与RFC 3986不兼容吗? 我正在使用这个问题中的示例:java.net.URI resolve against only query string,我将在此处复制它:

我正在尝试使用JDK java.net.URI构建URI。 我想将查询(字符串形式)附加到绝对URI对象中。 例如:

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);

理论上(或者我认为)resolve应该返回:
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中在查询字符串之前放置一个点作为路径,根据我在Web页面中使用相对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/more/long?query”,而不是“http://example.com/something/more/?query”,也就是说,遵循RFC的规定,但与Java实现不符。
我的理解是否正确,RFC和Java的方法是否不一致,还是我漏掉了什么?"

JDK1.6中的URI类实现了http://www.ietf.org/rfc/rfc2396.txt中定义的定义,而不是rfc3986。 - Sajan Chandran
是的,不兼容。如果您需要兼容的解决方案,请查看示例 - ursa
4个回答

16
是的,我同意URI.resolve(URI)方法与RFC 3986不兼容。原始问题本身就提供了大量研究,有助于得出这个结论。首先,让我们澄清任何困惑。正如Raedwald解释的那样(在现在已删除的答案中),基路径以/结尾或不以/结尾之间存在区别:
  • fizz相对于/foo/bar是:/foo/fizz
  • fizz相对于/foo/bar/是:/foo/bar/fizz
虽然是正确的,但这不是一个完整的答案,因为原问题并没有询问关于path(即上面的"fizz")的内容。相反,问题涉及到相对URI引用的独立查询组件。在示例代码中使用的URI类构造函数接受五个不同的字符串参数,除了queryString参数之外,所有参数都被传递为null。(请注意,Java将空字符串作为路径参数,并且这在逻辑上会导致“空”路径组件,因为“路径组件从未未定义”,尽管它“可能为空(零长度)”。)这将在稍后变得重要。

早先的评论中,Sajan Chandran指出java.net.URI的文档实现了RFC 2396而不是问题的主题RFC 3986。前者在2005年被后者废弃。URI类Javadoc未提及较新的RFC可能被解释为其不兼容的更多证据。让我们再加一些:

  • JDK-6791060建议更新此类以符合RFC 3986。其中的一条评论警告“RFC3986与2396不完全向后兼容”。该问题在2018年被关闭,因为它是JDK-8019345的重复项(截至2022年10月仍未解决,自2013年以来没有显着活动)。

  • 之前曾尝试更新URI类的部分内容以符合RFC 3986,例如JDK-6348622,但由于破坏了向后兼容性而被rolled back。(还请参阅JDK邮件列表上的this discussion。)

  • 尽管路径“合并”逻辑听起来相似,如noted by SubOptimal,但较新的RFC中指定的伪代码与actual implementation不匹配。在伪代码中,当相对URI的路径为空时,目标路径将从基本URI中直接复制。在这些条件下,伪代码的“合并”逻辑不会执行。与该规范相反,Java的URI实现在最后一个/字符后修剪基本路径,正如问题所观察到的那样。

如果您想要符合RFC 3986标准的行为,URI类有替代方案。Java EE 6到EE 8实现提供了javax.ws.rs.core.UriBuilder,在Jersey 1.18中似乎表现符合您的预期(见下文)。至少在编码不同URI组件方面,它声称意识到RFC的规定。随着从JavaEE到JakartaEE 9(约2020年)的转换,该类移动到jakarta.ws.rs.core.UriBuilder

除了J2EE之外,Spring 3.0引入了UriUtils,专门用于“基于RFC 3986的编码和解码”。不幸的是,Spring 3.1已弃用了部分该功能,并引入了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<String, BiFunction<URI, String, URI>> 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

1

如果你希望1URI.resolve()获得更好的行为,并且不想在程序中包含另一个大型依赖项2,那么我发现以下代码可以满足我的要求:

public URI resolve(URI base, URI relative) {
    if (Strings.isNullOrEmpty(base.getPath()))
        base = new URI(base.getScheme(), base.getAuthority(), "/",
            base.getQuery(), base.getFragment());
    if (Strings.isNullOrEmpty(uri.getPath()))
        uri = new URI(uri.getScheme(), uri.getAuthority(), base.getPath(),
            uri.getQuery(), uri.getFragment());
    return base.resolve(uri);
}

除了可读性更好的Guava中的Strings,没有其他非JDK的东西 - 如果您没有Guava,请用自己的一行代码替换。

脚注:

  1. 我不能声称这里的简单代码示例符合RFC3986规范。
  2. 例如Spring、javax.ws或 - 如this answer中所提到的 - Apache HTTPClient。

0

@Guss提出的解决方案是一个足够好的解决方法,但不幸的是,它有一个Guava依赖和一些小错误。

这是他的解决方案的重构,去除了Guava依赖和错误。我用它来替换URI.resolve()并将其放在我的帮助类URIUtils中,与其他方法一起成为扩展URI类的一部分,如果它不是final的话。

public static URI resolve(URI base, URI uri) throws URISyntaxException {
  if (base.getPath() == null || base.getPath().isEmpty())
    base = new URI(base.getScheme(), base.getAuthority(), "/", base.getQuery(), base.getFragment());
  if (uri.getPath() == null || uri.getPath().isEmpty())
    uri = new URI(uri.getScheme(), uri.getAuthority(), base.getPath(), uri.getQuery(), uri.getFragment());
  return base.resolve(uri);
}

通过比较一些常见的陷阱的输出,可以很容易地检查它是否在URI.resolve()周围工作。

public static void main(String[] args) throws URISyntaxException {
  URI host = new URI("https://www.test.com");

  URI uri = new URI("mypage.html");
  System.out.println(host.resolve(uri));
  System.out.println(URIUtils.resolve(host, uri));
  System.out.println();

  uri = new URI("./mypage.html");
  System.out.println(host.resolve(uri));
  System.out.println(URIUtils.resolve(host, uri));
  System.out.println();

  uri = new URI("#");
  System.out.println(host.resolve(uri));
  System.out.println(URIUtils.resolve(host, uri));
  System.out.println();

  uri = new URI("#second_block");
  System.out.println(host.resolve(uri));
  System.out.println(URIUtils.resolve(host, uri));
  System.out.println();
}

https://www.test.commypage.html
https://www.test.com/mypage.html

https://www.test.commypage.html
https://www.test.com/mypage.html

https://www.test.com#
https://www.test.com/#

0

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

在RFC2396 5.2.6a中

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

在RFC3986 5.2.3中

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


RFC3986第5.2.3节描述了如何执行“合并”操作,正如您引用的那样,但OP正在询问第5.2.2节中的伪代码,该伪代码似乎表明,如果参考路径(R.path)组件为空,则不执行该合并,并使用不同的逻辑。 - William Price
@WilliamPrice 我给你的回答点赞,因为它解释了所有要点。 :-) - SubOptimal

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