14

我认为 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 中放置一个点作为路径,根据我在网页中使用相对 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 方法与之不一致,还是我遗漏了什么?

4

4 回答 4

14

是的,我同意该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 年被后者淘汰。URI 类 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 弃用了其中的一些功能并引入了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
于 2014-12-25T05:41:07.233 回答
1

如果您想要更好的1行为URI.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);
}

为了可读性,唯一的非 JDKStrings来自 Guava - 如果您没有 Guava,请用您自己的 1-line-method 替换。

脚注:

  1. 我不能声称这里的简单代码示例符合 RFC3986。
  2. 例如 Spring、javax.ws 或 - 如本答案所述- Apache HTTPClient。
于 2020-05-03T16:35:41.630 回答
0

对我来说没有差异。使用 Java 行为。

在 RFC2396 5.2.6a

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

在 RFC3986 5.2.3

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

于 2014-12-19T10:50:32.413 回答
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/#
于 2021-10-03T18:31:32.367 回答