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

Posted

技术标签:

【中文标题】当相对 URI 包含空路径时,Java 的 URI.resolve 是不是与 RFC 3986 不兼容?【英文标题】:Is Java's URI.resolve incompatible with RFC 3986 when the relative URI contains an empty path?当相对 URI 包含空路径时,Java 的 URI.resolve 是否与 RFC 3986 不兼容? 【发布时间】:2014-04-07 19:51:14 【问题描述】:

我认为Java的URI.resolve方法的定义和实现与RFC 3986 section 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

我对@9​​87654323@的理解是,如果相对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 方法与之不一致,还是我遗漏了什么?

【问题讨论】:

JDK1.6中的URI类实现了ietf.org/rfc/rfc2396.txt而不是rfc3986中定义的定义。 是的,不兼容。如果您需要兼容的解决方案 - 请查看 example。 【参考方案1】:

是的,我同意 URI.resolve(URI) 方法与 RFC 3986 不兼容。 就其本身而言,最初的问题提出了很棒的大量研究,有助于这个结论。首先,让我们澄清一下所有的困惑。

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

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

虽然正确,但它不是一个完整的答案,因为最初的问题是没有询问path(即上面的“fizz”)。相反,问题与相对 URI 引用的单独 query component 有关。 URI 类constructor used in the example code 接受五个不同的字符串参数,并且除了queryString 参数之外的所有参数都作为null 传递。 (请注意,Java 接受一个空字符串作为路径参数,这在逻辑上会导致一个“空”路径组件,因为“the path component is never undefined”虽然它是“may be empty (zero length)”。)这在以后很重要。

在earlier comment 中,Sajan Chandran 指出java.net.URI class 被记录为实现RFC 2396 而不是问题的主题RFC 3986。前者在 2005 年被后者淘汰。URI 类 Javadoc 没有提及较新的 RFC 可以解释为它不兼容的更多证据。让我们再补充一点:

JDK-6791060 是一个未解决的问题,表明此类“应针对 RFC 3986 进行更新”。那里的评论警告说“RFC3986 并不完全倒退 兼容2396"。

之前曾尝试更新 URI 类的某些部分以符合 RFC 3986,例如 JDK-6348622,但后来为了打破向后兼容性而改为 rolled back。 (另请参阅 JDK 邮件列表中的 this discussion。)

虽然路径“合并”逻辑听起来与noted by SubOptimal 相似,但较新的RFC 中指定的伪代码与actual implementation 不匹配。在伪代码中,当相对 URI 的路径为 empty 时,生成的 目标路径会按原样从基本 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 ***22203111 

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

        ***22203111 test = new ***22203111();
        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

【讨论】:

【参考方案2】:

如果您希望URI.resolve() 提供更好的1 行为并且不想在您的程序中包含另一个大型依赖项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);

为了便于阅读,唯一非 JDK 的东西是来自 Guava 的 Strings - 如果您没有 Guava,请用您自己的 1 行方法替换。

脚注:

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

【讨论】:

【参考方案3】:

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

在 RFC2396 5.2.6a 中

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

在 RFC3986 5.2.3 中

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

【讨论】:

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

@Guss 提出的解决方案是一个足够好的解决方案,但不幸的是,其中存在 Guava 依赖项和一些小错误。

这是对他的解决方案的重构,消除了 Guava 依赖和错误。我用它来代替 URI.resolve() 并将它放在我的一个名为 URIUtils 的帮助器类中,以及如果它不是 final 时将成为扩展 URI 类的一部分的其他方法。

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/#

【讨论】:

以上是关于当相对 URI 包含空路径时,Java 的 URI.resolve 是不是与 RFC 3986 不兼容?的主要内容,如果未能解决你的问题,请参考以下文章

绝对 URI 中的相对路径:java.net.URI.checkPath(URI.java:1823)

失败:IllegalArgumentException java.net.URISyntaxException:绝对 URI 中的相对路径:

Java中路径的获取总结以及URL和URI的区别

java.lang.IllegalArgumentException:java.net.URISyntaxException:绝对 URI 中的相对路径:用于 Talend 中的 Hbase

读取 csv 文件时 MS Databricks Spark 中绝对 URI 中的相对路径

Databricks 上的 PySpark 在绝对 URI 中获取相对路径:尝试使用 DateStamps 读取 Json 文件时