Retrofit之baseUrl与Endpoint的combination

我们在使用retrofit的时候通常会使用Retrofit的baseUrl(baseUrl)来设置服务器的地址或者是后台API的公共路径;然后再在具体的API的请求方法上通过@GET、@POST等注解设置不同的相对路径(Endpoint)。

相信不少同学也曾经试过一不小心在复制baseUrl或Endpoint时,baseUrl尾部少了个'/',又或者Endpoint多复制了个'/'开头;然后就出现了类似404这种奇怪的错误。比如面这种情况:

baseUrl = http://example.com/api/
endpoint = /foo/bar/
result = http://example.com/foo/bar/

这字符串拼接的结果应该就是baseUrl + endpoint = " http://example.com/api/foo/bar/",为毛这中间丢失了api这部分路径的呢?这是不是Retrofit出现了Bug啊还是哪里抽风了???
---- 其实这是错误使用endpoint导致跟我们预期结果不一致的现象,因为我们的endpoint是以'/'开头,其含义是endpoint是一个绝对路径,然后在与baseUrl合并的时候就会把baseUrl主机后的部分路径给清除掉。

其实我们在使用baseUrl与Endpoint合并使用的时候需要注意以下几点:

  1. baseUrl必须以“/”结尾,否则主机最后面的部分路径将被忽略掉
  2. endpoint以“/”开始的话表示绝对路径,也就是baseURL只有服务器主机地址之前的部分将被继续使用
  3. endpoint包含主机地址时,最终主机地址将使用endpoint的地址,而scheme将沿用baseUrl的
  4. endpoint是一个完成的路径时,则整个替换baseUrl

在Retrofit的源码里面注解是这么描述关于baseUrl的用法的:

  /**
    * Set the API base URL.
    *
    * <p>The specified endpoint values (such as with {@link GET @GET}) are resolved against this
    * value using {@link HttpUrl#resolve(String)}. The behavior of this matches that of an {@code
    * <a href="">} link on a website resolving on the current URL.
    *
    * <p><b>Base URLs should always end in {@code /}.</b>
    *
    * <p>A trailing {@code /} ensures that endpoints values which are relative paths will correctly
    * append themselves to a base which has path components.
    *
    * <p><b>Correct:</b><br>
    * Base URL: http://example.com/api/<br>
    * Endpoint: foo/bar/<br>
    * Result: http://example.com/api/foo/bar/
    *
    * <p><b>Incorrect:</b><br>
    * Base URL: http://example.com/api<br>
    * Endpoint: foo/bar/<br>
    * Result: http://example.com/foo/bar/
    *
    * <p>This method enforces that {@code baseUrl} has a trailing {@code /}.
    *
    * <p><b>Endpoint values which contain a leading {@code /} are absolute.</b>
    *
    * <p>Absolute values retain only the host from {@code baseUrl} and ignore any specified path
    * components.
    *
    * <p>Base URL: http://example.com/api/<br>
    * Endpoint: /foo/bar/<br>
    * Result: http://example.com/foo/bar/
    *
    * <p>Base URL: http://example.com/<br>
    * Endpoint: /foo/bar/<br>
    * Result: http://example.com/foo/bar/
    *
    * <p><b>Endpoint values may be a full URL.</b>
    *
    * <p>Values which have a host replace the host of {@code baseUrl} and values also with a scheme
    * replace the scheme of {@code baseUrl}.
    *
    * <p>Base URL: http://example.com/<br>
    * Endpoint: https://github.com/square/retrofit/<br>
    * Result: https://github.com/square/retrofit/
    *
    * <p>Base URL: http://example.com<br>
    * Endpoint: //github.com/square/retrofit/<br>
    * Result: http://github.com/square/retrofit/ (note the scheme stays 'http')
    */
   public Builder baseUrl(HttpUrl baseUrl) {
     Objects.requireNonNull(baseUrl, "baseUrl == null");
     List<String> pathSegments = baseUrl.pathSegments();
     if (!"".equals(pathSegments.get(pathSegments.size() - 1))) {
       throw new IllegalArgumentException("baseUrl must end in /: " + baseUrl);
     }
     this.baseUrl = baseUrl;
     return this;
   }

那么baseUrl跟Endpoint它们是怎么组合拼接的呢?

retrofit是通过:url = baseUrl.resolve(relativeUrl);来将baseUrl跟Endpoint组合拼接的,resolve方法是okhttp3中HttpUrl类的一个方法,最终是由HttpUrl的Builder parse(@Nullable HttpUrl base, String input);方法来完成组合拼接任务。

  • 首先我们先来了解下一个url的组成:
    一个完整的URL是这样子的:
    URL = scheme://username:password@host:Port/PathSegment?QueryParameter#fragment
    通常我们平时使用的URL中很少会带有username:password这个Authority部分的,所以我们常见的URL是这个样子的:
    URL = scheme://host:Port/PathSegment?QueryParameter#fragment
    scheme :http/https协议
    Authority :username:password
    host : 主机地址
    Port :端口号;http默认为80,https默认为443
    PathSegment :路径
    QueryParameter :查询参数,我们的get请求参数就是在这个部分
    fragment :fragment不会发送到服务器,只是在客户端中使用
  • 组合拼接流程:
    1. 先通过查找Endpoint中的第一个':'的位置,当存在且长度为5则为"http:",为6则是"https:";当找不到时则使用baseUrl的scheme
    2. 当Endpoint剩余部分包含两个以上'/' '\'符号,或baseUrl为空或者baseUrl的scheme与Endpoint不一致时,Authority及主机端口路径等都将从Endpoint中解析读取;否则直接取baseUrl中的值
    3. Authority信息的处理,通过查找'@'符号的存在以及该符号前是否包含账号密码分隔符':'来确认Endpoint是否有用户账号及密码信息
    4. 当前位置到下一个':'部分就是主机部分了
    5. 主机部分后到下一个分隔符若可以解析为整型且在(0,65535]范围内则为端口号,否则端口号为默认值,按照scheme来负值
    6. 解析路径PathSegment部分,当前剩余部分到分隔符'?''#'间的部分为路径部分,当路径部分以'/' 或'\'开始的话则会将之前的PathSegment部分给reset,否则将之前最后一个PathSegment置为空串 👆上面需要注意的1、2点原因见下代码可知
      private void resolvePath(String input, int pos, int limit) {
        // Read a delimiter.
        if (pos == limit) {
            // Empty path: keep the base path as-is.
            return;
        }
        char c = input.charAt(pos);
        if (c == '/' || c == '\\') {
          // Absolute path: reset to the default "/".
          encodedPathSegments.clear();
          encodedPathSegments.add("");
          pos++;
        } else {
          // Relative path: clear everything after the last '/'.
          encodedPathSegments.set(encodedPathSegments.size() - 1, "");
        }
      
        // Read path segments.
        for (int i = pos; i < limit; ) {
          int pathSegmentDelimiterOffset = delimiterOffset(input, i, limit, "/\\");
          boolean segmentHasTrailingSlash = pathSegmentDelimiterOffset < limit;
          push(input, i, pathSegmentDelimiterOffset, segmentHasTrailingSlash, true);
          i = pathSegmentDelimiterOffset;
          if (segmentHasTrailingSlash) i++;
        }
      }
      
    7. 分割符'?'往后'#'之前部分则为查询参数QueryParameter
    8. 剩下的分隔符'#'开始往后部分就是fragment部分了

由于这部分代码比较多就不贴出来了,更多源码内容可以见:

https://github.com/square/retrofit

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容